<?php
namespace Elementor;
use Elementor\Core\Base\Base_Object;
use Elementor\Core\DynamicTags\Manager;
use Elementor\Core\Breakpoints\Manager as Breakpoints_Manager;
use Elementor\Core\Frontend\Performance;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor controls stack.
*
* An abstract class that provides the needed properties and methods to
* manage and handle controls in the editor panel to inheriting classes.
*
* @since 1.4.0
* @abstract
*/
abstract class Controls_Stack extends Base_Object {
/**
* Responsive 'desktop' device name.
*
* @deprecated 3.4.0
*/
const RESPONSIVE_DESKTOP = 'desktop';
/**
* Responsive 'tablet' device name.
*
* @deprecated 3.4.0
*/
const RESPONSIVE_TABLET = 'tablet';
/**
* Responsive 'mobile' device name.
*
* @deprecated 3.4.0
*/
const RESPONSIVE_MOBILE = 'mobile';
/**
* Generic ID.
*
* Holds the unique ID.
*
* @access private
*
* @var string
*/
private $id;
private $active_settings;
private $parsed_active_settings;
/**
* Parsed Dynamic Settings.
*
* @access private
*
* @var null|array
*/
private $parsed_dynamic_settings;
/**
* Raw Data.
*
* Holds all the raw data including the element type, the child elements,
* the user data.
*
* @access private
*
* @var null|array
*/
private $data;
/**
* The configuration.
*
* Holds the configuration used to generate the Elementor editor. It includes
* the element name, icon, categories, etc.
*
* @access private
*
* @var null|array
*/
private $config;
/**
* The additional configuration.
*
* Holds additional configuration that has been set using `set_config` method.
* The `config` property is not modified directly while using the method because
* it's used to check whether the initial config already loaded (in `get_config`).
* After the initial config loaded, the additional config is merged into it.
*
* @access private
*
* @var null|array
*/
private $additional_config = [];
/**
* Current section.
*
* Holds the current section while inserting a set of controls sections.
*
* @access private
*
* @var null|array
*/
private $current_section;
/**
* Current tab.
*
* Holds the current tab while inserting a set of controls tabs.
*
* @access private
*
* @var null|array
*/
private $current_tab;
/**
* Current popover.
*
* Holds the current popover while inserting a set of controls.
*
* @access private
*
* @var null|array
*/
private $current_popover;
/**
* Injection point.
*
* Holds the injection point in the stack where the control will be inserted.
*
* @access private
*
* @var null|array
*/
private $injection_point;
/**
* Data sanitized.
*
* @access private
*
* @var bool
*/
private $settings_sanitized = false;
/**
* Element render attributes.
*
* Holds all the render attributes of the element. Used to store data like
* the HTML class name and the class value, or HTML element ID name and value.
*
* @access private
*
* @var array
*/
private $render_attributes = [];
/**
* Get element name.
*
* Retrieve the element name.
*
* @since 1.4.0
* @access public
* @abstract
*
* @return string The name.
*/
abstract public function get_name();
/**
* Get unique name.
*
* Some classes need to use unique names, this method allows you to create
* them. By default it retrieves the regular name.
*
* @since 1.6.0
* @access public
*
* @return string Unique name.
*/
public function get_unique_name() {
return $this->get_name();
}
/**
* Get element ID.
*
* Retrieve the element generic ID.
*
* @since 1.4.0
* @access public
*
* @return string The ID.
*/
public function get_id() {
return $this->id;
}
/**
* Get element ID.
*
* Retrieve the element generic ID as integer.
*
* @since 1.8.0
* @access public
*
* @return string The converted ID.
*/
public function get_id_int() {
/** We ignore possible notices, in order to support elements created prior to v1.8.0 and might include
* non-base 16 characters as part of their ID.
*/
return @hexdec( $this->id );
}
/**
* Get widget number.
*
* Get the first three numbers of the element converted ID.
*
* @since 3.16
* @access public
*
* @return string The widget number.
*/
public function get_widget_number(): string {
return substr( $this->get_id_int(), 0, 3 );
}
/**
* Get the type.
*
* Retrieve the type, e.g. 'stack', 'section', 'widget' etc.
*
* @since 1.4.0
* @access public
* @static
*
* @return string The type.
*/
public static function get_type() {
return 'stack';
}
/**
* @since 2.9.0
* @access public
*
* @return bool
*/
public function is_editable() {
return true;
}
/**
* Get current section.
*
* When inserting new controls, this method will retrieve the current section.
*
* @since 1.7.1
* @access public
*
* @return null|array Current section.
*/
public function get_current_section() {
return $this->current_section;
}
/**
* Get current tab.
*
* When inserting new controls, this method will retrieve the current tab.
*
* @since 1.7.1
* @access public
*
* @return null|array Current tab.
*/
public function get_current_tab() {
return $this->current_tab;
}
/**
* Get controls.
*
* Retrieve all the controls or, when requested, a specific control.
*
* @since 1.4.0
* @access public
*
* @param string $control_id The ID of the requested control. Optional field,
* when set it will return a specific control.
* Default is null.
*
* @return mixed Controls list.
*/
public function get_controls( $control_id = null ) {
$stack = $this->get_stack();
if ( null !== $control_id ) {
$control_data = self::get_items( $stack['controls'], $control_id );
if ( null === $control_data && ! empty( $stack['style_controls'] ) ) {
$control_data = self::get_items( $stack['style_controls'], $control_id );
}
return $control_data;
}
$controls = $stack['controls'];
if ( Performance::is_use_style_controls() && ! empty( $stack['style_controls'] ) ) {
$controls += $stack['style_controls'];
}
return self::get_items( $controls, $control_id );
}
/**
* Get active controls.
*
* Retrieve an array of active controls that meet the condition field.
*
* If specific controls was given as a parameter, retrieve active controls
* from that list, otherwise check for all the controls available.
*
* @since 1.4.0
* @since 2.0.9 Added the `controls` and the `settings` parameters.
* @access public
* @deprecated 3.0.0
*
* @param array $controls Optional. An array of controls. Default is null.
* @param array $settings Optional. Controls settings. Default is null.
*
* @return array Active controls.
*/
public function get_active_controls( array $controls = null, array $settings = null ) {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.0.0' );
if ( ! $controls ) {
$controls = $this->get_controls();
}
if ( ! $settings ) {
$settings = $this->get_controls_settings();
}
$active_controls = array_reduce(
array_keys( $controls ), function( $active_controls, $control_key ) use ( $controls, $settings ) {
$control = $controls[ $control_key ];
if ( $this->is_control_visible( $control, $settings, $controls ) ) {
$active_controls[ $control_key ] = $control;
}
return $active_controls;
}, []
);
return $active_controls;
}
/**
* Get controls settings.
*
* Retrieve the settings for all the controls that represent them.
*
* @since 1.5.0
* @access public
*
* @return array Controls settings.
*/
public function get_controls_settings() {
return array_intersect_key( $this->get_settings(), $this->get_controls() );
}
/**
* Add new control to stack.
*
* Register a single control to allow the user to set/update data.
*
* This method should be used inside `register_controls()`.
*
* @since 1.4.0
* @access public
*
* @param string $id Control ID.
* @param array $args Control arguments.
* @param array $options Optional. Control options. Default is an empty array.
*
* @return bool True if control added, False otherwise.
*/
public function add_control( $id, array $args, $options = [] ) {
$default_options = [
'overwrite' => false,
'position' => null,
];
if ( isset( $args['scheme'] ) ) {
$args['global'] = [
'default' => Plugin::$instance->kits_manager->convert_scheme_to_global( $args['scheme'] ),
];
unset( $args['scheme'] );
}
$options = array_merge( $default_options, $options );
if ( $options['position'] ) {
$this->start_injection( $options['position'] );
}
if ( $this->injection_point ) {
$options['index'] = $this->injection_point['index']++;
}
if ( empty( $args['type'] ) || ! in_array( $args['type'], [ Controls_Manager::SECTION, Controls_Manager::WP_WIDGET ], true ) ) {
$args = $this->handle_control_position( $args, $id, $options['overwrite'] );
}
if ( $options['position'] ) {
$this->end_injection();
}
unset( $options['position'] );
if ( $this->current_popover ) {
$args['popover'] = [];
if ( ! $this->current_popover['initialized'] ) {
$args['popover']['start'] = true;
$this->current_popover['initialized'] = true;
}
}
if ( Performance::should_optimize_controls() ) {
$ui_controls = [
Controls_Manager::RAW_HTML,
Controls_Manager::DIVIDER,
Controls_Manager::HEADING,
Controls_Manager::BUTTON,
Controls_Manager::ALERT,
Controls_Manager::NOTICE,
Controls_Manager::DEPRECATED_NOTICE,
];
if ( ! empty( $args['type'] ) && ! empty( $args['section'] ) && in_array( $args['type'], $ui_controls ) ) {
$args = [
'type' => $args['type'],
'section' => $args['section'],
];
}
unset(
$args['label_block'],
$args['label'],
$args['title'],
$args['tab'],
$args['options'],
$args['placeholder'],
$args['separator'],
$args['size_units'],
$args['range'],
$args['toggle'],
$args['ai'],
$args['classes'],
$args['style_transfer'],
$args['show_label'],
$args['description'],
$args['label_on'],
$args['label_off'],
$args['labels'],
$args['handles'],
$args['editor_available'],
);
}
return Plugin::$instance->controls_manager->add_control_to_stack( $this, $id, $args, $options );
}
/**
* Remove control from stack.
*
* Unregister an existing control and remove it from the stack.
*
* @since 1.4.0
* @access public
*
* @param string $control_id Control ID.
*
* @return bool|\WP_Error
*/
public function remove_control( $control_id ) {
return Plugin::$instance->controls_manager->remove_control_from_stack( $this->get_unique_name(), $control_id );
}
/**
* Update control in stack.
*
* Change the value of an existing control in the stack. When you add new
* control you set the `$args` parameter, this method allows you to update
* the arguments by passing new data.
*
* @since 1.4.0
* @since 1.8.1 New `$options` parameter added.
*
* @access public
*
* @param string $control_id Control ID.
* @param array $args Control arguments. Only the new fields you want
* to update.
* @param array $options Optional. Some additional options. Default is
* an empty array.
*
* @return bool
*/
public function update_control( $control_id, array $args, array $options = [] ) {
$is_updated = Plugin::$instance->controls_manager->update_control_in_stack( $this, $control_id, $args, $options );
if ( ! $is_updated ) {
return false;
}
$control = $this->get_controls( $control_id );
if ( Controls_Manager::SECTION === $control['type'] ) {
$section_args = $this->get_section_args( $control_id );
$section_controls = $this->get_section_controls( $control_id );
foreach ( $section_controls as $section_control_id => $section_control ) {
$this->update_control( $section_control_id, $section_args, $options );
}
}
return true;
}
/**
* Get stack.
*
* Retrieve the stack of controls.
*
* @since 1.9.2
* @access public
*
* @return array Stack of controls.
*/
public function get_stack() {
$stack = Plugin::$instance->controls_manager->get_element_stack( $this );
if ( null === $stack ) {
$this->init_controls();
return Plugin::$instance->controls_manager->get_element_stack( $this );
}
return $stack;
}
/**
* Get position information.
*
* Retrieve the position while injecting data, based on the element type.
*
* @since 1.7.0
* @access public
*
* @param array $position {
* The injection position.
*
* @type string $type Injection type, either `control` or `section`.
* Default is `control`.
* @type string $at Where to inject. If `$type` is `control` accepts
* `before` and `after`. If `$type` is `section`
* accepts `start` and `end`. Default values based on
* the `type`.
* @type string $of Control/Section ID.
* @type array $fallback Fallback injection position. When the position is
* not found it will try to fetch the fallback
* position.
* }
*
* @return bool|array Position info.
*/
final public function get_position_info( array $position ) {
$default_position = [
'type' => 'control',
'at' => 'after',
];
if ( ! empty( $position['type'] ) && 'section' === $position['type'] ) {
$default_position['at'] = 'end';
}
$position = array_merge( $default_position, $position );
if (
( 'control' === $position['type'] && in_array( $position['at'], [ 'start', 'end' ], true ) ) ||
( 'section' === $position['type'] && in_array( $position['at'], [ 'before', 'after' ], true ) )
) {
_doing_it_wrong( sprintf( '%s::%s', get_called_class(), __FUNCTION__ ), 'Invalid position arguments. Use `before` / `after` for control or `start` / `end` for section.', '1.7.0' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return false;
}
$target_control_index = $this->get_control_index( $position['of'] );
if ( false === $target_control_index ) {
if ( ! empty( $position['fallback'] ) ) {
return $this->get_position_info( $position['fallback'] );
}
return false;
}
$target_section_index = $target_control_index;
$registered_controls = $this->get_controls();
$controls_keys = array_keys( $registered_controls );
while ( Controls_Manager::SECTION !== $registered_controls[ $controls_keys[ $target_section_index ] ]['type'] ) {
--$target_section_index;
}
if ( 'section' === $position['type'] ) {
++$target_control_index;
if ( 'end' === $position['at'] ) {
while ( Controls_Manager::SECTION !== $registered_controls[ $controls_keys[ $target_control_index ] ]['type'] ) {
if ( ++$target_control_index >= count( $registered_controls ) ) {
break;
}
}
}
}
$target_control = $registered_controls[ $controls_keys[ $target_control_index ] ];
if ( 'after' === $position['at'] ) {
++$target_control_index;
}
$section_id = $registered_controls[ $controls_keys[ $target_section_index ] ]['name'];
$position_info = [
'index' => $target_control_index,
'section' => $this->get_section_args( $section_id ),
];
if ( ! empty( $target_control['tabs_wrapper'] ) ) {
$position_info['tab'] = [
'tabs_wrapper' => $target_control['tabs_wrapper'],
'inner_tab' => $target_control['inner_tab'],
];
}
return $position_info;
}
/**
* Get control key.
*
* Retrieve the key of the control based on a given index of the control.
*
* @since 1.9.2
* @access public
*
* @param string $control_index Control index.
*
* @return int Control key.
*/
final public function get_control_key( $control_index ) {
$registered_controls = $this->get_controls();
$