first commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses;
|
||||
|
||||
use Elementor\Plugin;
|
||||
use Elementor\Modules\AtomicWidgets\Styles\Atomic_Styles_Manager;
|
||||
|
||||
class Atomic_Global_Styles {
|
||||
const STYLES_KEY = 'global';
|
||||
|
||||
public function register_hooks() {
|
||||
add_action(
|
||||
'elementor/atomic-widgets/styles/register',
|
||||
fn( Atomic_Styles_Manager $styles_manager ) => $this->register_styles( $styles_manager ),
|
||||
20,
|
||||
2
|
||||
);
|
||||
|
||||
add_action( 'elementor/global_classes/update', fn( string $context ) => $this->invalidate_cache( $context ), 10, 1 );
|
||||
|
||||
add_action(
|
||||
'deleted_post',
|
||||
fn( $post_id ) => $this->on_post_delete( $post_id )
|
||||
);
|
||||
|
||||
add_action(
|
||||
'elementor/core/files/clear_cache',
|
||||
fn() => $this->invalidate_cache(),
|
||||
);
|
||||
|
||||
add_filter('elementor/atomic-widgets/settings/transformers/classes',
|
||||
fn( $value ) => $this->transform_classes_names( $value )
|
||||
);
|
||||
}
|
||||
|
||||
private function register_styles( Atomic_Styles_Manager $styles_manager ) {
|
||||
$context = is_preview() ? Global_Classes_Repository::CONTEXT_PREVIEW : Global_Classes_Repository::CONTEXT_FRONTEND;
|
||||
|
||||
$get_styles = function () use ( $context ) {
|
||||
return Global_Classes_Repository::make()->context( $context )->all()->get_ordered_items()->map( function( $item ) {
|
||||
$item['id'] = $item['label'];
|
||||
return $item;
|
||||
})->reverse()->all(); // we should reverse the order of the items so that the last in the original array should be rendered first (to be overridden by the previous ones)
|
||||
};
|
||||
|
||||
$styles_manager->register(
|
||||
[ self::STYLES_KEY, $context ],
|
||||
$get_styles,
|
||||
);
|
||||
}
|
||||
|
||||
private function on_post_delete( $post_id ) {
|
||||
if ( ! Plugin::$instance->kits_manager->is_kit( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->invalidate_cache();
|
||||
}
|
||||
|
||||
private function invalidate_cache( ?string $context = null ) {
|
||||
if ( empty( $context ) || Global_Classes_Repository::CONTEXT_FRONTEND === $context ) {
|
||||
do_action( 'elementor/atomic-widgets/styles/clear', [ self::STYLES_KEY ] );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'elementor/atomic-widgets/styles/clear', [ self::STYLES_KEY, $context ] );
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses;
|
||||
|
||||
use Elementor\Core\Base\Document;
|
||||
use Elementor\Core\Utils\Collection;
|
||||
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
|
||||
use Elementor\Modules\AtomicWidgets\Elements\Base\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 );
|
||||
$additional_post_types = apply_filters( 'elementor/global_classes/additional_post_types', [] );
|
||||
|
||||
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 ),
|
||||
100,
|
||||
$additional_post_types
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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_name ) => ! in_array( $class_name, $deleted_classes_ids, true ) )
|
||||
->values();
|
||||
}
|
||||
|
||||
return $element_data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses;
|
||||
|
||||
use Elementor\Core\Utils\Collection;
|
||||
use Elementor\Modules\AtomicWidgets\Module;
|
||||
use Elementor\Modules\AtomicWidgets\OptIn\Opt_In;
|
||||
use Elementor\Core\Utils\Api\Parse_Result;
|
||||
use Elementor\Modules\AtomicWidgets\Parsers\Style_Parser;
|
||||
use Elementor\Modules\AtomicWidgets\Styles\Style_Schema;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
public static function check_for_duplicate_labels( array $existing_labels, array $items, array $new_items_ids ) {
|
||||
|
||||
if ( empty( $new_items_ids ) ) {
|
||||
return false;
|
||||
}
|
||||
$new_added_items = array_filter( $items, fn( $item ) => in_array( $item['id'], $new_items_ids, true ) );
|
||||
|
||||
$duplicates = [];
|
||||
|
||||
foreach ( $new_added_items as $item_id => $item ) {
|
||||
if ( in_array( $item['label'], $existing_labels, true ) ) {
|
||||
$duplicates[] = [
|
||||
'item_id' => $item_id,
|
||||
'label' => $item['label'],
|
||||
];
|
||||
}
|
||||
}
|
||||
return $duplicates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace Elementor\Modules\GlobalClasses;
|
||||
|
||||
use Elementor\Core\Kits\Documents\Kit;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypeMigrations\Migrations_Orchestrator;
|
||||
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;
|
||||
|
||||
private ?Global_Classes $cache = null;
|
||||
|
||||
private ?Kit $kit = null;
|
||||
|
||||
public function __construct( ?Kit $kit = null ) {
|
||||
$this->kit = $kit;
|
||||
}
|
||||
|
||||
public static function make( ?Kit $kit = null ): Global_Classes_Repository {
|
||||
return new self( $kit );
|
||||
}
|
||||
|
||||
public function context( string $context ): self {
|
||||
$this->context = $context;
|
||||
$this->cache = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function all( bool $force = false ): Global_Classes {
|
||||
if ( ! $force && null !== $this->cache ) {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
$meta_key = $this->get_meta_key();
|
||||
$kit = $this->get_kit();
|
||||
$all = $kit->get_json_meta( $meta_key );
|
||||
|
||||
$is_preview = static::META_KEY_PREVIEW === $meta_key;
|
||||
$is_empty = empty( $all );
|
||||
|
||||
if ( $is_preview && $is_empty ) {
|
||||
$all = $kit->get_json_meta( static::META_KEY_FRONTEND );
|
||||
}
|
||||
|
||||
Migrations_Orchestrator::make()->migrate(
|
||||
$all,
|
||||
$kit->get_id(),
|
||||
$meta_key,
|
||||
function( $migrated_data ) use ( $kit, $meta_key ) {
|
||||
$kit->update_json_meta( $meta_key, $migrated_data );
|
||||
}
|
||||
);
|
||||
|
||||
$this->cache = Global_Classes::make( $all['items'] ?? [], $all['order'] ?? [] );
|
||||
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
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' );
|
||||
}
|
||||
|
||||
$this->cache = null;
|
||||
|
||||
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(): Kit {
|
||||
return $this->kit ?? Plugin::$instance->kits_manager->get_active_kit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses;
|
||||
|
||||
use Elementor\Modules\GlobalClasses\Usage\Applied_Global_Classes_Usage;
|
||||
use Elementor\Core\Utils\Api\Error_Builder;
|
||||
use Elementor\Core\Utils\Api\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 API_BASE_USAGE = self::API_BASE . '/usage';
|
||||
const MAX_ITEMS = 100;
|
||||
const LABEL_PREFIX = 'DUP_';
|
||||
const MAX_LABEL_LENGTH = 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() => is_user_logged_in(),
|
||||
'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_USAGE, [
|
||||
[
|
||||
'callback' => fn() => $this->route_wrapper( fn() => $this->get_usage() ),
|
||||
'permission_callback' => fn() => current_user_can( 'manage_options' ),
|
||||
'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 get_usage() {
|
||||
$classes_usage = ( new Applied_Global_Classes_Usage() )->get_detailed_usage();
|
||||
|
||||
return Response_Builder::make( (object) $classes_usage )->build();
|
||||
}
|
||||
|
||||
private function put( \WP_REST_Request $request ) {
|
||||
$context = $request->get_param( 'context' );
|
||||
$changes = $request->get_param( 'changes' ) ?? [];
|
||||
$new_added_items_ids = $changes['added'] ?? [];
|
||||
$parser = Global_Classes_Parser::make();
|
||||
$existing_labels = Global_Classes_Repository::make()
|
||||
->context( $context )
|
||||
->all()
|
||||
->get_items()
|
||||
->map( function ( $item ) {
|
||||
return $item['label'];
|
||||
} )
|
||||
->all();
|
||||
|
||||
$items_result = $parser->parse_items(
|
||||
$request->get_param( 'items' )
|
||||
);
|
||||
|
||||
$items_count = count( $items_result->unwrap() );
|
||||
|
||||
if ( $items_count > self::MAX_ITEMS ) {
|
||||
return Error_Builder::make( 'global_classes_limit_exceeded' )
|
||||
->set_status( 400 )
|
||||
->set_meta([
|
||||
'current_count' => $items_count,
|
||||
'max_allowed' => self::MAX_ITEMS,
|
||||
])
|
||||
->set_message(sprintf(
|
||||
/* translators: %d: Maximum allowed items. */
|
||||
__( 'Global classes limit exceeded. Maximum allowed: %d', 'elementor' ),
|
||||
self::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,
|
||||
$changes,
|
||||
);
|
||||
|
||||
$duplicated_labels = Global_Classes_Parser::check_for_duplicate_labels(
|
||||
$existing_labels,
|
||||
$items_result->unwrap(),
|
||||
$new_added_items_ids
|
||||
);
|
||||
|
||||
$final_items = $items_result->unwrap();
|
||||
$duplicate_validation_result = null;
|
||||
|
||||
if ( ! empty( $duplicated_labels ) ) {
|
||||
$modified_labels = $this->handle_duplicates( $duplicated_labels, $existing_labels );
|
||||
$duplicate_validation_result = $modified_labels;
|
||||
foreach ( $modified_labels as $item_id => $labels ) {
|
||||
$final_items[ $item_id ]['label'] = $labels['modified'];
|
||||
}
|
||||
}
|
||||
|
||||
$repository->put(
|
||||
$changes_resolver->resolve_items( $final_items ),
|
||||
$changes_resolver->resolve_order( $order_result->unwrap() ),
|
||||
);
|
||||
|
||||
if ( $duplicate_validation_result ) {
|
||||
$response_data = [
|
||||
'code' => 'DUPLICATED_LABEL',
|
||||
'modifiedLabels' => $duplicate_validation_result,
|
||||
];
|
||||
return Response_Builder::make( $response_data )->build();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private function handle_duplicates( array $duplicate_labels, array $existing_labels ) {
|
||||
|
||||
$modified_labels = [];
|
||||
|
||||
foreach ( $duplicate_labels as $duplicate_label ) {
|
||||
$item_id = $duplicate_label['item_id'];
|
||||
$original_label = $duplicate_label['label'];
|
||||
|
||||
$modified_label = $this->generate_unique_label( $original_label, $existing_labels );
|
||||
|
||||
$modified_labels[ $item_id ] = [
|
||||
'original' => $original_label,
|
||||
'modified' => $modified_label,
|
||||
];
|
||||
}
|
||||
|
||||
return $modified_labels;
|
||||
}
|
||||
|
||||
|
||||
private function generate_unique_label( $original_label, $existing_labels ) {
|
||||
$prefix = self::LABEL_PREFIX;
|
||||
$max_length = self::MAX_LABEL_LENGTH;
|
||||
|
||||
$has_prefix = strpos( $original_label, $prefix ) === 0;
|
||||
|
||||
if ( $has_prefix ) {
|
||||
$base_label = substr( $original_label, strlen( $prefix ) );
|
||||
|
||||
$counter = 1;
|
||||
$new_label = $prefix . $base_label . $counter;
|
||||
|
||||
while ( in_array( $new_label, $existing_labels, true ) ) {
|
||||
++$counter;
|
||||
$new_label = $prefix . $base_label . $counter;
|
||||
}
|
||||
|
||||
if ( strlen( $new_label ) > $max_length ) {
|
||||
$available_length = $max_length - strlen( $prefix . $counter );
|
||||
$base_label = substr( $base_label, 0, $available_length );
|
||||
$new_label = $prefix . $base_label . $counter;
|
||||
}
|
||||
} else {
|
||||
$new_label = $prefix . $original_label;
|
||||
|
||||
if ( strlen( $new_label ) > $max_length ) {
|
||||
$available_length = $max_length - strlen( $prefix );
|
||||
$new_label = $prefix . substr( $original_label, 0, $available_length );
|
||||
}
|
||||
|
||||
$counter = 1;
|
||||
$base_label = substr( $original_label, 0, $available_length ?? strlen( $original_label ) );
|
||||
|
||||
while ( in_array( $new_label, $existing_labels, true ) ) {
|
||||
$new_label = $prefix . $base_label . $counter;
|
||||
|
||||
if ( strlen( $new_label ) > $max_length ) {
|
||||
$available_length = $max_length - strlen( $prefix . $counter );
|
||||
$base_label = substr( $original_label, 0, $available_length );
|
||||
$new_label = $prefix . $base_label . $counter;
|
||||
}
|
||||
|
||||
++$counter;
|
||||
}
|
||||
}
|
||||
|
||||
return $new_label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses;
|
||||
|
||||
use Elementor\Core\Utils\Collection;
|
||||
|
||||
class Global_Classes {
|
||||
private Collection $items;
|
||||
private Collection $order;
|
||||
private Collection $ordered_items;
|
||||
|
||||
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 );
|
||||
$this->ordered_items = $this->order
|
||||
->map( fn( $id ) => $data[ $id ] ?? null )
|
||||
->filter( fn( $item ) => null !== $item );
|
||||
}
|
||||
|
||||
public function get_items() {
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function get_order() {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function get_ordered_items() {
|
||||
return $this->ordered_items;
|
||||
}
|
||||
|
||||
public function get() {
|
||||
return [
|
||||
'items' => $this->get_items()->all(),
|
||||
'order' => $this->get_order()->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\ImportExportCustomization;
|
||||
|
||||
use Elementor\App\Modules\ImportExportCustomization\Processes\Export;
|
||||
use Elementor\App\Modules\ImportExportCustomization\Processes\Import;
|
||||
use Elementor\Modules\GlobalClasses\ImportExportCustomization\Runners\Export as Export_Runner;
|
||||
use Elementor\Modules\GlobalClasses\ImportExportCustomization\Runners\Import as Import_Runner;
|
||||
|
||||
class Import_Export_Customization {
|
||||
const FILE_NAME = 'global-classes';
|
||||
|
||||
public function register_hooks() {
|
||||
add_action( 'elementor/import-export-customization/export-kit', function ( Export $export ) {
|
||||
$export->register( new Export_Runner() );
|
||||
} );
|
||||
|
||||
add_action( 'elementor/import-export-customization/import-kit', function ( Import $import ) {
|
||||
$import->register( new Import_Runner() );
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\ImportExportCustomization\Runners;
|
||||
|
||||
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Export_Runner_Base;
|
||||
use Elementor\Modules\AtomicWidgets\Module as Atomic_Widgets_Module;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Parser;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
|
||||
use Elementor\Modules\GlobalClasses\ImportExportCustomization\Import_Export_Customization;
|
||||
use Elementor\Modules\GlobalClasses\Module as Global_Classes_Module;
|
||||
use Elementor\Plugin;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Export extends Export_Runner_Base {
|
||||
public static function get_name(): string {
|
||||
return 'global-classes';
|
||||
}
|
||||
|
||||
public function should_export( array $data ): bool {
|
||||
return (
|
||||
isset( $data['include'] ) &&
|
||||
in_array( 'settings', $data['include'], true ) &&
|
||||
$this->is_classes_enabled( $data )
|
||||
);
|
||||
}
|
||||
|
||||
private function is_classes_enabled( array $data ): bool {
|
||||
if ( ! $this->is_feature_active() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $data['customization']['settings']['classes'] ) ) {
|
||||
return (bool) $data['customization']['settings']['classes'];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function is_feature_active(): bool {
|
||||
return Plugin::$instance->experiments->is_feature_active( Global_Classes_Module::NAME )
|
||||
&& Plugin::$instance->experiments->is_feature_active( Atomic_Widgets_Module::EXPERIMENT_NAME );
|
||||
}
|
||||
|
||||
public function export( array $data ): array {
|
||||
$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' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$classes_data = $global_classes_result->unwrap();
|
||||
|
||||
if ( empty( $classes_data['items'] ) ) {
|
||||
return [
|
||||
'manifest' => [],
|
||||
'files' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'files' => [
|
||||
'path' => Import_Export_Customization::FILE_NAME,
|
||||
'data' => $classes_data,
|
||||
],
|
||||
'manifest' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\ImportExportCustomization\Runners;
|
||||
|
||||
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Import_Runner_Base;
|
||||
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
|
||||
use Elementor\Modules\AtomicWidgets\Utils\Utils;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Parser;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
|
||||
use Elementor\Modules\GlobalClasses\ImportExportCustomization\Import_Export_Customization;
|
||||
use Elementor\Plugin;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Import extends Import_Runner_Base {
|
||||
public static function get_name(): string {
|
||||
return 'global-classes';
|
||||
}
|
||||
|
||||
public function should_import( array $data ): bool {
|
||||
return (
|
||||
isset( $data['include'] ) &&
|
||||
in_array( 'settings', $data['include'], true ) &&
|
||||
! empty( $data['extracted_directory_path'] ) &&
|
||||
$this->is_classes_enabled( $data )
|
||||
);
|
||||
}
|
||||
|
||||
private function is_classes_enabled( array $data ): bool {
|
||||
if ( isset( $data['customization']['settings']['classes'] ) ) {
|
||||
return (bool) $data['customization']['settings']['classes'];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function import( array $data, array $imported_data ): array {
|
||||
$kit = Plugin::$instance->kits_manager->get_active_kit();
|
||||
|
||||
$file_name = Import_Export_Customization::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 [];
|
||||
}
|
||||
|
||||
$imported_classes = $global_classes_result->unwrap();
|
||||
$repository = Global_Classes_Repository::make();
|
||||
|
||||
$override_all = ! empty( $data['customization']['settings']['classesOverrideAll'] );
|
||||
|
||||
if ( $override_all ) {
|
||||
$repository->put(
|
||||
$imported_classes['items'],
|
||||
$imported_classes['order']
|
||||
);
|
||||
|
||||
return $imported_classes;
|
||||
}
|
||||
|
||||
$existing_classes = $this->get_existing_classes( $repository, $imported_data );
|
||||
$merged = $this->merge_classes( $existing_classes, $imported_classes );
|
||||
|
||||
$repository->put(
|
||||
$merged['items'],
|
||||
$merged['order']
|
||||
);
|
||||
|
||||
return $imported_classes;
|
||||
}
|
||||
|
||||
private function get_existing_classes( Global_Classes_Repository $repository, array $imported_data ): array {
|
||||
$existing = $repository->all()->get();
|
||||
|
||||
if ( ! empty( $existing['items'] ) ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$was_new_kit_created = ! empty( $imported_data['site-settings']['imported_kit_id'] );
|
||||
|
||||
if ( ! $was_new_kit_created ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$previous_kit_id = Plugin::$instance->kits_manager->get_previous_id();
|
||||
|
||||
if ( ! $previous_kit_id ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$previous_kit = Plugin::$instance->kits_manager->get_kit( $previous_kit_id );
|
||||
|
||||
return Global_Classes_Repository::make( $previous_kit )->all()->get();
|
||||
}
|
||||
|
||||
private function merge_classes( array $existing, array $imported ): array {
|
||||
$existing_items = $existing['items'] ?? [];
|
||||
$existing_order = $existing['order'] ?? [];
|
||||
$existing_labels = $this->get_existing_labels( $existing_items );
|
||||
|
||||
$imported_items = $imported['items'] ?? [];
|
||||
$imported_order = $imported['order'] ?? [];
|
||||
|
||||
foreach ( $imported_order as $imported_id ) {
|
||||
if ( ! isset( $imported_items[ $imported_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$imported_class = $imported_items[ $imported_id ];
|
||||
|
||||
$id_exists = array_key_exists( $imported_id, $existing_items );
|
||||
$new_id = $id_exists
|
||||
? $this->generate_unique_id( array_keys( $existing_items ) )
|
||||
: $imported_id;
|
||||
|
||||
$original_label = $imported_class['label'] ?? $imported_id;
|
||||
$new_label = ImportExportUtils::resolve_label_conflict( $original_label, $existing_labels );
|
||||
$existing_labels[] = strtolower( $new_label );
|
||||
|
||||
$imported_class['id'] = $new_id;
|
||||
$imported_class['label'] = $new_label;
|
||||
|
||||
$existing_items[ $new_id ] = $imported_class;
|
||||
$existing_order[] = $new_id;
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $existing_items,
|
||||
'order' => $existing_order,
|
||||
];
|
||||
}
|
||||
|
||||
private function get_existing_labels( array $items ): array {
|
||||
$labels = [];
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
if ( isset( $item['label'] ) ) {
|
||||
$labels[] = strtolower( $item['label'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
private function generate_unique_id( array $existing_ids ): string {
|
||||
return Utils::generate_id( 'g-', $existing_ids );
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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() );
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
102
wp-content/plugins/elementor/modules/global-classes/module.php
Normal file
102
wp-content/plugins/elementor/modules/global-classes/module.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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\ImportExportCustomization\Import_Export_Customization;
|
||||
use Elementor\Modules\GlobalClasses\Utils\Template_Library_Global_Classes;
|
||||
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 Atomic_Global_Styles() )->register_hooks();
|
||||
( new Global_Classes_Cleanup() )->register_hooks();
|
||||
( new Import_Export() )->register_hooks();
|
||||
( new Import_Export_Customization() )->register_hooks();
|
||||
( new Global_Classes_Database_Updater() )->register();
|
||||
|
||||
add_filter(
|
||||
'elementor/template_library/export/build_snapshots',
|
||||
[ Template_Library_Global_Classes::class, 'add_global_classes_snapshot' ],
|
||||
10,
|
||||
4
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'elementor/template_library/get_data/extract_snapshots',
|
||||
[ Template_Library_Global_Classes::class, 'extract_global_classes_from_data' ],
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'elementor/template_library/import/process_content',
|
||||
[ Template_Library_Global_Classes::class, 'process_global_classes_import' ],
|
||||
20,
|
||||
3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
'new_site' => [
|
||||
'default_active' => true,
|
||||
'minimum_installation_version' => '4.0.0',
|
||||
],
|
||||
]);
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Usage;
|
||||
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
|
||||
use Elementor\Plugin;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects and exposes usage data for all global CSS classes across Elementor documents.
|
||||
*/
|
||||
class Applied_Global_Classes_Usage {
|
||||
|
||||
/**
|
||||
* Document types that should be excluded from usage reporting.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $excluded_types = [ 'e-flexbox', 'template' ];
|
||||
|
||||
/**
|
||||
* Tracks usage for each global class.
|
||||
*
|
||||
* @var array<string, Css_Class_Usage>
|
||||
*/
|
||||
private array $class_usages = [];
|
||||
|
||||
/**
|
||||
* Returns the total usage count per class ID (excluding template-only classes).
|
||||
*
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public function get(): array {
|
||||
$this->build_class_usages();
|
||||
|
||||
$result = [];
|
||||
foreach ( $this->class_usages as $class_id => $usage ) {
|
||||
if ( $usage->get_total_usage() > 0 ) {
|
||||
$result[ $class_id ] = $usage->get_total_usage();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns detailed usage information per class ID.
|
||||
* Each class ID maps to a list of document usages (excluding excluded types).
|
||||
*
|
||||
* @return array<string, array{
|
||||
* pageId: int,
|
||||
* title: string,
|
||||
* type: string,
|
||||
* total: int,
|
||||
* elements: string[]
|
||||
* }>
|
||||
*/
|
||||
public function get_detailed_usage(): array {
|
||||
$this->build_class_usages();
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ( $this->class_usages as $class_id => $usage ) {
|
||||
$pages = $usage->get_pages();
|
||||
|
||||
$filtered_pages = array_filter(
|
||||
$pages,
|
||||
fn( $page_data ) => ! in_array( $page_data['type'], $this->excluded_types, true )
|
||||
);
|
||||
|
||||
if ( empty( $filtered_pages ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $filtered_pages as $page_id => $page_data ) {
|
||||
$result[ $class_id ][] = [
|
||||
'pageId' => $page_id,
|
||||
'title' => $page_data['title'],
|
||||
'type' => $page_data['type'],
|
||||
'total' => $page_data['total'],
|
||||
'elements' => $page_data['elements'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the internal usage map from all Elementor documents.
|
||||
*
|
||||
* This method initializes and aggregates class usage from all relevant documents,
|
||||
* merging duplicate class IDs found in multiple pages.
|
||||
*/
|
||||
private function build_class_usages(): void {
|
||||
$this->class_usages = [];
|
||||
|
||||
$class_ids = Global_Classes_Repository::make()
|
||||
->all()
|
||||
->get_items()
|
||||
->keys()
|
||||
->all();
|
||||
|
||||
Plugin::$instance->db->iterate_elementor_documents(
|
||||
function ( $document ) use ( $class_ids ) {
|
||||
$usage = new Document_Usage( $document );
|
||||
$usage->analyze();
|
||||
|
||||
foreach ( $usage->get_usages() as $class_id => $class_usage ) {
|
||||
if ( ! in_array( $class_id, $class_ids, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $this->class_usages[ $class_id ] ) ) {
|
||||
$this->class_usages[ $class_id ] = $class_usage;
|
||||
} else {
|
||||
$this->class_usages[ $class_id ]->merge( $class_usage );
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Usage;
|
||||
|
||||
/**
|
||||
* Tracks usage of a specific global CSS class across multiple Elementor documents.
|
||||
*/
|
||||
class Css_Class_Usage {
|
||||
|
||||
/** @var string */
|
||||
private string $class_id;
|
||||
|
||||
/** @var int */
|
||||
private int $total = 0;
|
||||
|
||||
/**
|
||||
* @var array<int, array{
|
||||
* title: string,
|
||||
* total: int,
|
||||
* elements: string[],
|
||||
* type?: string
|
||||
* }>
|
||||
*/
|
||||
private array $pages = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $class_id Global CSS class ID.
|
||||
*/
|
||||
public function __construct( string $class_id ) {
|
||||
$this->class_id = $class_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track usage of this class on a specific document and element.
|
||||
*
|
||||
* @param int $page_id Document ID.
|
||||
* @param string $page_title Document title.
|
||||
* @param string $element_id Element ID using this class.
|
||||
* @param string|null $document_type Optional document type (e.g. header, footer, etc).
|
||||
*/
|
||||
public function track_usage( int $page_id, string $page_title, string $element_id, ?string $document_type = null ): void {
|
||||
++$this->total;
|
||||
|
||||
if ( ! isset( $this->pages[ $page_id ] ) ) {
|
||||
$this->pages[ $page_id ] = [
|
||||
'title' => $page_title,
|
||||
'total' => 0,
|
||||
'elements' => [],
|
||||
];
|
||||
|
||||
if ( $document_type ) {
|
||||
$this->pages[ $page_id ]['type'] = $document_type;
|
||||
}
|
||||
}
|
||||
|
||||
++$this->pages[ $page_id ]['total'];
|
||||
$this->pages[ $page_id ]['elements'][] = $element_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge usage data from another instance with the same class ID.
|
||||
*
|
||||
* @param Css_Class_Usage $other The other usage object to merge in.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the class IDs do not match.
|
||||
*/
|
||||
public function merge( Css_Class_Usage $other ): void {
|
||||
if ( $other->get_class_id() !== $this->class_id ) {
|
||||
throw new \InvalidArgumentException( 'Mismatched class ID' );
|
||||
}
|
||||
|
||||
$this->total += $other->get_total_usage();
|
||||
|
||||
foreach ( $other->get_pages() as $page_id => $data ) {
|
||||
if ( ! isset( $this->pages[ $page_id ] ) ) {
|
||||
$this->pages[ $page_id ] = $data;
|
||||
} else {
|
||||
$this->pages[ $page_id ]['total'] += $data['total'];
|
||||
$this->pages[ $page_id ]['elements'] = array_merge(
|
||||
$this->pages[ $page_id ]['elements'],
|
||||
$data['elements']
|
||||
);
|
||||
|
||||
if ( empty( $this->pages[ $page_id ]['type'] ) && ! empty( $data['type'] ) ) {
|
||||
$this->pages[ $page_id ]['type'] = $data['type'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global class ID this instance tracks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_class_id(): string {
|
||||
return $this->class_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of elements using this class.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_usage(): int {
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of document usages.
|
||||
*
|
||||
* @return array<int, array{title: string, total: int, elements: string[], type?: string}>
|
||||
*/
|
||||
public function get_pages(): array {
|
||||
return $this->pages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Usage;
|
||||
|
||||
use Elementor\Plugin;
|
||||
use Elementor\Core\Base\Document as ElementorDocument;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
|
||||
|
||||
/**
|
||||
* Tracks usage of global CSS classes within a specific Elementor document.
|
||||
*/
|
||||
class Document_Usage {
|
||||
|
||||
|
||||
/** @var ElementorDocument */
|
||||
private ElementorDocument $document;
|
||||
|
||||
/** @var array<string, Css_Class_Usage> */
|
||||
private array $usages = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param ElementorDocument $document The Elementor document object.
|
||||
*/
|
||||
public function __construct( ElementorDocument $document ) {
|
||||
$this->document = $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the document to find and record usage of global CSS classes.
|
||||
*/
|
||||
public function analyze(): void {
|
||||
$page_id = $this->document->get_main_id();
|
||||
$page_title = $this->document->get_post()->post_title;
|
||||
$class_ids = $this->get_all_global_class_ids();
|
||||
$elements_data = $this->document->get_elements_raw_data() ?? [];
|
||||
|
||||
$document_type = $this->document->get_type();
|
||||
if ( empty( $document_type ) ) {
|
||||
$document_type = get_post_type( $page_id ) ?? 'unknown';
|
||||
}
|
||||
|
||||
if ( empty( $elements_data ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin::$instance->db->iterate_data(
|
||||
$elements_data,
|
||||
function ( $element_data ) use ( $class_ids, $page_id, $page_title, $document_type ) {
|
||||
$class_values = $element_data['settings']['classes']['value'] ?? [];
|
||||
|
||||
if ( empty( $class_values ) || ! is_array( $class_values ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $class_values as $class_id ) {
|
||||
if ( ! in_array( $class_id, $class_ids, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $this->usages[ $class_id ] ) ) {
|
||||
$this->usages[ $class_id ] = new Css_Class_Usage( $class_id );
|
||||
}
|
||||
|
||||
$this->usages[ $class_id ]->track_usage(
|
||||
$page_id,
|
||||
$page_title,
|
||||
$element_data['id'] ?? 'unknown',
|
||||
$document_type
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recorded usages of global CSS classes in this document.
|
||||
*
|
||||
* @return array<string, Css_Class_Usage>
|
||||
*/
|
||||
public function get_usages(): array {
|
||||
return $this->usages;
|
||||
}
|
||||
|
||||
/**֜
|
||||
* Retrieve all registered global CSS class IDs.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function get_all_global_class_ids(): array {
|
||||
return Global_Classes_Repository::make()
|
||||
->all()
|
||||
->get_items()
|
||||
->filter( fn( $item ) => ! empty( $item['id'] ?? null ) )
|
||||
->keys()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Usage;
|
||||
|
||||
use Elementor\Core\Utils\Collection;
|
||||
use Elementor\Modules\GlobalClasses\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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Utils;
|
||||
|
||||
use Elementor\Core\Base\Document;
|
||||
use Elementor\Core\Utils\Collection;
|
||||
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Element_Base;
|
||||
use Elementor\Modules\AtomicWidgets\Elements\Base\Atomic_Widget_Base;
|
||||
use Elementor\Plugin;
|
||||
|
||||
class Atomic_Elements_Utils {
|
||||
|
||||
public static function is_classes_prop( $prop ) {
|
||||
// phpcs:ignore
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Utils;
|
||||
|
||||
use Elementor\Core\Utils\Template_Library_Element_Iterator;
|
||||
use Elementor\Core\Utils\Template_Library_Import_Export_Utils;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Template_Library_Global_Classes_Element_Transformer {
|
||||
|
||||
public static function rewrite_elements_classes_ids( array $elements, array $id_map ): array {
|
||||
if ( empty( $elements ) || empty( $id_map ) ) {
|
||||
return $elements;
|
||||
}
|
||||
|
||||
return Template_Library_Element_Iterator::iterate(
|
||||
$elements,
|
||||
function ( $element_data ) use ( $id_map ) {
|
||||
$class_values = $element_data['settings']['classes']['value'] ?? null;
|
||||
|
||||
if ( ! is_array( $class_values ) ) {
|
||||
return $element_data;
|
||||
}
|
||||
|
||||
$element_data['settings']['classes']['value'] = self::map_class_values( $class_values, $id_map );
|
||||
|
||||
return $element_data;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function flatten_elements_classes( array $elements, array $global_classes, ?array $only_ids = null ): array {
|
||||
$items = $global_classes['items'] ?? [];
|
||||
|
||||
if ( empty( $elements ) || empty( $items ) ) {
|
||||
return $elements;
|
||||
}
|
||||
|
||||
$ids_to_flatten = null !== $only_ids ? array_fill_keys( $only_ids, true ) : null;
|
||||
|
||||
return Template_Library_Element_Iterator::iterate(
|
||||
$elements,
|
||||
function ( $element_data ) use ( $items, $ids_to_flatten ) {
|
||||
$class_values = $element_data['settings']['classes']['value'] ?? null;
|
||||
|
||||
if ( ! is_array( $class_values ) || empty( $class_values ) ) {
|
||||
return $element_data;
|
||||
}
|
||||
|
||||
[ $updated_values, $element_styles ] = self::flatten_class_values( $class_values, $element_data, $items, $ids_to_flatten );
|
||||
$element_data['settings']['classes']['value'] = array_values( array_unique( $updated_values ) );
|
||||
$element_data['styles'] = $element_styles;
|
||||
|
||||
return $element_data;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static function map_class_values( array $class_values, array $id_map ): array {
|
||||
$updated_values = [];
|
||||
|
||||
foreach ( $class_values as $class_id ) {
|
||||
if ( ! is_string( $class_id ) || '' === $class_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updated_values[] = $id_map[ $class_id ] ?? $class_id;
|
||||
}
|
||||
|
||||
return array_values( array_unique( $updated_values ) );
|
||||
}
|
||||
|
||||
private static function flatten_class_values( array $class_values, array $element_data, array $items, ?array $ids_to_flatten ): array {
|
||||
$updated_values = [];
|
||||
$element_styles = $element_data['styles'] ?? [];
|
||||
|
||||
$local_style_id = self::find_existing_local_style_id( $element_styles );
|
||||
$local_style_used = false;
|
||||
|
||||
foreach ( $class_values as $class_id ) {
|
||||
if ( ! is_string( $class_id ) || '' === $class_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( self::should_flatten_class_id( $class_id, $items, $ids_to_flatten ) ) {
|
||||
$incoming_variants = $items[ $class_id ]['variants'] ?? [];
|
||||
|
||||
if ( null === $local_style_id ) {
|
||||
$local_style_id = self::create_local_class_id( $element_data );
|
||||
$element_styles[ $local_style_id ] = self::build_local_class_style( $local_style_id, [] );
|
||||
}
|
||||
|
||||
$element_styles[ $local_style_id ]['variants'] = array_merge(
|
||||
$element_styles[ $local_style_id ]['variants'],
|
||||
$incoming_variants
|
||||
);
|
||||
|
||||
if ( ! $local_style_used ) {
|
||||
$updated_values[] = $local_style_id;
|
||||
$local_style_used = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$is_global = self::is_global_class_id( $class_id );
|
||||
|
||||
if ( ! $is_global || ( null !== $ids_to_flatten && isset( $items[ $class_id ] ) ) ) {
|
||||
$updated_values[] = $class_id;
|
||||
}
|
||||
}
|
||||
|
||||
return [ $updated_values, $element_styles ];
|
||||
}
|
||||
|
||||
private static function find_existing_local_style_id( array $element_styles ): ?string {
|
||||
foreach ( $element_styles as $style_id => $style ) {
|
||||
if ( isset( $style['label'] ) && Template_Library_Import_Export_Utils::LOCAL_CLASS_LABEL === $style['label'] ) {
|
||||
return $style_id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function is_global_class_id( string $class_id ): bool {
|
||||
return str_starts_with( $class_id, Template_Library_Import_Export_Utils::GLOBAL_CLASS_ID_PREFIX );
|
||||
}
|
||||
|
||||
private static function should_flatten_class_id( string $class_id, array $items, ?array $ids_to_flatten ): bool {
|
||||
if ( ! isset( $items[ $class_id ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( null !== $ids_to_flatten && ! isset( $ids_to_flatten[ $class_id ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function create_local_class_id( array $element_data ): string {
|
||||
return Template_Library_Import_Export_Utils::LOCAL_CLASS_ID_PREFIX
|
||||
. substr( $element_data['id'] ?? '', 0, 8 )
|
||||
. '-'
|
||||
. Template_Library_Import_Export_Utils::generate_random_string();
|
||||
}
|
||||
|
||||
private static function build_local_class_style( string $local_id, array $variants ): array {
|
||||
return [
|
||||
'id' => $local_id,
|
||||
'label' => Template_Library_Import_Export_Utils::LOCAL_CLASS_LABEL,
|
||||
'type' => 'class',
|
||||
'variants' => $variants,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Utils;
|
||||
|
||||
use Elementor\Core\Utils\Template_Library_Element_Iterator;
|
||||
use Elementor\Core\Utils\Template_Library_Import_Export_Utils;
|
||||
use Elementor\Core\Utils\Template_Library_Snapshot_Processor;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Parser;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
|
||||
use Elementor\Modules\GlobalClasses\Global_Classes_Rest_Api;
|
||||
use Elementor\Plugin;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Template_Library_Global_Classes_Snapshot_Builder extends Template_Library_Snapshot_Processor {
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function make(): self {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function extract_used_class_ids_from_elements( array $elements ): array {
|
||||
$ids = [];
|
||||
|
||||
if ( empty( $elements ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Template_Library_Element_Iterator::iterate(
|
||||
$elements,
|
||||
function ( $element_data ) use ( &$ids ) {
|
||||
$class_values = $element_data['settings']['classes']['value'] ?? [];
|
||||
|
||||
if ( is_array( $class_values ) ) {
|
||||
foreach ( $class_values as $class_id ) {
|
||||
if ( is_string( $class_id ) && '' !== $class_id ) {
|
||||
$ids[] = $class_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $element_data;
|
||||
}
|
||||
);
|
||||
|
||||
return array_values( array_unique( $ids ) );
|
||||
}
|
||||
|
||||
public static function build_snapshot_for_ids( array $ids ): ?array {
|
||||
if ( empty( $ids ) || ! self::make()->can_access_repository() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ids = Template_Library_Import_Export_Utils::normalize_string_ids( $ids );
|
||||
|
||||
if ( empty( $ids ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$current = self::make()->load_current_data();
|
||||
$filtered_items = Template_Library_Import_Export_Utils::filter_items_by_ids( $current['items'], $ids );
|
||||
|
||||
if ( empty( $filtered_items ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filtered_order = Template_Library_Import_Export_Utils::build_filtered_order( $current['order'], $filtered_items );
|
||||
|
||||
return self::parse_snapshot_or_null( [
|
||||
'items' => $filtered_items,
|
||||
'order' => $filtered_order,
|
||||
] );
|
||||
}
|
||||
|
||||
public static function build_snapshot_for_elements( array $elements ): ?array {
|
||||
$ids = self::extract_used_class_ids_from_elements( $elements );
|
||||
|
||||
if ( empty( $ids ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::build_snapshot_for_ids( $ids );
|
||||
}
|
||||
|
||||
public static function merge_snapshot_and_get_id_map( array $snapshot ): array {
|
||||
return self::make()->merge_and_get_id_map( $snapshot );
|
||||
}
|
||||
|
||||
public static function create_snapshot_as_new( array $snapshot ): array {
|
||||
return self::make()->create_all_as_new( $snapshot );
|
||||
}
|
||||
|
||||
protected function is_matching_item( array $existing_item, array $incoming_item ): bool {
|
||||
// For global classes, if the labels match, we consider them the same item
|
||||
// when merging, so we reuse the existing class and ignore incoming variants or extra fields.
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function normalize_for_comparison( array $item ): array {
|
||||
$id = $item['id'] ?? '';
|
||||
|
||||
if ( '' === $id ) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$parsed = self::parse_snapshot_or_null( [
|
||||
'items' => [ $id => $item ],
|
||||
'order' => [ $id ],
|
||||
] );
|
||||
|
||||
if ( null !== $parsed && isset( $parsed['items'][ $id ] ) ) {
|
||||
return $parsed['items'][ $id ];
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
protected function get_item_prefix(): string {
|
||||
return Template_Library_Import_Export_Utils::GLOBAL_CLASS_ID_PREFIX;
|
||||
}
|
||||
|
||||
protected function get_max_items(): int {
|
||||
return Global_Classes_Rest_Api::MAX_ITEMS;
|
||||
}
|
||||
|
||||
protected function can_access_repository(): bool {
|
||||
return class_exists( Global_Classes_Repository::class ) && $this->has_active_kit();
|
||||
}
|
||||
|
||||
protected function load_current_data(): array {
|
||||
$current = Global_Classes_Repository::make()->context( Global_Classes_Repository::CONTEXT_PREVIEW )->all()->get();
|
||||
|
||||
return [
|
||||
'items' => $current['items'] ?? [],
|
||||
'order' => $current['order'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
protected function parse_incoming_snapshot( array $snapshot ): ?array {
|
||||
return self::parse_snapshot_or_null( $snapshot );
|
||||
}
|
||||
|
||||
protected function get_incoming_items( array $parsed_snapshot ): array {
|
||||
$items = [];
|
||||
$order = $parsed_snapshot['order'] ?? array_keys( $parsed_snapshot['items'] ?? [] );
|
||||
|
||||
foreach ( $order as $id ) {
|
||||
if ( isset( $parsed_snapshot['items'][ $id ] ) ) {
|
||||
$items[ $id ] = $parsed_snapshot['items'][ $id ];
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
protected function count_current_items( array $items ): int {
|
||||
return count( $items );
|
||||
}
|
||||
|
||||
protected function save_data( array $items, array $metadata ): array {
|
||||
$order = $metadata['order'] ?? [];
|
||||
|
||||
Global_Classes_Repository::make()->put( $items, $order );
|
||||
|
||||
return [
|
||||
'global_classes' => [
|
||||
'items' => $items,
|
||||
'order' => $order,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepare_item_for_save( array $item, string $target_id ): array {
|
||||
$item['id'] = $target_id;
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function has_active_kit(): bool {
|
||||
return (bool) Plugin::instance()->kits_manager->get_active_kit();
|
||||
}
|
||||
|
||||
private static function parse_snapshot_or_null( array $snapshot ): ?array {
|
||||
$parse_result = Global_Classes_Parser::make()->parse( $snapshot );
|
||||
if ( ! $parse_result->is_valid() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $parse_result->unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\GlobalClasses\Utils;
|
||||
|
||||
use Elementor\Core\Utils\Template_Library_Import_Export_Utils;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Template_Library_Global_Classes {
|
||||
|
||||
public static function add_global_classes_snapshot( array $snapshots, $content, $template_id, array $export_data ): array {
|
||||
if ( ! is_array( $content ) ) {
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
if ( ! empty( $snapshots['global_classes'] ) ) {
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
$snapshot = Template_Library_Global_Classes_Snapshot_Builder::build_snapshot_for_elements( $content );
|
||||
|
||||
if ( ! empty( $snapshot ) ) {
|
||||
$snapshots['global_classes'] = $snapshot;
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
public static function extract_global_classes_from_data( array $snapshots, array $decoded_data, array $data ): array {
|
||||
$snapshot = $decoded_data['global_classes'] ?? null;
|
||||
|
||||
if ( ! empty( $snapshot ) && is_array( $snapshot ) ) {
|
||||
$snapshots['global_classes'] = $snapshot;
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
public static function process_global_classes_import( array $result, string $import_mode, array $data ): array {
|
||||
$snapshot = $data['global_classes'] ?? null;
|
||||
|
||||
if ( empty( $snapshot ) || ! is_array( $snapshot ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$snapshot = apply_filters(
|
||||
'elementor/global_classes/import/transform_snapshot',
|
||||
$snapshot,
|
||||
$import_mode,
|
||||
$result,
|
||||
$data
|
||||
);
|
||||
|
||||
$processed = Template_Library_Import_Export_Utils::process_import_by_mode(
|
||||
$import_mode,
|
||||
$result['content'],
|
||||
$snapshot,
|
||||
[ Template_Library_Global_Classes_Snapshot_Builder::class, 'merge_snapshot_and_get_id_map' ],
|
||||
[ Template_Library_Global_Classes_Snapshot_Builder::class, 'create_snapshot_as_new' ],
|
||||
[ Template_Library_Global_Classes_Element_Transformer::class, 'rewrite_elements_classes_ids' ],
|
||||
[ Template_Library_Global_Classes_Element_Transformer::class, 'flatten_elements_classes' ]
|
||||
);
|
||||
|
||||
$result['content'] = $processed['content'];
|
||||
$result['updated_global_classes'] = $processed['operation_result']['global_classes'] ?? null;
|
||||
$result['classes_to_flatten'] = $processed['ids_to_flatten'];
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user