first commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects interaction data from all rendered documents and provides centralized access.
|
||||
*/
|
||||
class Interactions_Collector {
|
||||
/**
|
||||
* @var Interactions_Collector
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* @var array Stores interaction data keyed by element ID
|
||||
* Format: [ 'element_id' => [ interaction_items... ] ]
|
||||
*/
|
||||
private $interactions_data = [];
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return Interactions_Collector
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register interaction data for an element.
|
||||
*
|
||||
* @param string $element_id The element ID (data-id attribute value)
|
||||
* @param array $interactions The full interactions array from the element
|
||||
*/
|
||||
public function register( $element_id, $interactions ) {
|
||||
if ( empty( $element_id ) || empty( $interactions ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->interactions_data[ $element_id ] = $interactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all collected interaction data.
|
||||
*
|
||||
* @return array Format: [ 'element_id' => interactions_array ]
|
||||
*/
|
||||
public function get_all() {
|
||||
return $this->interactions_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction data for a specific element.
|
||||
*
|
||||
* @param string $element_id The element ID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( $element_id ) {
|
||||
return $this->interactions_data[ $element_id ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset collected data (useful for testing or page reloads).
|
||||
*/
|
||||
public function reset() {
|
||||
$this->interactions_data = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions;
|
||||
|
||||
use Elementor\Plugin;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles frontend-specific interaction logic including:
|
||||
* - Collecting interactions from document elements during render
|
||||
* - Outputting interaction data as JSON in the page footer
|
||||
*
|
||||
* This class is responsible for the frontend rendering pipeline of interactions,
|
||||
* working with the Interactions_Collector for data storage and Adapter for data transformation.
|
||||
*/
|
||||
class Interactions_Frontend_Handler {
|
||||
/**
|
||||
* @var callable|null
|
||||
*/
|
||||
private $config_provider;
|
||||
|
||||
public function __construct( $config_provider = null ) {
|
||||
$this->config_provider = is_callable( $config_provider ) ? $config_provider : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect interactions from document elements during frontend render.
|
||||
*
|
||||
* This method is hooked to 'elementor/frontend/builder_content_data' filter
|
||||
* to capture interactions from all documents (header, footer, post content)
|
||||
* as they are rendered.
|
||||
*
|
||||
* @param array $elements_data The document's elements data.
|
||||
* @param int $post_id The document's post ID.
|
||||
* @return array The unmodified elements data (pass-through filter).
|
||||
*/
|
||||
public function collect_document_interactions( $elements_data, $post_id ) {
|
||||
// Only collect on frontend, not in editor
|
||||
if ( Plugin::$instance->editor->is_edit_mode() ) {
|
||||
return $elements_data;
|
||||
}
|
||||
|
||||
if ( empty( $elements_data ) || ! is_array( $elements_data ) ) {
|
||||
return $elements_data;
|
||||
}
|
||||
|
||||
$collector = Interactions_Collector::instance();
|
||||
|
||||
// Recursively collect interactions from all elements
|
||||
$this->collect_interactions_recursive( $elements_data, $collector );
|
||||
|
||||
return $elements_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively iterate through all elements and collect interactions.
|
||||
*
|
||||
* @param array $elements Array of element data.
|
||||
* @param Interactions_Collector $collector The collector instance.
|
||||
*/
|
||||
private function collect_interactions_recursive( $elements, $collector ) {
|
||||
if ( ! is_array( $elements ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $elements as $element ) {
|
||||
if ( ! is_array( $element ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this element has interactions
|
||||
if ( ! empty( $element['id'] ) && isset( $element['interactions'] ) ) {
|
||||
$element_id = $element['id'];
|
||||
$interactions = $element['interactions'];
|
||||
|
||||
// Decode if it's a JSON string
|
||||
if ( is_string( $interactions ) ) {
|
||||
$decoded = json_decode( $interactions, true );
|
||||
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
|
||||
$interactions = $decoded;
|
||||
} else {
|
||||
$interactions = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the interactions format - ensure we have items array
|
||||
if ( is_array( $interactions ) ) {
|
||||
// If interactions has 'items' key, it's already in the right format
|
||||
// If not, check if it's a direct array of items or has other structure
|
||||
if ( ! isset( $interactions['items'] ) ) {
|
||||
// Check if this looks like a direct array of interaction items
|
||||
// (first element has 'trigger' or 'animation' or '$$type')
|
||||
$first_item = reset( $interactions );
|
||||
if ( is_array( $first_item ) && ( isset( $first_item['trigger'] ) || isset( $first_item['animation'] ) || isset( $first_item['$$type'] ) ) ) {
|
||||
// It's a direct array of items, wrap it
|
||||
$interactions = [ 'items' => $interactions ];
|
||||
}
|
||||
}
|
||||
|
||||
// Register with collector if we have valid items
|
||||
$items = $interactions['items'] ?? [];
|
||||
if ( ! empty( $items ) || ! empty( $interactions ) ) {
|
||||
$collector->register( $element_id, $interactions );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process child elements
|
||||
if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
|
||||
$this->collect_interactions_recursive( $element['elements'], $collector );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output collected interaction data as a JSON script tag in the footer.
|
||||
*
|
||||
* This method is hooked to 'wp_footer' to output all collected interactions
|
||||
* as a centralized JSON data block that the frontend JavaScript can consume.
|
||||
*/
|
||||
public function print_interactions_data() {
|
||||
// Only output on frontend, not in editor
|
||||
if ( Plugin::$instance->editor->is_edit_mode() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collector = Interactions_Collector::instance();
|
||||
$all_interactions = $collector->get_all();
|
||||
|
||||
if ( empty( $all_interactions ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format: array of elements, each with elementId, dataId, and cleaned interactions
|
||||
$elements_with_interactions = [];
|
||||
foreach ( $all_interactions as $element_id => $interactions ) {
|
||||
$items = $this->extract_interaction_items( $interactions );
|
||||
|
||||
if ( empty( $items ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build element entry with elementId, dataId, and cleaned interactions array
|
||||
$elements_with_interactions[] = [
|
||||
'elementId' => $element_id,
|
||||
'dataId' => $element_id,
|
||||
'interactions' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
if ( empty( $elements_with_interactions ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueue_interactions_assets();
|
||||
|
||||
// Output as JSON script tag
|
||||
$json_data = wp_json_encode( $elements_with_interactions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON data is already encoded
|
||||
echo '<script type="application/json" id="' . Module::SCRIPT_ID_INTERACTIONS_DATA . '">' . $json_data . '</script>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract interaction items from various data formats.
|
||||
*
|
||||
* Handles multiple formats:
|
||||
* - v1 format: { items: [...] }
|
||||
* - v2 format with $$type: { items: { $$type: '...', value: [...] } }
|
||||
* - Direct arrays: [{ trigger: ..., animation: ... }, ...]
|
||||
*
|
||||
* @param array $interactions The interactions data.
|
||||
* @return array The extracted items array.
|
||||
*/
|
||||
private function extract_interaction_items( $interactions ) {
|
||||
if ( ! is_array( $interactions ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if it has 'items' key (standard format)
|
||||
if ( isset( $interactions['items'] ) ) {
|
||||
$items = $interactions['items'];
|
||||
|
||||
return is_array( $items ) ? $items : [];
|
||||
}
|
||||
|
||||
// Check if interactions itself is a direct array of items
|
||||
// (first element has interaction-related keys)
|
||||
$first_item = reset( $interactions );
|
||||
if ( is_array( $first_item ) && (
|
||||
isset( $first_item['trigger'] ) ||
|
||||
isset( $first_item['animation'] ) ||
|
||||
isset( $first_item['$$type'] )
|
||||
) ) {
|
||||
return $interactions;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function enqueue_interactions_assets() {
|
||||
wp_enqueue_script( Module::HANDLE_MOTION_JS );
|
||||
wp_enqueue_script( Module::HANDLE_FRONTEND );
|
||||
|
||||
$config = $this->config_provider ? call_user_func( $this->config_provider ) : [];
|
||||
|
||||
wp_localize_script(
|
||||
Module::HANDLE_FRONTEND,
|
||||
Module::JS_CONFIG_OBJECT,
|
||||
$config
|
||||
);
|
||||
}
|
||||
}
|
||||
178
wp-content/plugins/elementor/modules/interactions/module.php
Normal file
178
wp-content/plugins/elementor/modules/interactions/module.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?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';
|
||||
|
||||
const HANDLE_MOTION_JS = 'motion-js';
|
||||
const HANDLE_SHARED_UTILS = 'elementor-interactions-shared-utils';
|
||||
const HANDLE_FRONTEND = 'elementor-interactions';
|
||||
const HANDLE_EDITOR = 'elementor-editor-interactions';
|
||||
const JS_CONFIG_OBJECT = 'ElementorInteractionsConfig';
|
||||
const SCRIPT_ID_INTERACTIONS_DATA = 'elementor-interactions-data';
|
||||
|
||||
public function get_name() {
|
||||
return self::MODULE_NAME;
|
||||
}
|
||||
|
||||
private $preset_animations;
|
||||
|
||||
private $frontend_handler;
|
||||
|
||||
private function get_presets() {
|
||||
if ( ! $this->preset_animations ) {
|
||||
$this->preset_animations = new Presets();
|
||||
}
|
||||
|
||||
return $this->preset_animations;
|
||||
}
|
||||
|
||||
private function get_frontend_handler() {
|
||||
if ( ! $this->frontend_handler ) {
|
||||
$this->frontend_handler = new Interactions_Frontend_Handler( fn () => $this->get_config() );
|
||||
}
|
||||
|
||||
return $this->frontend_handler;
|
||||
}
|
||||
|
||||
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/preview/enqueue_scripts', fn () => $this->enqueue_preview_scripts() );
|
||||
add_action( 'elementor/editor/after_enqueue_scripts', fn () => $this->enqueue_editor_scripts() );
|
||||
|
||||
// Collect interactions from documents before they render (header, footer, post content)
|
||||
add_filter( 'elementor/frontend/builder_content_data', [ $this->get_frontend_handler(), 'collect_document_interactions' ], 10, 2 );
|
||||
|
||||
// Output centralized interaction data in footer
|
||||
add_action( 'wp_footer', [ $this->get_frontend_handler(), 'print_interactions_data' ], 1 );
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
public function get_config() {
|
||||
return [
|
||||
'constants' => $this->get_presets()->defaults(),
|
||||
'breakpoints' => $this->get_active_breakpoints(),
|
||||
];
|
||||
}
|
||||
|
||||
private function get_active_breakpoints() {
|
||||
$breakpoints_config = Plugin::$instance->breakpoints->get_breakpoints_config();
|
||||
$active_breakpoints = Plugin::$instance->breakpoints->get_active_breakpoints();
|
||||
|
||||
$breakpoints = [];
|
||||
|
||||
foreach ( array_keys( $active_breakpoints ) as $breakpoint_label ) {
|
||||
$breakpoints[ $breakpoint_label ] = $breakpoints_config[ $breakpoint_label ];
|
||||
}
|
||||
|
||||
return $breakpoints;
|
||||
}
|
||||
|
||||
private function register_frontend_scripts() {
|
||||
$suffix = ( Utils::is_script_debug() || Utils::is_elementor_tests() ) ? '' : '.min';
|
||||
|
||||
wp_register_script(
|
||||
self::HANDLE_MOTION_JS,
|
||||
ELEMENTOR_ASSETS_URL . 'lib/motion/motion' . $suffix . '.js',
|
||||
[],
|
||||
'11.13.5',
|
||||
true
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
self::HANDLE_SHARED_UTILS,
|
||||
$this->get_js_assets_url( 'interactions-shared-utils' ),
|
||||
[ self::HANDLE_MOTION_JS ],
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
self::HANDLE_FRONTEND,
|
||||
$this->get_js_assets_url( 'interactions' ),
|
||||
[ self::HANDLE_MOTION_JS, self::HANDLE_SHARED_UTILS ],
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
self::HANDLE_EDITOR,
|
||||
$this->get_js_assets_url( 'editor-interactions' ),
|
||||
[ self::HANDLE_MOTION_JS, self::HANDLE_SHARED_UTILS ],
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_editor_scripts() {
|
||||
wp_add_inline_script(
|
||||
'elementor-common',
|
||||
'window.' . self::JS_CONFIG_OBJECT . ' = ' . wp_json_encode( $this->get_config() ) . ';',
|
||||
'before'
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_preview_scripts() {
|
||||
wp_enqueue_script( self::HANDLE_SHARED_UTILS );
|
||||
// Ensure motion-js and editor-interactions handler are available in preview iframe
|
||||
wp_enqueue_script( self::HANDLE_MOTION_JS );
|
||||
wp_enqueue_script( self::HANDLE_EDITOR );
|
||||
|
||||
wp_localize_script(
|
||||
self::HANDLE_EDITOR,
|
||||
self::JS_CONFIG_OBJECT,
|
||||
$this->get_config()
|
||||
);
|
||||
}
|
||||
}
|
||||
107
wp-content/plugins/elementor/modules/interactions/parser.php
Normal file
107
wp-content/plugins/elementor/modules/interactions/parser.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Presets {
|
||||
const DEFAULT_DURATION = 600;
|
||||
const DEFAULT_DELAY = 0;
|
||||
const DEFAULT_SLIDE_DISTANCE = 100;
|
||||
const DEFAULT_SCALE_START = 0;
|
||||
const DEFAULT_RELATIVE_TO = 'viewport';
|
||||
const DEFAULT_END = 15;
|
||||
const DEFAULT_START = 85;
|
||||
|
||||
const BASE_TRIGGERS = [ 'load', 'scrollIn' ];
|
||||
const ADDITIONAL_TRIGGERS = [ 'scrollOut', 'scrollOn', 'hover', 'click' ];
|
||||
|
||||
const DEFAULT_EASING = 'easeIn';
|
||||
const BASE_EFFECTS = [ 'fade', 'slide', 'scale' ];
|
||||
const ADDITIONAL_EFFECTS = [ 'custom' ];
|
||||
|
||||
const TYPES = [ 'in', 'out' ];
|
||||
const DIRECTIONS = [ 'left', 'right', 'top', 'bottom', '' ];
|
||||
|
||||
const BASE_EASING = [ 'easeIn' ];
|
||||
const ADDITIONAL_EASING = [ 'easeOut', 'easeInOut', 'backIn', 'backInOut', 'backOut', 'linear' ];
|
||||
|
||||
const DEFAULT_REPEAT = '';
|
||||
const REPEAT_OPTIONS = [ 'loop', 'times', '' ];
|
||||
|
||||
public static function easing_options() {
|
||||
return array_merge( self::BASE_EASING, self::ADDITIONAL_EASING );
|
||||
}
|
||||
|
||||
public static function effects_options() {
|
||||
return array_merge( self::BASE_EFFECTS, self::ADDITIONAL_EFFECTS );
|
||||
}
|
||||
|
||||
public static function triggers_options() {
|
||||
return array_merge( self::BASE_TRIGGERS, self::ADDITIONAL_TRIGGERS );
|
||||
}
|
||||
|
||||
public function defaults() {
|
||||
return [
|
||||
'defaultDuration' => self::DEFAULT_DURATION,
|
||||
'defaultDelay' => self::DEFAULT_DELAY,
|
||||
'slideDistance' => self::DEFAULT_SLIDE_DISTANCE,
|
||||
'scaleStart' => self::DEFAULT_SCALE_START,
|
||||
'defaultEasing' => self::DEFAULT_EASING,
|
||||
'relativeTo' => self::DEFAULT_RELATIVE_TO,
|
||||
'repeat' => self::DEFAULT_REPEAT,
|
||||
'start' => self::DEFAULT_START,
|
||||
'end' => self::DEFAULT_END,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Boolean_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
|
||||
use Elementor\Modules\Interactions\Presets;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Animation_Config_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'config-v2';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'replay' => Boolean_Prop_Type::make()->meta( 'pro', true )->description( 'Whether to replay the animation' ),
|
||||
'easing' => String_Prop_Type::make()->meta( 'enum', Presets::easing_options() )->default( Presets::DEFAULT_EASING )->meta( 'pro', Presets::ADDITIONAL_EASING )->description( 'The easing function to use for the animation' ),
|
||||
'relativeTo' => String_Prop_Type::make()->meta( 'pro', true )->description( 'The container scope used by scroll-based interactions' ),
|
||||
'repeat' => String_Prop_Type::make()->meta( 'enum', Presets::REPEAT_OPTIONS )->default( Presets::DEFAULT_REPEAT )->meta( 'pro', true )->description( 'Repeat mode for interactions that can run multiple times' ),
|
||||
'times' => Number_Prop_Type::make()->meta( 'pro', true )->description( 'Total number of times to play when repeat mode is "times"' ),
|
||||
'start' => Size_Prop_Type::make()->units( '%' )->default_unit( '%' )->meta( 'pro', true )->description( 'The start to use for the animation' ),
|
||||
'end' => Size_Prop_Type::make()->units( '%' )->default_unit( '%' )->meta( 'pro', true )->description( 'The end to use for the animation' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
|
||||
use Elementor\Modules\Interactions\Presets;
|
||||
use Elementor\Modules\Interactions\Utils\Prop_Shape_Filter_For_Pro;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Animation_Preset_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'animation-preset-props';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'effect' => String_Prop_Type::make()->meta( 'enum', Presets::effects_options() )->meta( 'pro', Presets::ADDITIONAL_EFFECTS )->description( 'The effect to use for the animation' ),
|
||||
'type' => String_Prop_Type::make()->meta( 'enum', Presets::TYPES )->description( 'The type to use for the animation' ),
|
||||
'direction' => String_Prop_Type::make()->meta( 'enum', Presets::DIRECTIONS )->description( 'The direction to use for the animation' ),
|
||||
'timing_config' => Timing_Config_Prop_Type::make()->description( 'The timing config to use for the animation' ),
|
||||
'config' => Animation_Config_Prop_Type::make()->description( 'The config to use for the animation' ),
|
||||
'custom_effect' => Custom_Effect_Prop_Type::make()->meta( 'pro', true ),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Custom_Effect_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'custom-effect';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'keyframes' => Keyframes_Prop_Type::make()->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Array_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Excluded_Breakpoints_Prop_Type extends Array_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'excluded-breakpoints';
|
||||
}
|
||||
|
||||
protected function define_item_type(): Prop_Type {
|
||||
return String_Prop_Type::make();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Interaction_Breakpoints_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'interaction-breakpoints';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'excluded' => Excluded_Breakpoints_Prop_Type::make()->description( 'The excluded breakpoints' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type;
|
||||
use Elementor\Modules\Interactions\Presets;
|
||||
use Elementor\Modules\Interactions\Utils\Prop_Shape_Filter_For_Pro;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Interaction_Item_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'interaction-item';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'interaction_id' => String_Prop_Type::make()->description( 'The interaction id to use for the animation' ),
|
||||
'trigger' => String_Prop_Type::make()->meta( 'enum', Presets::triggers_options() )->meta( 'pro', Presets::ADDITIONAL_TRIGGERS )->description( 'The trigger to use for the animation' ),
|
||||
'animation' => Animation_Preset_Prop_Type::make()->description( 'The animation to use for the interaction' ),
|
||||
'breakpoints' => Interaction_Breakpoints_Prop_Type::make()->description( 'The breakpoints to use for the animation' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\Styles\Size_Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Keyframe_Stop_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'keyframe-stop';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'stop' => Size_Prop_Type::make()
|
||||
->default_unit( Size_Constants::UNIT_PERCENT )
|
||||
->required(),
|
||||
'settings' => Keyframe_Stop_Settings_Prop_Type::make()
|
||||
->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Move_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Rotate_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Scale_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Transform\Functions\Transform_Skew_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Keyframe_Stop_Settings_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'keyframe-stop-settings';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'opacity' => Size_Prop_Type::make(),
|
||||
'move' => Transform_Move_Prop_Type::make(),
|
||||
'rotate' => Transform_Rotate_Prop_Type::make(),
|
||||
'scale' => Transform_Scale_Prop_Type::make(),
|
||||
'skew' => Transform_Skew_Prop_Type::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function validate_value( $value ): bool {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed_keys = array_keys( $this->get_shape() );
|
||||
$value_keys = array_keys( $value );
|
||||
|
||||
if ( array_diff( $value_keys, $allowed_keys ) !== [] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::validate_value( $value );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Array_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Keyframes_Prop_Type extends Array_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'keyframes';
|
||||
}
|
||||
|
||||
protected function define_item_type(): Prop_Type {
|
||||
return Keyframe_Stop_Prop_Type::make();
|
||||
}
|
||||
|
||||
protected function validate_value( $value ): bool {
|
||||
$is_empty_array = empty( $value ) && is_array( $value );
|
||||
|
||||
if ( $is_empty_array ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::validate_value( $value );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Size_Prop_Type;
|
||||
use Elementor\Modules\AtomicWidgets\Styles\Size_Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Time_Size_Prop_Type extends Size_Prop_Type {
|
||||
public static function make() {
|
||||
return parent::make()->units( Size_Constants::time() )->default_unit( Size_Constants::UNIT_MILLI_SECOND );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Props;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Timing_Config_Prop_Type extends Object_Prop_Type {
|
||||
public static function get_key(): string {
|
||||
return 'timing-config';
|
||||
}
|
||||
|
||||
protected function define_shape(): array {
|
||||
return [
|
||||
'duration' => Time_Size_Prop_Type::make()->description( 'The duration to use for the animation' ),
|
||||
'delay' => Time_Size_Prop_Type::make()->description( 'The delay to use for the animation' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Schema;
|
||||
|
||||
use Elementor\Modules\Interactions\Props\Interaction_Item_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Interactions_Schema {
|
||||
public static function get() {
|
||||
return apply_filters( 'elementor/atomic-widgets/interactions/schema', static::get_interactions_schema() );
|
||||
}
|
||||
|
||||
public static function get_interactions_schema(): array {
|
||||
return [
|
||||
'version' => 1,
|
||||
'items' => [ Interaction_Item_Prop_Type::make()->description( 'Interaction item' ) ],
|
||||
];
|
||||
}
|
||||
}
|
||||
416
wp-content/plugins/elementor/modules/interactions/validation.php
Normal file
416
wp-content/plugins/elementor/modules/interactions/validation.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions;
|
||||
|
||||
use Elementor\Modules\Interactions\Validators\Breakpoints_Value as BreakpointsValueValidator;
|
||||
use Elementor\Modules\Interactions\Validators\Custom_Effect_Value;
|
||||
use Elementor\Modules\Interactions\Validators\String_Value as StringValueValidator;
|
||||
use Elementor\Modules\Interactions\Validators\Trigger_Value as TriggerValueValidator;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Validation {
|
||||
private $elements_to_interactions_counter = [];
|
||||
private $max_number_of_interactions = 5;
|
||||
|
||||
private const VALID_EFFECTS = [ 'fade', 'slide', 'scale', 'custom' ];
|
||||
private const VALID_TYPES = [ 'in', 'out' ];
|
||||
private const VALID_DIRECTIONS = [ '', 'left', 'right', 'top', 'bottom' ];
|
||||
private const VALID_REPEAT_MODES = [ '', 'loop', 'times' ];
|
||||
|
||||
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 ) ) {
|
||||
if ( isset( $interactions['items']['$$type'] ) && 'array' === $interactions['items']['$$type'] ) {
|
||||
return isset( $interactions['items']['value'] ) ? $interactions['items']['value'] : [];
|
||||
}
|
||||
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 ) ) {
|
||||
if ( isset( $decoded['items']['$$type'] ) && 'array' === $decoded['items']['$$type'] ) {
|
||||
return isset( $decoded['items']['value'] ) ? $decoded['items']['value'] : [];
|
||||
}
|
||||
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_trigger_prop( $value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->is_valid_animation_prop( $value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->is_valid_breakpoints_prop( $value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function is_valid_trigger_prop( $data ) {
|
||||
if ( ! array_key_exists( 'trigger', $data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TriggerValueValidator::is_valid( $data['trigger'] );
|
||||
}
|
||||
|
||||
private function is_valid_breakpoints_prop( $data ) {
|
||||
if ( array_key_exists( 'breakpoints', $data ) ) {
|
||||
return BreakpointsValueValidator::is_valid( $data['breakpoints'] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function is_valid_string_prop( $data, $key, $allowed_values = null ) {
|
||||
if ( ! isset( $data[ $key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringValueValidator::is_valid( $data[ $key ], $allowed_values );
|
||||
}
|
||||
|
||||
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_value = $data['config']['value'];
|
||||
|
||||
if ( isset( $config_value['replay'] ) && ! $this->is_valid_boolean_prop( $config_value, 'replay' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $config_value['easing'] ) && ! $this->is_valid_string_prop( $config_value, 'easing' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $config_value['relativeTo'] ) && ! $this->is_valid_string_prop( $config_value, 'relativeTo' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $config_value['repeat'] ) && ! $this->is_valid_string_prop( $config_value, 'repeat', self::VALID_REPEAT_MODES ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $config_value['times'] ) && ! $this->is_valid_number_prop_in_range( $config_value, 'times', 1 ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $config_value['start'] ) && ! $this->is_valid_size_prop_in_range( $config_value, 'start', 0, 100 ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $config_value['end'] ) && ! $this->is_valid_size_prop_in_range( $config_value, 'end', 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;
|
||||
}
|
||||
|
||||
if ( ! Custom_Effect_Value::is_valid( $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 (accepts both 'number' and 'size' formats)
|
||||
if ( ! $this->is_valid_timing_value( $timing_value, 'duration', 0 ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate delay (accepts both 'number' and 'size' formats)
|
||||
if ( ! $this->is_valid_timing_value( $timing_value, 'delay', 0 ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate timing value that can be either 'number' or 'size' type.
|
||||
* - number format: {$$type: 'number', value: 123}
|
||||
* - size format: {$$type: 'size', value: {size: 123, unit: 'ms'}}
|
||||
*/
|
||||
private function is_valid_timing_value( $data, $key, $min = null, $max = null ) {
|
||||
if ( ! isset( $data[ $key ] ) || ! is_array( $data[ $key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prop = $data[ $key ];
|
||||
|
||||
if ( ! isset( $prop['$$type'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Accept 'number' format
|
||||
if ( 'number' === $prop['$$type'] ) {
|
||||
return $this->is_valid_number_prop_in_range( $data, $key, $min, $max );
|
||||
}
|
||||
|
||||
// Accept 'size' format
|
||||
if ( 'size' === $prop['$$type'] ) {
|
||||
return $this->is_valid_size_prop_in_range( $data, $key, $min, $max );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate size prop: {$$type: 'size', value: {size: X, unit: 'ms'}}
|
||||
*/
|
||||
private function is_valid_size_prop_in_range( $data, $key, $min = null, $max = null ) {
|
||||
if ( ! isset( $data[ $key ] ) || ! is_array( $data[ $key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prop = $data[ $key ];
|
||||
|
||||
if ( ! isset( $prop['$$type'] ) || 'size' !== $prop['$$type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $prop['value'] ) || ! is_array( $prop['value'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $prop['value']['size'] ) || ! is_numeric( $prop['value']['size'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = (float) $prop['value']['size'];
|
||||
|
||||
if ( null !== $min && $value < $min ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( null !== $max && $value > $max ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Validators;
|
||||
|
||||
use Elementor\Modules\Interactions\Validators\String_Value as StringValueValidator;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Breakpoints_Value {
|
||||
public static function is_valid( $breakpoints_prop_value ) {
|
||||
if ( ! is_array( $breakpoints_prop_value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $breakpoints_prop_value['$$type'] ) || 'interaction-breakpoints' !== $breakpoints_prop_value['$$type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $breakpoints_prop_value['value'] ) || ! is_array( $breakpoints_prop_value['value'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::validate_value( $breakpoints_prop_value['value'] );
|
||||
}
|
||||
|
||||
private static function validate_value( $value ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $value['excluded'] ) || ! is_array( $value['excluded'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::validate_excluded( $value['excluded'] );
|
||||
}
|
||||
|
||||
private static function validate_excluded( $excluded ) {
|
||||
if ( ! is_array( $excluded ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $excluded['$$type'] ) || 'excluded-breakpoints' !== $excluded['$$type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $excluded['value'] ) || ! is_array( $excluded['value'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::validate_excluded_value( $excluded['value'] );
|
||||
}
|
||||
|
||||
private static function validate_excluded_value( $value ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $value as $breakpoint_value ) {
|
||||
if ( ! StringValueValidator::is_valid( $breakpoint_value ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Validators;
|
||||
|
||||
use Elementor\Modules\AtomicWidgets\Parsers\Props_Parser;
|
||||
use Elementor\Modules\Interactions\Props\Custom_Effect_Prop_Type;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: At least a value validator interface to enforce is_valid fxn for consistency
|
||||
*/
|
||||
class Custom_Effect_Value {
|
||||
public static function is_valid( array $animation_value ): bool {
|
||||
$effect_value = $animation_value['effect']['value'] ?? null;
|
||||
|
||||
if ( 'custom' !== $effect_value ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! isset( $animation_value['custom_effect'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$props_parser = Props_Parser::make( [
|
||||
'custom_effect' => Custom_Effect_Prop_Type::make(),
|
||||
] );
|
||||
|
||||
$result = $props_parser->parse( [ 'custom_effect' => $animation_value['custom_effect'] ] );
|
||||
|
||||
return $result->is_valid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Validators;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class String_Value {
|
||||
public static function is_valid( $prop_value, $allowed_values = null ) {
|
||||
if ( ! is_array( $prop_value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $prop_value['$$type'] ) || 'string' !== $prop_value['$$type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $prop_value['value'] ) || ! is_string( $prop_value['value'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( null !== $allowed_values && ! in_array( $prop_value['value'], $allowed_values, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Elementor\Modules\Interactions\Validators;
|
||||
|
||||
use Elementor\Modules\Interactions\Validators\String_Value as StringValueValidator;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class Trigger_Value {
|
||||
private const VALID_TRIGGERS = [
|
||||
'load',
|
||||
'scrollIn',
|
||||
'scrollOut',
|
||||
'scrollOn',
|
||||
'hover',
|
||||
'click',
|
||||
];
|
||||
|
||||
public static function is_valid( $trigger_prop_value ) {
|
||||
return StringValueValidator::is_valid( $trigger_prop_value, static::VALID_TRIGGERS );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user