first commit

This commit is contained in:
Roman Pyrih
2025-07-11 12:34:24 +02:00
commit 296b13244b
10181 changed files with 3916595 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace Elementor\Modules\GlobalClasses\Database;
use Elementor\Core\Database\Base_Database_Updater;
use Elementor\Modules\GlobalClasses\Database\Migrations\Add_Capabilities;
class Global_Classes_Database_Updater extends Base_Database_Updater {
const DB_VERSION = 1;
const OPTION_NAME = 'elementor_global_classes_db_version';
protected function get_migrations(): array {
return [
1 => new Add_Capabilities(),
];
}
protected function get_db_version() {
return static::DB_VERSION;
}
protected function get_db_version_option_name(): string {
return static::OPTION_NAME;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\Modules\GlobalClasses\Database\Migrations;
use Elementor\Core\Database\Base_Migration;
class Add_Capabilities extends Base_Migration {
const UPDATE_CLASS = 'elementor_global_classes_update_class';
const REMOVE_CSS_CLASS = 'elementor_global_classes_remove_class';
const APPLY_CSS_CLASS = 'elementor_global_classes_apply_class';
public function up() {
$capabilities = [
self::UPDATE_CLASS => [ 'administrator' ],
self::REMOVE_CSS_CLASS => [ 'administrator', 'editor', 'author', 'contributor', 'shop_manager' ],
self::APPLY_CSS_CLASS => [ 'administrator', 'editor', 'author', 'contributor', 'shop_manager' ],
];
foreach ( $capabilities as $capability => $roles ) {
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->add_cap( $capability );
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Global_Classes_Changes_Resolver {
private Global_Classes_Repository $repository;
private Collection $added;
private Collection $deleted;
private Collection $modified;
public function __construct( Global_Classes_Repository $repository, array $changes ) {
$this->repository = $repository;
$this->added = Collection::make( $changes['added'] ?? [] );
$this->deleted = Collection::make( $changes['deleted'] ?? [] );
$this->modified = Collection::make( $changes['modified'] ?? [] );
}
public static function make( Global_Classes_Repository $repository, array $changes ): self {
return new self( $repository, $changes );
}
public function resolve_items( array $payload ) {
$touched = $this->added->merge( $this->modified )->values();
$items_to_save = Collection::make( $payload )->only( $touched );
return $this->repository
->all()
->get_items()
->except( $this->deleted->values() )
->merge( $items_to_save )
->all();
}
public function resolve_order( array $payload ) {
$payload = Collection::make( $payload );
$current_order = $this->repository->all()->get_order();
$missing_in_current_order = $payload
->filter( fn( $item ) => ! $this->added->contains( $item ) )
->diff( $current_order );
$payload = $payload->filter( fn( $item ) => ! $missing_in_current_order->contains( $item ) );
$missing_in_payload = $current_order
->filter( fn( $item ) => ! $this->deleted->contains( $item ) )
->diff( $payload );
return $missing_in_payload
->merge( $payload )
->all();
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Base\Document;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Global_Classes_Cleanup {
public function register_hooks() {
add_action(
'elementor/global_classes/update',
fn( $context, $new_value, $prev_value ) => $this->on_classes_update( $new_value, $prev_value ),
10,
3
);
}
private function on_classes_update( $new_value, $prev_value ) {
$deleted_classes_ids = $this->get_deleted_classes_ids( $new_value, $prev_value );
if ( ! empty( $deleted_classes_ids ) ) {
Plugin::$instance->db->iterate_elementor_documents(
fn( $document, $elements_data ) => $this->unapply_deleted_classes( $document, $elements_data, $deleted_classes_ids )
);
}
}
private function get_deleted_classes_ids( $new_value, $prev_value ) {
$prev_ids = array_keys( $prev_value['items'] );
$new_ids = array_keys( $new_value['items'] );
return array_values(
array_diff( $prev_ids, $new_ids )
);
}
private function unapply_deleted_classes( $document, $elements_data, $deleted_classes_ids ) {
$elements_data = Plugin::$instance->db->iterate_data( $elements_data, function( $element_data ) use ( $deleted_classes_ids ) {
$element_type = Atomic_Elements_Utils::get_element_type( $element_data );
$element_instance = Atomic_Elements_Utils::get_element_instance( $element_type );
if ( ! Atomic_Elements_Utils::is_atomic_element( $element_instance ) ) {
return $element_data;
}
/** @var Atomic_Element_Base | Atomic_Widget_Base $element_instance */
return $this->unapply_classes_from_element( $element_instance->get_props_schema(), $element_data, $deleted_classes_ids );
} );
$document->update_json_meta( Document::ELEMENTOR_DATA_META_KEY, $elements_data );
}
private function unapply_classes_from_element( $props_schema, $element_data, $deleted_classes_ids ) {
foreach ( $props_schema as $prop ) {
if ( ! Atomic_Elements_Utils::is_classes_prop( $prop ) ) {
continue;
}
$current_classes = $element_data['settings'][ $prop->get_key() ] ?? null;
if ( ! $current_classes ) {
continue;
}
$element_data['settings'][ $prop->get_key() ]['value'] = Collection::make( $current_classes['value'] )
->filter( fn( $class ) => ! in_array( $class, $deleted_classes_ids, true ) )
->values();
}
return $element_data;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Modules\AtomicWidgets\Styles\Styles_Renderer;
use Elementor\Plugin;
class Global_Classes_CSS_File extends Post_CSS {
const FILE_PREFIX = 'global-classes-';
const META_KEY = '_elementor_css__global_classes';
public function __construct( $kit_id = null ) {
$kit_id = $kit_id ?? Plugin::$instance->kits_manager->get_active_id();
parent::__construct( $kit_id );
}
public function get_name() {
return 'global-classes';
}
protected function get_file_handle_id() {
return 'elementor-global-classes-' . $this->get_post_id();
}
protected function get_enqueue_dependencies() {
return [];
}
protected function get_inline_dependency() {
return '';
}
protected function render( $context ) {
$global_classes = Global_Classes_Repository::make()
->context( $context )
->all();
if ( $global_classes->get_items()->is_empty() ) {
return;
}
$sorted_items = $global_classes
->get_order()
->reverse()
->map(
fn( $id ) => $global_classes->get_items()->get( $id )
);
$css = Styles_Renderer::make(
Plugin::$instance->breakpoints->get_breakpoints_config()
)->on_prop_transform( function( $key, $value ) {
if ( 'font-family' !== $key ) {
return;
}
$this->add_font( $value );
} )->render(
$sorted_items->map( function( $item ) {
$item['cssName'] = $item['label'];
return $item;
} )->all()
);
$this->get_stylesheet()->add_raw_css( $css );
}
protected function render_css() {
$this->render( Global_Classes_Repository::CONTEXT_FRONTEND );
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\Modules\GlobalClasses;
class Global_Classes_CSS_Preview extends Global_Classes_CSS_File {
const FILE_PREFIX = 'global-classes-preview-';
private $meta_cache = [];
public function is_update_required() {
return true;
}
protected function load_meta() {
return $this->meta_cache;
}
protected function delete_meta() {
$this->meta_cache = [];
}
protected function update_meta( $meta ) {
$this->meta_cache = $meta;
}
protected function get_file_handle_id() {
return 'elementor-global-classes-preview-' . $this->get_post_id();
}
protected function render_css() {
$this->render( Global_Classes_Repository::CONTEXT_PREVIEW );
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Plugin;
class Global_Classes_CSS {
public function register_hooks() {
add_action(
'elementor/frontend/after_enqueue_styles',
fn() => $this->enqueue_styles()
);
add_action(
'elementor/global_classes/update',
fn( $context ) => $this->clear_css_cache( $context )
);
add_action(
'elementor/core/files/clear_cache',
fn() => $this->clear_css_cache( Global_Classes_Repository::CONTEXT_FRONTEND )
);
add_action(
'deleted_post',
fn( $post_id ) => $this->on_post_delete( $post_id )
);
add_action(
'elementor/core/files/after_generate_css',
fn() => $this->generate_styles()
);
add_filter('elementor/atomic-widgets/settings/transformers/classes',
fn( $value ) => $this->transform_classes_names( $value )
);
}
private function enqueue_styles() {
$css_file = is_preview()
? new Global_Classes_CSS_Preview()
: new Global_Classes_CSS_File();
$css_file->enqueue();
}
private function generate_styles() {
( new Global_Classes_CSS_File() )->update();
}
private function on_post_delete( $post_id ) {
if ( ! Plugin::$instance->kits_manager->is_kit( $post_id ) ) {
return;
}
$this->clear_css_cache(
Global_Classes_Repository::CONTEXT_FRONTEND,
$post_id
);
}
private function clear_css_cache( string $context, $kit_id = null ): void {
( new Global_Classes_CSS_Preview( $kit_id ) )->delete();
if ( Global_Classes_Repository::CONTEXT_FRONTEND === $context ) {
( new Global_Classes_CSS_File( $kit_id ) )->delete();
}
}
private function transform_classes_names( $ids ) {
$context = is_preview() ? Global_Classes_Repository::CONTEXT_PREVIEW : Global_Classes_Repository::CONTEXT_FRONTEND;
$classes = Global_Classes_Repository::make()
->context( $context )
->all()
->get_items();
return array_map(
function( $id ) use ( $classes ) {
$class = $classes->get( $id );
return $class ? $class['label'] : $id;
},
$ids
);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Module;
use Elementor\Modules\AtomicWidgets\Parsers\Parse_Result;
use Elementor\Modules\AtomicWidgets\Parsers\Style_Parser;
use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
use Elementor\Plugin;
class Global_Classes_Parser {
public static function make() {
return new static();
}
public function parse( $data ): Parse_Result {
$result = Parse_Result::make();
if ( ! isset( $data['items'] ) ) {
$result->errors()->add( 'items', 'missing' );
return $result;
}
if ( ! isset( $data['order'] ) ) {
$result->errors()->add( 'order', 'missing' );
return $result;
}
$items = $data['items'];
$order = $data['order'];
if ( ! is_array( $items ) ) {
$result->errors()->add( 'items', 'invalid' );
return $result;
}
if ( ! is_array( $order ) ) {
$result->errors()->add( 'order', 'invalid' );
return $result;
}
$items_result = $this->parse_items( $items );
if ( ! $items_result->is_valid() ) {
$result->errors()->merge( $items_result->errors(), 'items' );
return $result;
}
$order_result = $this->parse_order( $order, $items_result->unwrap() );
if ( ! $order_result->is_valid() ) {
$result->errors()->merge( $order_result->errors(), 'order' );
return $result;
}
return $result->wrap( [
'items' => $items_result->unwrap(),
'order' => $order_result->unwrap(),
] );
}
public function parse_items( array $items ) {
$sanitized_items = [];
$result = Parse_Result::make();
$style_parser = Style_Parser::make( Style_Schema::get() );
$existing_labels = [];
foreach ( $items as $item_id => $item ) {
$item_result = $style_parser->parse( $item );
if ( ! $item_result->is_valid() ) {
$result->errors()->merge( $item_result->errors(), $item_id );
continue;
}
$sanitized_item = $item_result->unwrap();
if ( $item_id !== $sanitized_item['id'] ) {
$result->errors()->add( "$item_id.id", 'mismatching_value' );
continue;
}
if ( Plugin::$instance->experiments->is_feature_active( Module::EXPERIMENT_VERSION_3_30 ) ) {
if ( in_array( $sanitized_item['label'], $existing_labels, true ) ) {
$result->errors()->add( "$item_id.id", 'duplicated_class_label' );
continue;
}
}
$sanitized_items[ $sanitized_item['id'] ] = $sanitized_item;
$existing_labels[] = $sanitized_item['label'];
}
return $result->wrap( $sanitized_items );
}
public function parse_order( array $order, array $items ): Parse_Result {
$result = Parse_Result::make();
$items = Collection::make( $items );
$order = Collection::make( $order )
->filter( fn( $item ) => is_string( $item ) )
->unique();
$existing_ids = $items->keys();
$excess_ids = $order->diff( $existing_ids );
$missing_ids = $existing_ids->diff( $order );
$excess_ids->each( fn( $id ) => $result->errors()->add( $id, 'excess' ) );
$missing_ids->each( fn( $id ) => $result->errors()->add( $id, 'missing' ) );
return $result->is_valid()
? $result->wrap( $order->values() )
: $result;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Base\Document;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Global_Classes_Repository {
const META_KEY_FRONTEND = '_elementor_global_classes';
const META_KEY_PREVIEW = '_elementor_global_classes_preview';
const CONTEXT_FRONTEND = 'frontend';
const CONTEXT_PREVIEW = 'preview';
private string $context = self::CONTEXT_FRONTEND;
public static function make(): Global_Classes_Repository {
return new self();
}
public function context( string $context ): self {
$this->context = $context;
return $this;
}
public function all() {
$meta_key = $this->get_meta_key();
$all = $this->get_kit()->get_json_meta( $meta_key );
$is_preview = static::META_KEY_PREVIEW === $meta_key;
$is_empty = empty( $all );
if ( $is_preview && $is_empty ) {
$all = $this->get_kit()->get_json_meta( static::META_KEY_FRONTEND );
}
return Global_Classes::make( $all['items'] ?? [], $all['order'] ?? [] );
}
public function put( array $items, array $order ) {
$current_value = $this->all()->get();
$updated_value = [
'items' => $items,
'order' => $order,
];
// `update_metadata` fails for identical data, so we skip it.
if ( $current_value === $updated_value ) {
return;
}
$meta_key = $this->get_meta_key();
$value = $this->get_kit()->update_json_meta( $meta_key, $updated_value );
$should_delete_preview = static::META_KEY_FRONTEND === $meta_key;
if ( $should_delete_preview ) {
$this->get_kit()->delete_meta( static::META_KEY_PREVIEW );
}
if ( ! $value ) {
throw new \Exception( 'Failed to update global classes' );
}
do_action( 'elementor/global_classes/update', $this->context, $updated_value, $current_value );
}
private function get_meta_key(): string {
return static::CONTEXT_FRONTEND === $this->context
? static::META_KEY_FRONTEND
: static::META_KEY_PREVIEW;
}
private function get_kit() {
return Plugin::$instance->kits_manager->get_active_kit();
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Modules\GlobalClasses\Utils\Error_Builder;
use Elementor\Modules\GlobalClasses\Utils\Response_Builder;
use Elementor\Modules\GlobalClasses\Database\Migrations\Add_Capabilities;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Global_Classes_REST_API {
const API_NAMESPACE = 'elementor/v1';
const API_BASE = 'global-classes';
const MAX_ITEMS = 50;
private $repository = null;
public function register_hooks() {
add_action( 'rest_api_init', fn() => $this->register_routes() );
}
private function get_repository() {
if ( ! $this->repository ) {
$this->repository = new Global_Classes_Repository();
}
return $this->repository;
}
private function register_routes() {
register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
[
'methods' => 'GET',
'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->all( $request ) ),
'permission_callback' => fn() => true,
'args' => [
'context' => [
'type' => 'string',
'required' => false,
'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
'enum' => [
Global_Classes_Repository::CONTEXT_FRONTEND,
Global_Classes_Repository::CONTEXT_PREVIEW,
],
],
],
],
] );
register_rest_route( self::API_NAMESPACE, '/' . self::API_BASE, [
[
'methods' => 'PUT',
'callback' => fn( $request ) => $this->route_wrapper( fn() => $this->put( $request ) ),
'permission_callback' => fn() => current_user_can( Add_Capabilities::UPDATE_CLASS ),
'args' => [
'context' => [
'type' => 'string',
'required' => false,
'default' => Global_Classes_Repository::CONTEXT_FRONTEND,
'enum' => [
Global_Classes_Repository::CONTEXT_FRONTEND,
Global_Classes_Repository::CONTEXT_PREVIEW,
],
],
'changes' => [
'type' => 'object',
'required' => true,
'additionalProperties' => false,
'properties' => [
'added' => [
'type' => 'array',
'required' => true,
'items' => [ 'type' => 'string' ],
],
'deleted' => [
'type' => 'array',
'required' => true,
'items' => [ 'type' => 'string' ],
],
'modified' => [
'type' => 'array',
'required' => true,
'items' => [ 'type' => 'string' ],
],
],
],
'items' => [
'required' => true,
'type' => 'object',
'additionalProperties' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'required' => true,
],
'variants' => [
'type' => 'array',
'required' => true,
],
'type' => [
'type' => 'string',
'enum' => [ 'class' ],
'required' => true,
],
'label' => [
'type' => 'string',
'required' => true,
],
],
],
],
'order' => [
'required' => true,
'type' => 'array',
'items' => [
'type' => 'string',
],
],
],
],
] );
}
private function all( \WP_REST_Request $request ) {
$context = $request->get_param( 'context' );
$classes = $this->get_repository()->context( $context )->all();
return Response_Builder::make( (object) $classes->get_items()->all() )
->set_meta( [ 'order' => $classes->get_order()->all() ] )
->build();
}
private function put( \WP_REST_Request $request ) {
$parser = Global_Classes_Parser::make();
$items_result = $parser->parse_items(
$request->get_param( 'items' )
);
$items_count = count( $items_result->unwrap() );
if ( $items_count >= static::MAX_ITEMS ) {
return Error_Builder::make( 'global_classes_limit_exceeded' )
->set_status( 400 )
->set_message( sprintf(
__( 'Global classes limit exceeded. Maximum allowed: %d', 'elementor' ),
static::MAX_ITEMS
) )
->build();
}
if ( ! $items_result->is_valid() ) {
return Error_Builder::make( 'invalid_items' )
->set_status( 400 )
->set_message( 'Invalid items: ' . $items_result->errors()->to_string() )
->build();
}
$order_result = $parser->parse_order(
$request->get_param( 'order' ),
$items_result->unwrap()
);
if ( ! $order_result->is_valid() ) {
return Error_Builder::make( 'invalid_order' )
->set_status( 400 )
->set_message( 'Invalid order: ' . $order_result->errors()->to_string() )
->build();
}
$repository = $this->get_repository()
->context( $request->get_param( 'context' ) );
$changes_resolver = Global_Classes_Changes_Resolver::make(
$repository,
$request->get_param( 'changes' ) ?? [],
);
$repository->put(
$changes_resolver->resolve_items( $items_result->unwrap() ),
$changes_resolver->resolve_order( $order_result->unwrap() ),
);
return Response_Builder::make()->no_content()->build();
}
private function route_wrapper( callable $cb ) {
try {
$response = $cb();
} catch ( \Exception $e ) {
return Error_Builder::make( 'unexpected_error' )
->set_message( __( 'Something went wrong', 'elementor' ) )
->build();
}
return $response;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Utils\Collection;
class Global_Classes {
private Collection $items;
private Collection $order;
public static function make( array $items = [], array $order = [] ) {
return new static( $items, $order );
}
private function __construct( array $data = [], array $order = [] ) {
$this->items = Collection::make( $data );
$this->order = Collection::make( $order );
}
public function get_items() {
return $this->items;
}
public function get_order() {
return $this->order;
}
public function get() {
return [
'items' => $this->get_items()->all(),
'order' => $this->get_order()->all(),
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Elementor\Modules\GlobalClasses\ImportExport;
use Elementor\App\Modules\ImportExport\Runners\Export\Export_Runner_Base;
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
use Elementor\Modules\GlobalClasses\Global_Classes_Parser;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Export_Runner extends Export_Runner_Base {
public static function get_name(): string {
return 'global-classes';
}
public function should_export( array $data ) {
// Same as the site-settings runner.
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true )
);
}
public function export( array $data ) {
$kit = Plugin::$instance->kits_manager->get_active_kit();
if ( ! $kit ) {
return [
'manifest' => [],
'files' => [],
];
}
$global_classes = Global_Classes_Repository::make()->all()->get();
$global_classes_result = Global_Classes_Parser::make()->parse( $global_classes );
if ( ! $global_classes_result->is_valid() ) {
return [
'manifest' => [],
'files' => [],
];
}
return [
'files' => [
'path' => Import_Export::FILE_NAME,
'data' => $global_classes_result->unwrap(),
],
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Elementor\Modules\GlobalClasses\ImportExport;
use Elementor\App\Modules\ImportExport\Processes\Export;
use Elementor\App\Modules\ImportExport\Processes\Import;
class Import_Export {
const FILE_NAME = 'global-classes';
public function register_hooks() {
add_action( 'elementor/import-export/export-kit', function ( Export $export ) {
$export->register( new Export_Runner() );
} );
add_action( 'elementor/import-export/import-kit', function ( Import $import ) {
$import->register( new Import_Runner() );
} );
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Elementor\Modules\GlobalClasses\ImportExport;
use Elementor\App\Modules\ImportExport\Runners\Import\Import_Runner_Base;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
use Elementor\Modules\GlobalClasses\Global_Classes_Parser;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Import_Runner extends Import_Runner_Base {
public static function get_name(): string {
return 'global-classes';
}
public function should_import( array $data ) {
// Same as the site-settings runner.
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true ) &&
! empty( $data['site_settings']['settings'] ) &&
! empty( $data['extracted_directory_path'] )
);
}
public function import( array $data, array $imported_data ) {
$kit = Plugin::$instance->kits_manager->get_active_kit();
$file_name = Import_Export::FILE_NAME;
$global_classes = ImportExportUtils::read_json_file( "{$data['extracted_directory_path']}/{$file_name}.json" );
if ( ! $kit || ! $global_classes ) {
return [];
}
$global_classes_result = Global_Classes_Parser::make()->parse( $global_classes );
if ( ! $global_classes_result->is_valid() ) {
return [];
}
$global_classes = $global_classes_result->unwrap();
Global_Classes_Repository::make()->put(
$global_classes['items'],
$global_classes['order']
);
return $global_classes;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\AtomicWidgets\Module as Atomic_Widgets_Module;
use Elementor\Modules\GlobalClasses\Database\Global_Classes_Database_Updater;
use Elementor\Modules\GlobalClasses\ImportExport\Import_Export;
use Elementor\Modules\GlobalClasses\Usage\Global_Classes_Usage;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const NAME = 'e_classes';
const ENFORCE_CAPABILITIES_EXPERIMENT = 'global_classes_should_enforce_capabilities';
// TODO: Add global classes package
const PACKAGES = [
'editor-global-classes',
];
public function get_name() {
return 'global-classes';
}
public function __construct() {
parent::__construct();
$this->register_features();
$is_feature_active = Plugin::$instance->experiments->is_feature_active( self::NAME );
$is_atomic_widgets_active = Plugin::$instance->experiments->is_feature_active( Atomic_Widgets_Module::EXPERIMENT_NAME );
// TODO: When the `e_atomic_elements` feature is not hidden, add it as a dependency
if ( $is_feature_active && $is_atomic_widgets_active ) {
add_filter( 'elementor/editor/v2/packages', fn( $packages ) => $this->add_packages( $packages ) );
( new Global_Classes_Usage() )->register_hooks();
( new Global_Classes_REST_API() )->register_hooks();
( new Global_Classes_CSS() )->register_hooks();
( new Global_Classes_Cleanup() )->register_hooks();
( new Import_Export() )->register_hooks();
( new Global_Classes_Database_Updater() )->register();
}
}
private function register_features() {
Plugin::$instance->experiments->add_feature([
'name' => self::NAME,
'title' => esc_html__( 'Global Classes', 'elementor' ),
'description' => esc_html__( 'Enable global CSS classes.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_INACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_ALPHA,
]);
Plugin::$instance->experiments->add_feature([
'name' => self::ENFORCE_CAPABILITIES_EXPERIMENT,
'title' => esc_html__( 'Enforce global classes capabilities', 'elementor' ),
'description' => esc_html__( 'Enforce global classes capabilities.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
]);
}
private function add_packages( $packages ) {
return array_merge( $packages, self::PACKAGES );
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Elementor\Modules\Global_Classes\Usage;
use Elementor\Core\Base\Document;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Applied_Global_Classes_Usage {
/**
* Get data about how global classes are applied across Elementor elements.
*
* @return array<string, int> Statistics about applied global classes per global class
*/
public function get() {
$total_count_per_class_id = [];
$global_class_ids = Global_Classes_Repository::make()->all()->get_items()->keys()->all();
if ( empty( $global_class_ids ) ) {
return [];
}
Plugin::$instance->db->iterate_elementor_documents( function( $document, $elements_data ) use ( &$total_count_per_class_id, $global_class_ids ) {
$count_per_global_class = $this->get_classes_count_per_class( $elements_data, $global_class_ids );
$total_count_per_class_id = Collection::make( $count_per_global_class )->reduce( function( $carry, $count, $class_id ) {
$carry[ $class_id ] ??= 0;
$carry[ $class_id ] += $count;
return $carry;
}, $total_count_per_class_id );
});
foreach ( $global_class_ids as $global_class_id ) {
$total_count_per_class_id[ $global_class_id ] ??= 0;
}
return $total_count_per_class_id;
}
private function get_classes_count_per_class( $elements_data, $global_class_ids ) {
$count_per_class = [];
Plugin::$instance->db->iterate_data( $elements_data, function( $element_data ) use ( $global_class_ids, &$count_per_class ) {
$element_type = Atomic_Elements_Utils::get_element_type( $element_data );
$element_instance = Atomic_Elements_Utils::get_element_instance( $element_type );
if ( ! Atomic_Elements_Utils::is_atomic_element( $element_instance ) ) {
return;
}
/** @var Atomic_Element_Base | Atomic_Widget_Base $element_instance */
$applied_classes_per_element = $this->get_applied_global_classes_per_element( $element_instance->get_props_schema(), $element_data, $global_class_ids );
foreach ( $applied_classes_per_element as $global_class_id => $count ) {
$count_per_class[ $global_class_id ] ??= 0;
$count_per_class[ $global_class_id ] += $count;
}
});
return $count_per_class;
}
private function get_applied_global_classes_per_element( $atomic_props_schema, $atomic_element_data, $global_class_ids ) {
return Collection::make( $atomic_props_schema )->reduce( function( $carry, $prop_value, $prop_name ) use ( $atomic_element_data, $global_class_ids ) {
if ( ! Atomic_Elements_Utils::is_classes_prop( $prop_value ) ) {
return $carry;
}
$prop_applied_global_class_ids = $this->get_applied_global_classes( $atomic_element_data['settings'][ $prop_name ]['value'] ?? [], $global_class_ids );
foreach ( $prop_applied_global_class_ids as $global_class_id ) {
$carry[ $global_class_id ] ??= 0;
$carry[ $global_class_id ] += 1;
}
return $carry;
}, [] );
}
private function get_applied_global_classes( $prop, $global_class_ids ) {
return array_intersect( $prop, $global_class_ids );
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Elementor\Modules\GlobalClasses\Usage;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\Global_Classes\Usage\Applied_Global_Classes_Usage;
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
class Global_Classes_Usage {
const MIN_CLASSES_COUNT = 1;
public function register_hooks() {
add_filter( 'elementor/tracker/send_tracking_data_params', fn( $params ) => $this->add_tracking_data( $params ) );
}
private function add_tracking_data( $params ) {
$params['usages']['global_classes']['total_count'] = Global_Classes_Repository::make()->all()->get_items()->count();
if ( 0 === $params['usages']['global_classes']['total_count'] ) {
return $params;
}
$applied_global_classes_usage = ( new Applied_Global_Classes_Usage() )->get();
$applied_global_classes_usage = Collection::make( $applied_global_classes_usage )
->filter( fn( $count ) => $count <= self::MIN_CLASSES_COUNT )
->keys()
->count();
if ( ! empty( $applied_global_classes_usage ) ) {
$params['usages']['global_classes']['low_usage_global_classes_count'] = $applied_global_classes_usage;
}
return $params;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\Modules\GlobalClasses\Utils;
use Elementor\Core\Base\Document;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Element_Base;
use Elementor\Modules\AtomicWidgets\Elements\Atomic_Widget_Base;
use Elementor\Plugin;
class Atomic_Elements_Utils {
public static function is_classes_prop( $prop ) {
return 'plain' === $prop::KIND && 'classes' === $prop->get_key();
}
public static function get_element_type( $element ) {
return 'widget' === $element['elType'] ? $element['widgetType'] : $element['elType'];
}
public static function get_element_instance( $element_type ) {
$widget = Plugin::$instance->widgets_manager->get_widget_types( $element_type );
$element = Plugin::$instance->elements_manager->get_element_types( $element_type );
return $widget ?? $element;
}
public static function is_atomic_element( $element_instance ) {
if ( ! $element_instance ) {
return false;
}
return (
$element_instance instanceof Atomic_Element_Base ||
$element_instance instanceof Atomic_Widget_Base
);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\Modules\GlobalClasses\Utils;
class Error_Builder {
private string $message;
private int $status;
private string $code;
private function __construct( $code, $status = 500 ) {
$this->code = $code;
$this->status = $status;
}
public static function make( $code, $status = 500 ) {
return new self( $code, $status );
}
public function set_status( int $status ) {
$this->status = $status;
return $this;
}
public function set_message( string $message ) {
$this->message = $message;
return $this;
}
public function build() {
return new \WP_Error( $this->code, $this->message, [ 'status' => $this->status ] );
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Elementor\Modules\GlobalClasses\Utils;
class Response_Builder {
private $data;
private int $status;
private array $meta = [];
private bool $empty = false;
const NO_CONTENT = 204;
private function __construct( $data, $status ) {
$this->data = $data;
$this->status = $status;
}
public static function make( $data = null, $status = 200 ) {
return new self( $data, $status );
}
public function set_meta( array $meta ) {
$this->meta = $meta;
return $this;
}
public function set_status( int $status ) {
$this->status = $status;
return $this;
}
public function no_content() {
return $this->set_status( static::NO_CONTENT );
}
public function build() {
$res_data = static::NO_CONTENT === $this->status
? null
: [
'data' => $this->data,
'meta' => $this->meta,
];
return new \WP_REST_Response( $res_data, $this->status );
}
}