* Unregister an existing control and remove it from the stack.
* @param string $control_id Control ID.
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.8.1 New `$options` parameter added.
* @param string $control_id Control ID.
* @param array $args Control arguments. Only the new fields you want
* @param array $options Optional. Some additional options. Default is
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 );
$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 );
* Retrieve the stack of controls.
* @return array Stack of controls.
public function get_stack() {
$stack = Plugin::$instance->controls_manager->get_element_stack( $this );
return Plugin::$instance->controls_manager->get_element_stack( $this );
* Get position information.
* Retrieve the position while injecting data, based on the element type.
* @param array $position {
* The injection position.
* @type string $type Injection type, either `control` or `section`.
* @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
* @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
* @return bool|array Position info.
final public function get_position_info( array $position ) {
if ( ! empty( $position['type'] ) && 'section' === $position['type'] ) {
$default_position['at'] = 'end';
$position = array_merge( $default_position, $position );
( '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
$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'] );
$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'] ) {
if ( 'section' === $position['type'] ) {
if ( 'end' === $position['at'] ) {
while ( Controls_Manager::SECTION !== $registered_controls[ $controls_keys[ $target_control_index ] ]['type'] ) {
if ( ++$target_control_index >= count( $registered_controls ) ) {
$target_control = $registered_controls[ $controls_keys[ $target_control_index ] ];
if ( 'after' === $position['at'] ) {
$section_id = $registered_controls[ $controls_keys[ $target_section_index ] ]['name'];
'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'],
* Retrieve the key of the control based on a given index of the control.
* @param string $control_index Control index.
* @return int Control key.
final public function get_control_key( $control_index ) {
$registered_controls = $this->get_controls();
$controls_keys = array_keys( $registered_controls );
return $controls_keys[ $control_index ];
* Retrieve the index of the control based on a given key of the control.
* @param string $control_key Control key.
* @return false|int Control index.
final public function get_control_index( $control_key ) {
$controls = $this->get_controls();
$controls_keys = array_keys( $controls );
return array_search( $control_key, $controls_keys );
* Retrieve all controls under a specific section.
* @param string $section_id Section ID.
* @return array Section controls
final public function get_section_controls( $section_id ) {
$section_index = $this->get_control_index( $section_id );
$registered_controls = $this->get_controls();
$controls_keys = array_keys( $registered_controls );
if ( ! isset( $controls_keys[ $section_index ] ) ) {
$control_key = $controls_keys[ $section_index ];
if ( Controls_Manager::SECTION === $registered_controls[ $control_key ]['type'] ) {
$section_controls[ $control_key ] = $registered_controls[ $control_key ];
return $section_controls;
* Add new group control to stack.
* Register a set of related controls grouped together as a single unified
* control. For example grouping together like typography controls into a
* single, easy-to-use control.
* @param string $group_name Group control name.
* @param array $args Group control arguments. Default is an empty array.
* @param array $options Optional. Group control options. Default is an
final public function add_group_control( $group_name, array $args = [], array $options = [] ) {
$group = Plugin::$instance->controls_manager->get_control_groups( $group_name );
wp_die( sprintf( '%s::%s: Group "%s" not found.', get_called_class(), __FUNCTION__, $group_name ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$group->add_controls( $this, $args, $options );
* Retrieve style controls for all active controls or, when requested, from
* a specific set of controls.
* @since 2.0.9 Added the `settings` parameter.
* @param array $controls Optional. Controls list. Default is null.
* @param array $settings Optional. Controls settings. Default is null.
* @return array Style controls.
final public function get_style_controls( ?array $controls = null, ?array $settings = null ) {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.0.0' );
$controls = $this->get_active_controls( $controls, $settings );
foreach ( $controls as $control_name => $control ) {
$control_obj = Plugin::$instance->controls_manager->get_control( $control['type'] );
if ( ! $control_obj instanceof Base_Data_Control ) {
$control = array_merge( $control_obj->get_settings(), $control );
if ( $control_obj instanceof Control_Repeater ) {
foreach ( $this->get_settings( $control_name ) as $item ) {
$style_fields[] = $this->get_style_controls( $control['fields'], $item );
$control['style_fields'] = $style_fields;
if ( ! empty( $control['selectors'] ) || ! empty( $control['dynamic'] ) || ! empty( $control['style_fields'] ) ) {
$style_controls[ $control_name ] = $control;
* Retrieve all the tabs assigned to the control.
* @return array Tabs controls.
final public function get_tabs_controls() {
return $this->get_stack()['tabs'];
* Add new responsive control to stack.
* Register a set of controls to allow editing based on user screen size.
* This method registers one or more controls per screen size/device, depending on the current Responsive Control
* Duplication Mode. There are 3 control duplication modes:
* * 'off' - Only a single control is generated. In the Editor, this control is duplicated in JS.
* * 'on' - Multiple controls are generated, one control per enabled device/breakpoint + a default/desktop control.
* * 'dynamic' - If the control includes the `'dynamic' => 'active' => true` property - the control is duplicated,
* once for each device/breakpoint + default/desktop.
* If the control doesn't include the `'dynamic' => 'active' => true` property - the control is not duplicated.
* @param string $id Responsive control ID.
* @param array $args Responsive control arguments.
* @param array $options Optional. Responsive control options. Default is
final public function add_responsive_control( $id, array $args, $options = [] ) {
$args['responsive'] = [];
$active_breakpoints = Plugin::$instance->breakpoints->get_active_breakpoints();
$devices = Plugin::$instance->breakpoints->get_active_devices_list( [
if ( isset( $args['devices'] ) ) {
$devices = array_intersect( $devices, $args['devices'] );
$args['responsive']['devices'] = $devices;
unset( $args['devices'] );
$control_to_check = $args;
if ( ! empty( $options['overwrite'] ) ) {
$existing_control = Plugin::$instance->controls_manager->get_control_from_stack( $this->get_unique_name(), $id );
if ( ! is_wp_error( $existing_control ) ) {
$control_to_check = $existing_control;
$responsive_duplication_mode = Plugin::$instance->breakpoints->get_responsive_control_duplication_mode();
$additional_breakpoints_active = Plugin::$instance->experiments->is_feature_active( 'additional_custom_breakpoints' );
$control_is_dynamic = ! empty( $control_to_check['dynamic']['active'] );
$is_frontend_available = ! empty( $control_to_check['frontend_available'] );
$has_prefix_class = ! empty( $control_to_check['prefix_class'] );
// If the new responsive controls experiment is active, create only one control - duplicates per device will
// be created in JS in the Editor.
$additional_breakpoints_active
&& ( 'off' === $responsive_duplication_mode || ( 'dynamic' === $responsive_duplication_mode && ! $control_is_dynamic ) )
// Some responsive controls need responsive settings to be available to the widget handler, even when empty.
&& ! $is_frontend_available
$args['is_responsive'] = true;
if ( ! empty( $options['overwrite'] ) ) {
$this->update_control( $id, $args, [
'recursive' => ! empty( $options['recursive'] ),
$this->add_control( $id, $args, $options );
if ( isset( $args['default'] ) ) {
$args['desktop_default'] = $args['default'];
unset( $args['default'] );
foreach ( $devices as $device_name ) {
// Set parent using the name from previous iteration.
if ( isset( $control_name ) ) {
// If $control_name end with _widescreen use desktop name instead.
$control_args['parent'] = '_widescreen' === substr( $control_name, -strlen( '_widescreen' ) ) ? $id : $control_name;
$control_args['parent'] = null;
if ( isset( $control_args['device_args'] ) ) {
if ( ! empty( $control_args['device_args'][ $device_name ] ) ) {
$control_args = array_merge( $control_args, $control_args['device_args'][ $device_name ] );
unset( $control_args['device_args'] );
if ( ! empty( $args['prefix_class'] ) ) {
$device_to_replace = Breakpoints_Manager::BREAKPOINT_KEY_DESKTOP === $device_name ? '' : '-' . $device_name;
$control_args['prefix_class'] = sprintf( $args['prefix_class'], $device_to_replace );
if ( Breakpoints_Manager::BREAKPOINT_KEY_DESKTOP !== $device_name ) {
$direction = $active_breakpoints[ $device_name ]->get_direction();
$control_args['responsive'][ $direction ] = $device_name;
if ( isset( $control_args['min_affected_device'] ) ) {
if ( ! empty( $control_args['min_affected_device'][ $device_name ] ) ) {
$control_args['responsive']['min'] = $control_args['min_affected_device'][ $device_name ];
unset( $control_args['min_affected_device'] );
if ( isset( $control_args[ $device_name . '_default' ] ) ) {
$control_args['default'] = $control_args[ $device_name . '_default' ];
foreach ( $devices as $device ) {
unset( $control_args[ $device . '_default' ] );
$id_suffix = Breakpoints_Manager::BREAKPOINT_KEY_DESKTOP === $device_name ? '' : '_' . $device_name;
$control_name = $id . $id_suffix;
// Set this control as child of previous iteration control.
if ( ! empty( $control_args['parent'] ) ) {
$this->update_control( $control_args['parent'], [ 'inheritors' => [ $control_name ] ] );
if ( ! empty( $options['overwrite'] ) ) {
$this->update_control( $control_name, $control_args, [
'recursive' => ! empty( $options['recursive'] ),
$this->add_control( $control_name, $control_args, $options );
* Update responsive control in stack.
* Change the value of an existing responsive control in the stack. When you