*/
protected $old_sidebars_widgets = array();
+ /**
+ * Mapping of widget ID base to whether it supports selective refresh.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var array
+ */
+ protected $selective_refreshable_widgets;
+
/**
* Mapping of setting type to setting ID pattern.
*
* @var array
*/
protected $setting_id_patterns = array(
- 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
- 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
+ 'widget_instance' => '/^widget_(?P<id_base>.+?)(?:\[(?P<widget_number>\d+)\])?$/',
+ 'sidebar_widgets' => '/^sidebars_widgets\[(?P<sidebar_id>.+?)\]$/',
);
/**
public function __construct( $manager ) {
$this->manager = $manager;
+ // Skip useless hooks when the user can't manage widgets anyway.
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return;
+ }
+
add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
- add_action( 'after_setup_theme', array( $this, 'register_settings' ) );
+ add_action( 'widgets_init', array( $this, 'register_settings' ), 95 );
add_action( 'wp_loaded', array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) );
add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );
add_action( 'dynamic_sidebar', array( $this, 'tally_rendered_widgets' ) );
add_filter( 'is_active_sidebar', array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
add_filter( 'dynamic_sidebar_has_widgets', array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
+
+ // Selective Refresh.
+ add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
+ add_action( 'customize_preview_init', array( $this, 'selective_refresh_init' ) );
+ }
+
+ /**
+ * List whether each registered widget can be use selective refresh.
+ *
+ * If the theme does not support the customize-selective-refresh-widgets feature,
+ * then this will always return an empty array.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @return array Mapping of id_base to support. If theme doesn't support
+ * selective refresh, an empty array is returned.
+ */
+ public function get_selective_refreshable_widgets() {
+ global $wp_widget_factory;
+ if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
+ return array();
+ }
+ if ( ! isset( $this->selective_refreshable_widgets ) ) {
+ $this->selective_refreshable_widgets = array();
+ foreach ( $wp_widget_factory->widgets as $wp_widget ) {
+ $this->selective_refreshable_widgets[ $wp_widget->id_base ] = ! empty( $wp_widget->widget_options['customize_selective_refresh'] );
+ }
+ }
+ return $this->selective_refreshable_widgets;
}
/**
- * Get the widget setting type given a setting ID.
+ * Determines if a widget supports selective refresh.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param string $id_base Widget ID Base.
+ * @return bool Whether the widget can be selective refreshed.
+ */
+ public function is_widget_selective_refreshable( $id_base ) {
+ $selective_refreshable_widgets = $this->get_selective_refreshable_widgets();
+ return ! empty( $selective_refreshable_widgets[ $id_base ] );
+ }
+
+ /**
+ * Retrieves the widget setting type given a setting ID.
*
* @since 4.2.0
* @access protected
*
* @staticvar array $cache
*
- * @param $setting_id Setting ID.
+ * @param string $setting_id Setting ID.
* @return string|void Setting type.
*/
protected function get_setting_type( $setting_id ) {
}
/**
- * Inspect the incoming customized data for any widget settings, and dynamically add them up-front so widgets will be initialized properly.
+ * Inspects the incoming customized data for any widget settings, and dynamically adds
+ * them up-front so widgets will be initialized properly.
*
* @since 4.2.0
* @access public
}
/**
- * Determine the arguments for a dynamically-created setting.
+ * Determines the arguments for a dynamically-created setting.
*
* @since 4.2.0
* @access public
*
- * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
- * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
+ * @param false|array $args The arguments to the WP_Customize_Setting constructor.
+ * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
* @return false|array Setting arguments, false otherwise.
*/
public function filter_customize_dynamic_setting_args( $args, $setting_id ) {
}
/**
- * Get an unslashed post value or return a default.
+ * Retrieves an unslashed post value or return a default.
*
* @since 3.9.0
- *
* @access protected
*
* @param string $name Post value.
}
/**
- * Filter old_sidebars_widgets_data Customizer setting.
+ * Filters old_sidebars_widgets_data Customizer setting.
*
- * When switching themes, filter the Customizer setting
- * old_sidebars_widgets_data to supply initial $sidebars_widgets before they
- * were overridden by retrieve_widgets(). The value for
- * old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
+ * When switching themes, filter the Customizer setting old_sidebars_widgets_data
+ * to supply initial $sidebars_widgets before they were overridden by retrieve_widgets().
+ * The value for old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
* theme_mod.
*
- * @see WP_Customize_Widgets::handle_theme_switch()
* @since 3.9.0
* @access public
*
+ * @see WP_Customize_Widgets::handle_theme_switch()
+ *
* @param array $old_sidebars_widgets
* @return array
*/
}
/**
- * Filter sidebars_widgets option for theme switch.
+ * Filters sidebars_widgets option for theme switch.
*
- * When switching themes, the retrieve_widgets() function is run when the
- * Customizer initializes, and then the new sidebars_widgets here get
- * supplied as the default value for the sidebars_widgets option.
+ * When switching themes, the retrieve_widgets() function is run when the Customizer initializes,
+ * and then the new sidebars_widgets here get supplied as the default value for the sidebars_widgets
+ * option.
*
- * @see WP_Customize_Widgets::handle_theme_switch()
* @since 3.9.0
* @access public
*
+ * @see WP_Customize_Widgets::handle_theme_switch()
* @global array $sidebars_widgets
*
* @param array $sidebars_widgets
}
/**
- * Make sure all widgets get loaded into the Customizer.
+ * Ensures all widgets get loaded into the Customizer.
*
* Note: these actions are also fired in wp_ajax_update_widget().
*
}
/**
- * Ensure widgets are available for all types of previews.
+ * Ensures widgets are available for all types of previews.
*
- * When in preview, hook to 'customize_register' for settings
- * after WordPress is loaded so that all filters have been
- * initialized (e.g. Widget Visibility).
+ * When in preview, hook to 'customize_register' for settings after WordPress is loaded
+ * so that all filters have been initialized (e.g. Widget Visibility).
*
* @since 3.9.0
* @access public
}
/**
- * Register Customizer settings and controls for all sidebars and widgets.
+ * Registers Customizer settings and controls for all sidebars and widgets.
*
* @since 3.9.0
* @access public
public function customize_register() {
global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
+ add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
+
$sidebars_widgets = array_merge(
array( 'wp_inactive_widgets' => array() ),
array_fill_keys( array_keys( $wp_registered_sidebars ), array() ),
/*
* Add a setting which will be supplied for the theme's sidebars_widgets
- * theme_mod when the the theme is switched.
+ * theme_mod when the theme is switched.
*/
if ( ! $this->manager->is_theme_active() ) {
$setting_id = 'old_sidebars_widgets_data';
}
$this->manager->add_panel( 'widgets', array(
- 'title' => __( 'Widgets' ),
- 'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
- 'priority' => 110,
+ 'type' => 'widgets',
+ 'title' => __( 'Widgets' ),
+ 'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
+ 'priority' => 110,
+ 'active_callback' => array( $this, 'is_panel_active' ),
) );
foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
$sidebar_widget_ids = array();
}
- $is_registered_sidebar = isset( $wp_registered_sidebars[ $sidebar_id ] );
+ $is_registered_sidebar = is_registered_sidebar( $sidebar_id );
$is_inactive_widgets = ( 'wp_inactive_widgets' === $sidebar_id );
$is_active_sidebar = ( $is_registered_sidebar && ! $is_inactive_widgets );
$this->manager->get_setting( $new_setting_id )->preview();
}
}
+ }
- add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
+ /**
+ * Determines whether the widgets panel is active, based on whether there are sidebars registered.
+ *
+ * @since 4.4.0
+ * @access public
+ *
+ * @see WP_Customize_Panel::$active_callback
+ *
+ * @global array $wp_registered_sidebars
+ * @return bool Active.
+ */
+ public function is_panel_active() {
+ global $wp_registered_sidebars;
+ return ! empty( $wp_registered_sidebars );
}
/**
- * Covert a widget_id into its corresponding Customizer setting ID (option name).
+ * Converts a widget_id into its corresponding Customizer setting ID (option name).
*
* @since 3.9.0
* @access public
}
/**
- * Determine whether the widget is considered "wide".
+ * Determines whether the widget is considered "wide".
+ *
+ * Core widgets which may have controls wider than 250, but can still be shown
+ * in the narrow Customizer panel. The RSS and Text widgets in Core, for example,
+ * have widths of 400 and yet they still render fine in the Customizer panel.
*
- * Core widgets which may have controls wider than 250, but can
- * still be shown in the narrow Customizer panel. The RSS and Text
- * widgets in Core, for example, have widths of 400 and yet they
- * still render fine in the Customizer panel. This method will
- * return all Core widgets as being not wide, but this can be
+ * This method will return all Core widgets as being not wide, but this can be
* overridden with the is_wide_widget_in_customizer filter.
*
* @since 3.9.0
}
/**
- * Covert a widget ID into its id_base and number components.
+ * Converts a widget ID into its id_base and number components.
*
* @since 3.9.0
* @access public
}
/**
- * Convert a widget setting ID (option path) to its id_base and number components.
+ * Converts a widget setting ID (option path) to its id_base and number components.
*
* @since 3.9.0
* @access public
}
/**
- * Call admin_print_styles-widgets.php and admin_print_styles hooks to
+ * Calls admin_print_styles-widgets.php and admin_print_styles hooks to
* allow custom styles from plugins.
*
* @since 3.9.0
}
/**
- * Call admin_print_scripts-widgets.php and admin_print_scripts hooks to
+ * Calls admin_print_scripts-widgets.php and admin_print_scripts hooks to
* allow custom scripts from plugins.
*
* @since 3.9.0
}
/**
- * Enqueue scripts and styles for Customizer panel and export data to JavaScript.
+ * Enqueues scripts and styles for Customizer panel and export data to JavaScript.
*
* @since 3.9.0
* @access public
);
$settings = array(
- 'nonce' => wp_create_nonce( 'update-widget' ),
'registeredSidebars' => array_values( $wp_registered_sidebars ),
'registeredWidgets' => $wp_registered_widgets,
'availableWidgets' => $available_widgets, // @todo Merge this with registered_widgets
'error' => __( 'An error has occurred. Please reload the page and try again.' ),
'widgetMovedUp' => __( 'Widget moved up' ),
'widgetMovedDown' => __( 'Widget moved down' ),
+ 'noAreasRendered' => __( 'There are no widget areas currently rendered in the preview. Navigate in the preview to a template that makes use of a widget area in order to access its widgets here.' ),
+ 'reorderModeOn' => __( 'Reorder mode enabled' ),
+ 'reorderModeOff' => __( 'Reorder mode closed' ),
+ 'reorderLabelOn' => esc_attr__( 'Reorder widgets' ),
+ 'reorderLabelOff' => esc_attr__( 'Close reorder mode' ),
),
'tpl' => array(
'widgetReorderNav' => $widget_reorder_nav_tpl,
'moveWidgetArea' => $move_widget_area_tpl,
),
+ 'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
);
foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
}
/**
- * Render the widget form control templates into the DOM.
+ * Renders the widget form control templates into the DOM.
*
* @since 3.9.0
* @access public
}
/**
- * Call admin_print_footer_scripts and admin_print_scripts hooks to
+ * Calls admin_print_footer_scripts and admin_print_scripts hooks to
* allow custom scripts from plugins.
*
* @since 3.9.0
}
/**
- * Get common arguments to supply when constructing a Customizer setting.
+ * Retrieves common arguments to supply when constructing a Customizer setting.
*
* @since 3.9.0
* @access public
$args = array(
'type' => 'option',
'capability' => 'edit_theme_options',
- 'transport' => 'refresh',
'default' => array(),
);
if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
$args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
$args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
+ $args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
} elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
$args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
$args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
+ $args['transport'] = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
}
$args = array_merge( $args, $overrides );
}
/**
- * Make sure that sidebar widget arrays only ever contain widget IDS.
+ * Ensures sidebar widget arrays only ever contain widget IDS.
*
* Used as the 'sanitize_callback' for each $sidebars_widgets setting.
*
}
/**
- * Build up an index of all available widgets for use in Backbone models.
+ * Builds up an index of all available widgets for use in Backbone models.
*
* @since 3.9.0
* @access public
'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
'is_disabled' => $is_disabled,
'id_base' => $id_base,
- 'transport' => 'refresh',
+ 'transport' => $this->is_widget_selective_refreshable( $id_base ) ? 'postMessage' : 'refresh',
'width' => $wp_registered_widget_controls[$widget['id']]['width'],
'height' => $wp_registered_widget_controls[$widget['id']]['height'],
'is_wide' => $this->is_wide_widget( $widget['id'] ),
}
/**
- * Naturally order available widgets by name.
+ * Naturally orders available widgets by name.
*
* @since 3.9.0
* @access protected
}
/**
- * Get the widget control markup.
+ * Retrieves the widget control markup.
*
* @since 3.9.0
* @access public
* @return string Widget control form HTML markup.
*/
public function get_widget_control( $args ) {
+ $args[0]['before_form'] = '<div class="form">';
+ $args[0]['after_form'] = '</div><!-- .form -->';
+ $args[0]['before_widget_content'] = '<div class="widget-content">';
+ $args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
ob_start();
-
call_user_func_array( 'wp_widget_control', $args );
- $replacements = array(
- '<form method="post">' => '<div class="form">',
- '</form>' => '</div><!-- .form -->',
- );
-
$control_tpl = ob_get_clean();
+ return $control_tpl;
+ }
- $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
+ /**
+ * Retrieves the widget control markup parts.
+ *
+ * @since 4.4.0
+ * @access public
+ *
+ * @param array $args Widget control arguments.
+ * @return array {
+ * @type string $control Markup for widget control wrapping form.
+ * @type string $content The contents of the widget form itself.
+ * }
+ */
+ public function get_widget_control_parts( $args ) {
+ $args[0]['before_widget_content'] = '<div class="widget-content">';
+ $args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
+ $control_markup = $this->get_widget_control( $args );
+
+ $content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] );
+ $content_end_pos = strrpos( $control_markup, $args[0]['after_widget_content'] );
+
+ $control = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) );
+ $control .= substr( $control_markup, $content_end_pos );
+ $content = trim( substr(
+ $control_markup,
+ $content_start_pos + strlen( $args[0]['before_widget_content'] ),
+ $content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] )
+ ) );
- return $control_tpl;
+ return compact( 'control', 'content' );
}
/**
- * Add hooks for the Customizer preview.
+ * Adds hooks for the Customizer preview.
*
* @since 3.9.0
* @access public
}
/**
- * Refresh nonce for widget updates.
+ * Refreshes the nonce for widget updates.
*
* @since 4.2.0
* @access public
}
/**
- * When previewing, make sure the proper previewing widgets are used.
+ * When previewing, ensures the proper previewing widgets are used.
*
- * Because wp_get_sidebars_widgets() gets called early at init
- * (via wp_convert_widget_settings()) and can set global variable
- * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' )
- * before the Customizer preview filter is added, we have to reset
- * it after the filter has been added.
+ * Because wp_get_sidebars_widgets() gets called early at {@see 'init' } (via
+ * wp_convert_widget_settings()) and can set global variable `$_wp_sidebars_widgets`
+ * to the value of `get_option( 'sidebars_widgets' )` before the Customizer preview
+ * filter is added, it has to be reset after the filter has been added.
*
* @since 3.9.0
* @access public
}
/**
- * Enqueue scripts for the Customizer preview.
+ * Enqueues scripts for the Customizer preview.
*
* @since 3.9.0
* @access public
*/
public function customize_preview_enqueue() {
wp_enqueue_script( 'customize-preview-widgets' );
+ wp_enqueue_style( 'customize-preview' );
}
/**
- * Insert default style for highlighted widget at early point so theme
+ * Inserts default style for highlighted widget at early point so theme
* stylesheet can override.
*
* @since 3.9.0
* @access public
- *
- * @action wp_print_styles
*/
public function print_preview_css() {
?>
}
/**
- * At the very end of the page, at the very end of the wp_footer,
- * communicate the sidebars that appeared on the page.
+ * Communicates the sidebars that appeared on the page at the very end of the page,
+ * and at the very end of the wp_footer,
*
* @since 3.9.0
* @access public
*/
public function export_preview_data() {
global $wp_registered_sidebars, $wp_registered_widgets;
+
// Prepare Customizer settings to pass to JavaScript.
$settings = array(
'renderedSidebars' => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
'registeredSidebars' => array_values( $wp_registered_sidebars ),
'registeredWidgets' => $wp_registered_widgets,
'l10n' => array(
- 'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
+ 'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
),
+ 'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
);
foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
unset( $registered_widget['callback'] ); // may not be JSON-serializeable
}
/**
- * Keep track of the widgets that were rendered.
+ * Tracks the widgets that were rendered.
*
* @since 3.9.0
* @access public
}
/**
- * Determine if a sidebar is rendered on the page.
+ * Determines if a sidebar is rendered on the page.
*
* @since 4.0.0
* @access public
}
/**
- * Tally the sidebars rendered via is_active_sidebar().
+ * Tallies the sidebars rendered via is_active_sidebar().
*
- * Keep track of the times that is_active_sidebar() is called
- * in the template, and assume that this means that the sidebar
- * would be rendered on the template if there were widgets
- * populating it.
+ * Keep track of the times that is_active_sidebar() is called in the template,
+ * and assume that this means that the sidebar would be rendered on the template
+ * if there were widgets populating it.
*
* @since 3.9.0
* @access public
*
- * @global array $wp_registered_sidebars
- *
* @param bool $is_active Whether the sidebar is active.
* @param string $sidebar_id Sidebar ID.
- * @return bool
+ * @return bool Whether the sidebar is active.
*/
public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
- if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
+ if ( is_registered_sidebar( $sidebar_id ) ) {
$this->rendered_sidebars[] = $sidebar_id;
}
/*
}
/**
- * Tally the sidebars rendered via dynamic_sidebar().
+ * Tallies the sidebars rendered via dynamic_sidebar().
*
* Keep track of the times that dynamic_sidebar() is called in the template,
* and assume this means the sidebar would be rendered on the template if
* @since 3.9.0
* @access public
*
- * @global array $wp_registered_sidebars
- *
* @param bool $has_widgets Whether the current sidebar has widgets.
* @param string $sidebar_id Sidebar ID.
- * @return bool
+ * @return bool Whether the current sidebar has widgets.
*/
public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
- if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
+ if ( is_registered_sidebar( $sidebar_id ) ) {
$this->rendered_sidebars[] = $sidebar_id;
}
}
/**
- * Get MAC for a serialized widget instance string.
+ * Retrieves MAC for a serialized widget instance string.
*
* Allows values posted back from JS to be rejected if any tampering of the
* data has occurred.
}
/**
- * Sanitize a widget instance.
+ * Sanitizes a widget instance.
*
- * Unserialize the JS-instance for storing in the options. It's important
- * that this filter only get applied to an instance once.
+ * Unserialize the JS-instance for storing in the options. It's important that this filter
+ * only get applied to an instance *once*.
*
* @since 3.9.0
* @access public
}
/**
- * Convert widget instance into JSON-representable format.
+ * Converts a widget instance into JSON-representable format.
*
* @since 3.9.0
* @access public
}
/**
- * Strip out widget IDs for widgets which are no longer registered.
+ * Strips out widget IDs for widgets which are no longer registered.
*
* One example where this might happen is when a plugin orphans a widget
* in a sidebar upon deactivation.
}
/**
- * Find and invoke the widget update and control callbacks.
+ * Finds and invokes the widget update and control callbacks.
*
- * Requires that $_POST be populated with the instance data.
+ * Requires that `$_POST` be populated with the instance data.
*
* @since 3.9.0
* @access public
* in place from WP_Customize_Setting::preview() will use this value
* instead of the default widget instance value (an empty array).
*/
- $this->manager->set_post_value( $setting_id, $instance );
+ $this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance ) );
// Obtain the widget control with the updated instance in place.
ob_start();
}
/**
- * Update widget settings asynchronously.
+ * Updates widget settings asynchronously.
*
* Allows the Customizer to update a widget using its form, but return the new
* instance info via Ajax instead of saving it to the options table.
*
- * Most code here copied from wp_ajax_save_widget()
+ * Most code here copied from wp_ajax_save_widget().
*
* @since 3.9.0
* @access public
*
* @see wp_ajax_save_widget()
- *
*/
public function wp_ajax_update_widget() {
wp_send_json_success( compact( 'form', 'instance' ) );
}
- /***************************************************************************
- * Option Update Capturing
- ***************************************************************************/
+ /*
+ * Selective Refresh Methods
+ */
+
+ /**
+ * Filters arguments for dynamic widget partials.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param array|false $partial_args Partial arguments.
+ * @param string $partial_id Partial ID.
+ * @return array (Maybe) modified partial arguments.
+ */
+ public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
+ if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
+ return $partial_args;
+ }
+
+ if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
+ if ( false === $partial_args ) {
+ $partial_args = array();
+ }
+ $partial_args = array_merge(
+ $partial_args,
+ array(
+ 'type' => 'widget',
+ 'render_callback' => array( $this, 'render_widget_partial' ),
+ 'container_inclusive' => true,
+ 'settings' => array( $this->get_setting_id( $matches['widget_id'] ) ),
+ 'capability' => 'edit_theme_options',
+ )
+ );
+ }
+
+ return $partial_args;
+ }
+
+ /**
+ * Adds hooks for selective refresh.
+ *
+ * @since 4.5.0
+ * @access public
+ */
+ public function selective_refresh_init() {
+ if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
+ return;
+ }
+ add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
+ add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
+ add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
+ add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
+ }
+
+ /**
+ * Inject selective refresh data attributes into widget container elements.
+ *
+ * @param array $params {
+ * Dynamic sidebar params.
+ *
+ * @type array $args Sidebar args.
+ * @type array $widget_args Widget args.
+ * }
+ * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args()
+ *
+ * @return array Params.
+ */
+ public function filter_dynamic_sidebar_params( $params ) {
+ $sidebar_args = array_merge(
+ array(
+ 'before_widget' => '',
+ 'after_widget' => '',
+ ),
+ $params[0]
+ );
+
+ // Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
+ $matches = array();
+ $is_valid = (
+ isset( $sidebar_args['id'] )
+ &&
+ is_registered_sidebar( $sidebar_args['id'] )
+ &&
+ ( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
+ &&
+ preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
+ );
+ if ( ! $is_valid ) {
+ return $params;
+ }
+ $this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
+
+ $context = array(
+ 'sidebar_id' => $sidebar_args['id'],
+ );
+ if ( isset( $this->context_sidebar_instance_number ) ) {
+ $context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
+ } else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
+ $context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
+ }
+
+ $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
+ $attributes .= ' data-customize-partial-type="widget"';
+ $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
+ $attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
+ $sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
+
+ $params[0] = $sidebar_args;
+ return $params;
+ }
+
+ /**
+ * List of the tag names seen for before_widget strings.
+ *
+ * This is used in the {@see 'filter_wp_kses_allowed_html'} filter to ensure that the
+ * data-* attributes can be whitelisted.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var array
+ */
+ protected $before_widget_tags_seen = array();
+
+ /**
+ * Ensures the HTML data-* attributes for selective refresh are allowed by kses.
+ *
+ * This is needed in case the `$before_widget` is run through wp_kses() when printed.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param array $allowed_html Allowed HTML.
+ * @return array (Maybe) modified allowed HTML.
+ */
+ public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
+ foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
+ if ( ! isset( $allowed_html[ $tag_name ] ) ) {
+ $allowed_html[ $tag_name ] = array();
+ }
+ $allowed_html[ $tag_name ] = array_merge(
+ $allowed_html[ $tag_name ],
+ array_fill_keys( array(
+ 'data-customize-partial-id',
+ 'data-customize-partial-type',
+ 'data-customize-partial-placement-context',
+ 'data-customize-partial-widget-id',
+ 'data-customize-partial-options',
+ ), true )
+ );
+ }
+ return $allowed_html;
+ }
+
+ /**
+ * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
+ *
+ * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var array
+ */
+ protected $sidebar_instance_count = array();
+
+ /**
+ * The current request's sidebar_instance_number context.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var int
+ */
+ protected $context_sidebar_instance_number;
+
+ /**
+ * Current sidebar ID being rendered.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var array
+ */
+ protected $current_dynamic_sidebar_id_stack = array();
+
+ /**
+ * Begins keeping track of the current sidebar being rendered.
+ *
+ * Insert marker before widgets are rendered in a dynamic sidebar.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param int|string $index Index, name, or ID of the dynamic sidebar.
+ */
+ public function start_dynamic_sidebar( $index ) {
+ array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
+ if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
+ $this->sidebar_instance_count[ $index ] = 0;
+ }
+ $this->sidebar_instance_count[ $index ] += 1;
+ if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
+ printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
+ }
+ }
+
+ /**
+ * Finishes keeping track of the current sidebar being rendered.
+ *
+ * Inserts a marker after widgets are rendered in a dynamic sidebar.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param int|string $index Index, name, or ID of the dynamic sidebar.
+ */
+ public function end_dynamic_sidebar( $index ) {
+ array_shift( $this->current_dynamic_sidebar_id_stack );
+ if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
+ printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
+ }
+ }
+
+ /**
+ * Current sidebar being rendered.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var string
+ */
+ protected $rendering_widget_id;
+
+ /**
+ * Current widget being rendered.
+ *
+ * @since 4.5.0
+ * @access protected
+ * @var string
+ */
+ protected $rendering_sidebar_id;
+
+ /**
+ * Filters sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
+ *
+ * @since 4.5.0
+ * @access protected
+ *
+ * @param array $sidebars_widgets Sidebars widgets.
+ * @return array Filtered sidebars widgets.
+ */
+ public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
+ $sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
+ return $sidebars_widgets;
+ }
+
+ /**
+ * Renders a specific widget using the supplied sidebar arguments.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @see dynamic_sidebar()
+ *
+ * @param WP_Customize_Partial $partial Partial.
+ * @param array $context {
+ * Sidebar args supplied as container context.
+ *
+ * @type string $sidebar_id ID for sidebar for widget to render into.
+ * @type int $sidebar_instance_number Disambiguating instance number.
+ * }
+ * @return string|false
+ */
+ public function render_widget_partial( $partial, $context ) {
+ $id_data = $partial->id_data();
+ $widget_id = array_shift( $id_data['keys'] );
+
+ if ( ! is_array( $context )
+ || empty( $context['sidebar_id'] )
+ || ! is_registered_sidebar( $context['sidebar_id'] )
+ ) {
+ return false;
+ }
+
+ $this->rendering_sidebar_id = $context['sidebar_id'];
+
+ if ( isset( $context['sidebar_instance_number'] ) ) {
+ $this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] );
+ }
+
+ // Filter sidebars_widgets so that only the queried widget is in the sidebar.
+ $this->rendering_widget_id = $widget_id;
+
+ $filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
+ add_filter( 'sidebars_widgets', $filter_callback, 1000 );
+
+ // Render the widget.
+ ob_start();
+ dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] );
+ $container = ob_get_clean();
+
+ // Reset variables for next partial render.
+ remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
+
+ $this->context_sidebar_instance_number = null;
+ $this->rendering_sidebar_id = null;
+ $this->rendering_widget_id = null;
+
+ return $container;
+ }
+
+ //
+ // Option Update Capturing
+ //
/**
* List of captured widget option updates.
protected $_is_capturing_option_updates = false;
/**
- * Determine whether the captured option update should be ignored.
+ * Determines whether the captured option update should be ignored.
*
* @since 3.9.0
* @access protected
}
/**
- * Retrieve captured widget option updates.
+ * Retrieves captured widget option updates.
*
* @since 3.9.0
* @access protected
}
/**
- * Get the option that was captured from being saved.
+ * Retrieves the option that was captured from being saved.
*
* @since 4.2.0
* @access protected
*
* @param string $option_name Option name.
- * @param mixed $default Optional. Default value to return if the option does not exist.
+ * @param mixed $default Optional. Default value to return if the option does not exist. Default false.
* @return mixed Value set for the option.
*/
protected function get_captured_option( $option_name, $default = false ) {
}
/**
- * Get the number of captured widget option updates.
+ * Retrieves the number of captured widget option updates.
*
* @since 3.9.0
* @access protected
}
/**
- * Start keeping track of changes to widget options, caching new values.
+ * Begins keeping track of changes to widget options, caching new values.
*
* @since 3.9.0
* @access protected
}
/**
- * Pre-filter captured option values before updating.
+ * Pre-filters captured option values before updating.
*
* @since 3.9.0
* @access public
}
/**
- * Pre-filter captured option values before retrieving.
+ * Pre-filters captured option values before retrieving.
*
* @since 3.9.0
* @access public
}
/**
- * Undo any changes to the options since options capture began.
+ * Undoes any changes to the options since options capture began.
*
* @since 3.9.0
* @access protected
return;
}
- remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
+ remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
foreach ( array_keys( $this->_captured_options ) as $option_name ) {
remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );