first commit

This commit is contained in:
2026-03-26 13:48:22 +01:00
commit 6af83e92ed
7795 changed files with 2766332 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
<?php
namespace Elementor\Modules\Interactions;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Experiments\Manager as Experiments_Manager;
use Elementor\Modules\AtomicWidgets\Module as AtomicWidgetsModule;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
const MODULE_NAME = 'e-interactions';
const EXPERIMENT_NAME = 'e_interactions';
public function get_name() {
return self::MODULE_NAME;
}
private $preset_animations;
private function get_presets() {
if ( ! $this->preset_animations ) {
$this->preset_animations = new Presets();
}
return $this->preset_animations;
}
public static function get_experimental_data() {
return [
'name' => self::EXPERIMENT_NAME,
'title' => esc_html__( 'Interactions', 'elementor' ),
'description' => esc_html__( 'Enable element interactions.', 'elementor' ),
'hidden' => true,
'default' => Experiments_Manager::STATE_ACTIVE,
'release_status' => Experiments_Manager::RELEASE_STATUS_DEV,
];
}
public function is_experiment_active() {
return Plugin::$instance->experiments->is_feature_active( self::EXPERIMENT_NAME )
&& Plugin::$instance->experiments->is_feature_active( AtomicWidgetsModule::EXPERIMENT_NAME );
}
public function __construct() {
parent::__construct();
if ( ! $this->is_experiment_active() ) {
return;
}
add_action( 'elementor/frontend/after_register_scripts', fn () => $this->register_frontend_scripts() );
add_action( 'elementor/editor/before_enqueue_scripts', fn () => $this->enqueue_editor_scripts() );
add_action( 'elementor/frontend/before_enqueue_scripts', fn () => $this->enqueue_interactions() );
add_action( 'elementor/preview/enqueue_scripts', fn () => $this->enqueue_preview_scripts() );
add_action( 'elementor/editor/after_enqueue_scripts', fn () => $this->enqueue_editor_scripts() );
add_filter( 'elementor/document/save/data',
/**
* @throws \Exception
*/
function( $data, $document ) {
$validation = new Validation();
$document_after_sanitization = $validation->sanitize( $data );
$validation->validate();
return $document_after_sanitization;
},
10, 2 );
add_filter( 'elementor/document/save/data', function( $data, $document ) {
return ( new Parser( $document->get_main_id() ) )->assign_interaction_ids( $data );
}, 11, 2 );
}
private function get_config() {
return [
'constants' => $this->get_presets()->defaults(),
'animationOptions' => $this->get_presets()->list(),
];
}
private function register_frontend_scripts() {
$suffix = ( Utils::is_script_debug() || Utils::is_elementor_tests() ) ? '' : '.min';
wp_register_script(
'motion-js',
ELEMENTOR_ASSETS_URL . 'lib/motion/motion' . $suffix . '.js',
[],
'11.13.5',
true
);
wp_register_script(
'elementor-interactions',
$this->get_js_assets_url( 'interactions' ),
[ 'motion-js' ],
'1.0.0',
true
);
wp_register_script(
'elementor-editor-interactions',
$this->get_js_assets_url( 'editor-interactions' ),
[ 'motion-js' ],
'1.0.0',
true
);
}
public function enqueue_interactions(): void {
wp_enqueue_script( 'motion-js' );
wp_enqueue_script( 'elementor-interactions' );
wp_localize_script(
'elementor-interactions',
'ElementorInteractionsConfig',
$this->get_config()
);
}
public function enqueue_editor_scripts() {
wp_add_inline_script(
'elementor-common',
'window.ElementorInteractionsConfig = ' . wp_json_encode( $this->get_config() ) . ';',
'before'
);
}
public function enqueue_preview_scripts() {
// Ensure motion-js and editor-interactions handler are available in preview iframe
wp_enqueue_script( 'motion-js' );
wp_enqueue_script( 'elementor-editor-interactions' );
wp_localize_script(
'elementor-editor-interactions',
'ElementorInteractionsConfig',
$this->get_config()
);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Elementor\Modules\Interactions;
use Elementor\Modules\AtomicWidgets\Utils\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Parser {
protected $post_id;
protected $ids_lookup = [];
public function __construct( $post_id ) {
$this->post_id = $post_id;
}
public function assign_interaction_ids( $data ) {
if ( isset( $data['elements'] ) && is_array( $data['elements'] ) ) {
$data['elements'] = $this->process_interactions_for( $data['elements'] );
}
return $data;
}
private function process_interactions_for( $elements ) {
if ( ! is_array( $elements ) ) {
return $elements;
}
foreach ( $elements as &$element ) {
if ( isset( $element['interactions'] ) ) {
$element['interactions'] = $this->maybe_assign_interaction_ids( $element['interactions'], $element['id'] );
}
if ( isset( $element['elements'] ) && is_array( $element['elements'] ) ) {
$element['elements'] = $this->process_interactions_for( $element['elements'] );
}
}
return $elements;
}
private function maybe_assign_interaction_ids( $interactions_json, $element_id ) {
$interactions = $this->decode_interactions( $interactions_json );
if ( ! isset( $interactions['items'] ) ) {
return [];
}
foreach ( $interactions['items'] as &$interaction ) {
if ( ! isset( $interaction['$$type'] ) || 'interaction-item' !== $interaction['$$type'] ) {
continue;
}
$existing_id = null;
if ( isset( $interaction['value']['interaction_id']['value'] ) ) {
$existing_id = $interaction['value']['interaction_id']['value'];
}
if ( $existing_id && $this->is_temp_id( $existing_id ) ) {
$interaction['value']['interaction_id'] = [
'$$type' => 'string',
'value' => $this->get_next_interaction_id( $element_id ),
];
} elseif ( $existing_id ) {
$this->ids_lookup[] = $existing_id;
} else {
$interaction['value']['interaction_id'] = [
'$$type' => 'string',
'value' => $this->get_next_interaction_id( $element_id ),
];
}
}
return wp_json_encode( $interactions );
}
private function is_temp_id( $id ) {
return is_string( $id ) && strpos( $id, 'temp-' ) === 0;
}
private function decode_interactions( $interactions ) {
if ( is_array( $interactions ) ) {
return $interactions;
}
if ( is_string( $interactions ) ) {
$decoded = json_decode( $interactions, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
return $decoded;
}
}
return [
'items' => [],
'version' => 1,
];
}
protected function get_next_interaction_id( $prefix ) {
$next_id = Utils::generate_id( "{$this->post_id}-{$prefix}-", $this->ids_lookup );
$this->ids_lookup[] = $next_id;
return $next_id;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Elementor\Modules\Interactions;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Presets {
const DEFAULT_DURATION = 300;
const DEFAULT_DELAY = 0;
const DEFAULT_SLIDE_DISTANCE = 100;
const DEFAULT_SCALE_START = 0;
const DEFAULT_EASING = 'linear';
const TRIGGERS = [ 'load', 'scrollIn', 'scrollOn' ]; // 'scrollOut' is not supported yet.
const EFFECTS = [ 'fade', 'slide', 'scale' ];
const TYPES = [ 'in', 'out' ];
const DIRECTIONS = [ 'left', 'right', 'top', 'bottom' ];
const DURATIONS = [ 0, 100, 200, 300, 400, 500, 750, 1000, 1250, 1500 ];
const DELAYS = [ 0, 100, 200, 300, 400, 500, 750, 1000, 1250, 1500 ];
public function list() {
return $this->generate_animation_options();
}
public function defaults() {
return [
'defaultDuration' => self::DEFAULT_DURATION,
'defaultDelay' => self::DEFAULT_DELAY,
'slideDistance' => self::DEFAULT_SLIDE_DISTANCE,
'scaleStart' => self::DEFAULT_SCALE_START,
'easing' => self::DEFAULT_EASING,
];
}
private function get_label( $key, $value ) {
$special_labels = [
'trigger' => [
'load' => __( 'On page load', 'elementor' ),
'scrollIn' => __( 'Scroll into view', 'elementor' ),
'scrollOut' => __( 'Scroll out of view', 'elementor' ),
],
];
if ( isset( $special_labels[ $key ][ $value ] ) ) {
return $special_labels[ $key ][ $value ];
}
$label = ucwords( str_replace( '-', ' ', $value ) );
return esc_html( $label );
}
private function generate_animation_options() {
$options = [];
foreach ( self::TRIGGERS as $trigger ) {
foreach ( self::EFFECTS as $effect ) {
foreach ( self::TYPES as $type ) {
foreach ( self::DIRECTIONS as $direction ) {
foreach ( self::DURATIONS as $duration ) {
foreach ( self::DELAYS as $delay ) {
$value = "{$trigger}-{$effect}-{$type}-{$direction}-{$duration}-{$delay}";
$label = sprintf(
'%s: %s %s',
$this->get_label( 'trigger', $trigger ),
$this->get_label( 'effect', $effect ),
$this->get_label( 'type', $type ),
);
$options[] = [
'value' => $value,
'label' => $label,
];
}
}
}
foreach ( self::DURATIONS as $duration ) {
foreach ( self::DELAYS as $delay ) {
$value = "{$trigger}-{$effect}-{$type}--{$duration}-{$delay}";
$label = sprintf(
'%s: %s %s',
$this->get_label( 'trigger', $trigger ),
$this->get_label( 'effect', $effect ),
$this->get_label( 'type', $type ),
);
$options[] = [
'value' => $value,
'label' => $label,
];
}
}
}
}
}
return $options;
}
}

View File

@@ -0,0 +1,329 @@
<?php
namespace Elementor\Modules\Interactions;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Validation {
private $elements_to_interactions_counter = [];
private $max_number_of_interactions = 5;
private const VALID_TRIGGERS = [ 'load', 'scrollIn', 'scrollOut', 'scrollOn' ];
private const VALID_EFFECTS = [ 'fade', 'slide', 'scale' ];
private const VALID_TYPES = [ 'in', 'out' ];
private const VALID_DIRECTIONS = [ '', 'left', 'right', 'top', 'bottom' ];
public function sanitize( $document ) {
return $this->sanitize_document_data( $document );
}
public function validate() {
foreach ( $this->elements_to_interactions_counter as $element_id => $number_of_interactions ) {
if ( $number_of_interactions > $this->max_number_of_interactions ) {
throw new \Exception(
sprintf(
// translators: %1$s: element ID, %2$d: maximum number of interactions allowed.
esc_html__( 'Element %1$s has more than %2$d interactions', 'elementor' ),
esc_html( $element_id ),
esc_html( $this->max_number_of_interactions )
)
);
}
}
return true;
}
private function sanitize_document_data( $data ) {
if ( isset( $data['elements'] ) && is_array( $data['elements'] ) ) {
$data['elements'] = $this->sanitize_elements_interactions( $data['elements'] );
}
return $data;
}
private function sanitize_elements_interactions( $elements ) {
if ( ! is_array( $elements ) ) {
return $elements;
}
foreach ( $elements as &$element ) {
if ( isset( $element['interactions'] ) ) {
$element['interactions'] = $this->sanitize_interactions( $element['interactions'], $element['id'] );
}
if ( isset( $element['elements'] ) && is_array( $element['elements'] ) ) {
$element['elements'] = $this->sanitize_elements_interactions( $element['elements'] );
}
}
return $elements;
}
private function decode_interactions( $interactions ) {
if ( is_array( $interactions ) ) {
return isset( $interactions['items'] ) ? $interactions['items'] : [];
}
if ( is_string( $interactions ) ) {
$decoded = json_decode( $interactions, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
return isset( $decoded['items'] ) ? $decoded['items'] : [];
}
}
return [];
}
private function increment_interactions_counter_for( $element_id ) {
if ( ! array_key_exists( $element_id, $this->elements_to_interactions_counter ) ) {
$this->elements_to_interactions_counter[ $element_id ] = 0;
}
++$this->elements_to_interactions_counter[ $element_id ];
return $this;
}
private function sanitize_interactions( $interactions, $element_id ) {
$sanitized = [
'items' => [],
'version' => 1,
];
$list_of_interactions = $this->decode_interactions( $interactions );
foreach ( $list_of_interactions as $interaction ) {
if ( $this->is_valid_interaction_item( $interaction ) ) {
$sanitized['items'][] = $interaction;
$this->increment_interactions_counter_for( $element_id );
}
}
if ( empty( $sanitized['items'] ) ) {
return [];
}
return wp_json_encode( $sanitized );
}
private function is_valid_interaction_item( $item ) {
if ( ! is_array( $item ) ) {
return false;
}
// Validate PropType format: { $$type: 'interaction-item', value: { ... } }
if ( ! isset( $item['$$type'] ) || 'interaction-item' !== $item['$$type'] ) {
return false;
}
if ( ! isset( $item['value'] ) || ! is_array( $item['value'] ) ) {
return false;
}
$value = $item['value'];
// Validate required fields exist
if ( isset( $value['interaction_id'] ) && ! $this->is_valid_string_prop( $value, 'interaction_id' ) ) {
return false;
}
if ( ! $this->is_valid_string_prop( $value, 'trigger', self::VALID_TRIGGERS ) ) {
return false;
}
if ( ! $this->is_valid_animation_prop( $value ) ) {
return false;
}
return true;
}
private function is_valid_string_prop( $data, $key, $allowed_values = null ) {
if ( ! isset( $data[ $key ] ) || ! is_array( $data[ $key ] ) ) {
return false;
}
$prop = $data[ $key ];
if ( ! isset( $prop['$$type'] ) || 'string' !== $prop['$$type'] ) {
return false;
}
if ( ! isset( $prop['value'] ) || ! is_string( $prop['value'] ) ) {
return false;
}
if ( null !== $allowed_values && ! in_array( $prop['value'], $allowed_values, true ) ) {
return false;
}
return true;
}
private function is_valid_boolean_prop( $data, $key ) {
if ( ! isset( $data[ $key ] ) || ! is_array( $data[ $key ] ) ) {
return false;
}
$prop = $data[ $key ];
if ( ! isset( $prop['$$type'] ) || 'boolean' !== $prop['$$type'] ) {
return false;
}
if ( ! isset( $prop['value'] ) || ! is_bool( $prop['value'] ) ) {
return false;
}
return true;
}
private function is_valid_number_prop( $data, $key ) {
if ( ! isset( $data[ $key ] ) || ! is_array( $data[ $key ] ) ) {
return false;
}
$prop = $data[ $key ];
if ( ! isset( $prop['$$type'] ) || 'number' !== $prop['$$type'] ) {
return false;
}
if ( ! isset( $prop['value'] ) || ! is_numeric( $prop['value'] ) ) {
return false;
}
return true;
}
private function is_valid_number_prop_in_range( $data, $key, $min = null, $max = null ) {
if ( ! $this->is_valid_number_prop( $data, $key ) ) {
return false;
}
$value = (float) $data[ $key ]['value'];
if ( null !== $min && $value < $min ) {
return false;
}
if ( null !== $max && $value > $max ) {
return false;
}
return true;
}
private function is_valid_config_prop( $data ) {
if ( ! isset( $data['config'] ) || ! is_array( $data['config'] ) ) {
return false;
}
$config = $data['config'];
if ( ! isset( $config['$$type'] ) || 'config' !== $config['$$type'] ) {
return false;
}
if ( ! isset( $config['value'] ) || ! is_array( $config['value'] ) ) {
return false;
}
$config_value = $config['value'];
if ( isset( $config_value['replay'] ) && ! $this->is_valid_boolean_prop( $config_value, 'replay' ) ) {
return false;
}
if ( isset( $config_value['relativeTo'] ) && ! $this->is_valid_string_prop( $config_value, 'relativeTo' ) ) {
return false;
}
if ( isset( $config_value['offsetTop'] ) && ! $this->is_valid_number_prop_in_range( $config_value, 'offsetTop', 0, 100 ) ) {
return false;
}
if ( isset( $config_value['offsetBottom'] ) && ! $this->is_valid_number_prop_in_range( $config_value, 'offsetBottom', 0, 100 ) ) {
return false;
}
return true;
}
private function is_valid_animation_prop( $data ) {
if ( ! isset( $data['animation'] ) || ! is_array( $data['animation'] ) ) {
return false;
}
$animation = $data['animation'];
if ( ! isset( $animation['$$type'] ) || 'animation-preset-props' !== $animation['$$type'] ) {
return false;
}
if ( ! isset( $animation['value'] ) || ! is_array( $animation['value'] ) ) {
return false;
}
$animation_value = $animation['value'];
// Validate effect
if ( ! $this->is_valid_string_prop( $animation_value, 'effect', self::VALID_EFFECTS ) ) {
return false;
}
// Validate type
if ( ! $this->is_valid_string_prop( $animation_value, 'type', self::VALID_TYPES ) ) {
return false;
}
// Validate direction (can be empty string)
if ( ! $this->is_valid_string_prop( $animation_value, 'direction', self::VALID_DIRECTIONS ) ) {
return false;
}
// Validate timing_config
if ( ! $this->is_valid_timing_config( $animation_value ) ) {
return false;
}
if ( isset( $animation_value['config'] ) && ! $this->is_valid_config_prop( $animation_value ) ) {
return false;
}
return true;
}
private function is_valid_timing_config( $data ) {
if ( ! isset( $data['timing_config'] ) || ! is_array( $data['timing_config'] ) ) {
return false;
}
$timing = $data['timing_config'];
if ( ! isset( $timing['$$type'] ) || 'timing-config' !== $timing['$$type'] ) {
return false;
}
if ( ! isset( $timing['value'] ) || ! is_array( $timing['value'] ) ) {
return false;
}
$timing_value = $timing['value'];
// Validate duration
if ( ! $this->is_valid_number_prop( $timing_value, 'duration' ) ) {
return false;
}
// Validate delay
if ( ! $this->is_valid_number_prop( $timing_value, 'delay' ) ) {
return false;
}
return true;
}
}