: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$this->manager->add_control( $control );
$new_setting_ids[] = $setting_id;
if ( ! $use_widgets_block_editor ) {
// Add a control for each active widget (located in a sidebar).
foreach ( $sidebar_widget_ids as $i => $widget_id ) {
// Skip widgets that may have gone away due to a plugin being deactivated.
if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
$registered_widget = $wp_registered_widgets[ $widget_id ];
$setting_id = $this->get_setting_id( $widget_id );
$id_base = $wp_registered_widget_controls[ $widget_id ]['id_base'];
$control = new WP_Widget_Form_Customize_Control(
'label' => $registered_widget['name'],
'section' => $section_id,
'sidebar_id' => $sidebar_id,
'widget_id' => $widget_id,
'widget_id_base' => $id_base,
'width' => $wp_registered_widget_controls[ $widget_id ]['width'],
'height' => $wp_registered_widget_controls[ $widget_id ]['height'],
'is_wide' => $this->is_wide_widget( $widget_id ),
$this->manager->add_control( $control );
if ( $this->manager->settings_previewed() ) {
foreach ( $new_setting_ids as $new_setting_id ) {
$this->manager->get_setting( $new_setting_id )->preview();
* Determines whether the widgets panel is active, based on whether there are sidebars registered.
* @see WP_Customize_Panel::$active_callback
* @global array $wp_registered_sidebars
public function is_panel_active() {
global $wp_registered_sidebars;
return ! empty( $wp_registered_sidebars );
* Converts a widget_id into its corresponding Customizer setting ID (option name).
* @param string $widget_id Widget ID.
* @return string Maybe-parsed widget ID.
public function get_setting_id( $widget_id ) {
$parsed_widget_id = $this->parse_widget_id( $widget_id );
$setting_id = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
if ( ! is_null( $parsed_widget_id['number'] ) ) {
$setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
* 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.
* This method will return all Core widgets as being not wide, but this can be
* overridden with the {@see 'is_wide_widget_in_customizer'} filter.
* @global array $wp_registered_widget_controls
* @param string $widget_id Widget ID.
* @return bool Whether or not the widget is a "wide" widget.
public function is_wide_widget( $widget_id ) {
global $wp_registered_widget_controls;
$parsed_widget_id = $this->parse_widget_id( $widget_id );
$width = $wp_registered_widget_controls[ $widget_id ]['width'];
$is_core = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases, true );
$is_wide = ( $width > 250 && ! $is_core );
* Filters whether the given widget is considered "wide".
* @param bool $is_wide Whether the widget is wide, Default false.
* @param string $widget_id Widget ID.
return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
* Converts a widget ID into its id_base and number components.
* @param string $widget_id Widget ID.
* @return array Array containing a widget's id_base and number components.
public function parse_widget_id( $widget_id ) {
if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
$parsed['id_base'] = $matches[1];
$parsed['number'] = (int) $matches[2];
// Likely an old single widget.
$parsed['id_base'] = $widget_id;
* Converts a widget setting ID (option path) to its id_base and number components.
* @param string $setting_id Widget setting ID.
* @return array|WP_Error Array containing a widget's id_base and number components,
public function parse_widget_setting_id( $setting_id ) {
if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
return new WP_Error( 'widget_setting_invalid_id' );
$number = isset( $matches[3] ) ? (int) $matches[3] : null;
return compact( 'id_base', 'number' );
* Calls admin_print_styles-widgets.php and admin_print_styles hooks to
* allow custom styles from plugins.
public function print_styles() {
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_styles-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_styles' );
* Calls admin_print_scripts-widgets.php and admin_print_scripts hooks to
* allow custom scripts from plugins.
public function print_scripts() {
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_scripts-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_print_scripts' );
* Enqueues scripts and styles for Customizer panel and export data to JavaScript.
* @global WP_Scripts $wp_scripts
* @global array $wp_registered_sidebars
* @global array $wp_registered_widgets
public function enqueue_scripts() {
global $wp_scripts, $wp_registered_sidebars, $wp_registered_widgets;
wp_enqueue_style( 'customize-widgets' );
wp_enqueue_script( 'customize-widgets' );
/** This action is documented in wp-admin/admin-header.php */
do_action( 'admin_enqueue_scripts', 'widgets.php' );
* Export available widgets with control_tpl removed from model
* since plugins need templates to be in the DOM.
$available_widgets = array();
foreach ( $this->get_available_widgets() as $available_widget ) {
unset( $available_widget['control_tpl'] );
$available_widgets[] = $available_widget;
$widget_reorder_nav_tpl = sprintf(
'<div class="widget-reorder-nav"><span class="move-widget" tabindex="0">%1$s</span><span class="move-widget-down" tabindex="0">%2$s</span><span class="move-widget-up" tabindex="0">%3$s</span></div>',
__( 'Move to another area…' ),
$move_widget_area_tpl = str_replace(
array( '{description}', '{btn}' ),
__( 'Select an area to move this widget into:' ),
_x( 'Move', 'Move widget' ),
'<div class="move-widget-area">
<p class="description">{description}</p>
<ul class="widget-area-select">
<% _.each( sidebars, function ( sidebar ){ %>
<li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
<div class="move-widget-actions">
<button class="move-widget-btn button" type="button">{btn}</button>
* Gather all strings in PHP that may be needed by JS on the client.
* Once JS i18n is implemented (in #20491), this can be removed.
$some_non_rendered_areas_messages = array();
$some_non_rendered_areas_messages[1] = html_entity_decode(
__( 'Your theme has 1 other widget area, but this particular page does not display it.' ),
get_bloginfo( 'charset' )
$registered_sidebar_count = count( $wp_registered_sidebars );
for ( $non_rendered_count = 2; $non_rendered_count < $registered_sidebar_count; $non_rendered_count++ ) {
$some_non_rendered_areas_messages[ $non_rendered_count ] = html_entity_decode(
/* translators: %s: The number of other widget areas registered but not rendered. */
'Your theme has %s other widget area, but this particular page does not display it.',
'Your theme has %s other widget areas, but this particular page does not display them.',
number_format_i18n( $non_rendered_count )
get_bloginfo( 'charset' )
if ( 1 === $registered_sidebar_count ) {
$no_areas_shown_message = html_entity_decode(
__( 'Your theme has 1 widget area, but this particular page does not display it.' )
get_bloginfo( 'charset' )
$no_areas_shown_message = html_entity_decode(
/* translators: %s: The total number of widget areas registered. */
'Your theme has %s widget area, but this particular page does not display it.',
'Your theme has %s widget areas, but this particular page does not display them.',
$registered_sidebar_count
number_format_i18n( $registered_sidebar_count )
get_bloginfo( 'charset' )
'registeredSidebars' => array_values( $wp_registered_sidebars ),
'registeredWidgets' => $wp_registered_widgets,
'availableWidgets' => $available_widgets, // @todo Merge this with registered_widgets.
'saveBtnLabel' => __( 'Apply' ),
'saveBtnTooltip' => __( 'Save and preview changes before publishing them.' ),
'removeBtnLabel' => __( 'Remove' ),
'removeBtnTooltip' => __( 'Keep widget settings and move it to the inactive widgets' ),
'error' => __( 'An error has occurred. Please reload the page and try again.' ),
'widgetMovedUp' => __( 'Widget moved up' ),
'widgetMovedDown' => __( 'Widget moved down' ),
'navigatePreview' => __( 'You can navigate to other pages on your site while using the Customizer to view and edit the widgets displayed on those pages.' ),
'someAreasShown' => $some_non_rendered_areas_messages,
'noAreasShown' => $no_areas_shown_message,
'reorderModeOn' => __( 'Reorder mode enabled' ),
'reorderModeOff' => __( 'Reorder mode closed' ),
'reorderLabelOn' => esc_attr__( 'Reorder widgets' ),
/* translators: %d: The number of widgets found. */
'widgetsFound' => __( 'Number of widgets found: %d' ),
'noWidgetsFound' => __( 'No widgets found.' ),
'widgetReorderNav' => $widget_reorder_nav_tpl,
'moveWidgetArea' => $move_widget_area_tpl,
'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
unset( $registered_widget['callback'] ); // May not be JSON-serializable.
sprintf( 'var _wpCustomizeWidgetsSettings = %s;', wp_json_encode( $settings ) )
* TODO: Update 'wp-customize-widgets' to not rely so much on things in
* 'customize-widgets'. This will let us skip most of the above and not
* enqueue 'customize-widgets' which saves bytes.
if ( wp_use_widgets_block_editor() ) {
$block_editor_context = new WP_Block_Editor_Context(
'name' => 'core/customize-widgets',
$editor_settings = get_block_editor_settings(
get_legacy_widget_block_editor_settings(),
'wp.domReady( function() {
wp.customizeWidgets.initialize( "widgets-customizer", %s );
wp_json_encode( $editor_settings )
// Preload server-registered block schemas.
'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( get_block_categories( $block_editor_context ) ) ),
wp_enqueue_script( 'wp-customize-widgets' );
wp_enqueue_style( 'wp-customize-widgets' );
/** This action is documented in edit-form-blocks.php */
do_action( 'enqueue_block_editor_assets' );
* Renders the widget form control templates into the DOM.
public function output_widget_control_templates() {
<div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
<div id="available-widgets">
<div class="customize-section-title">
<button class="customize-section-back" tabindex="-1">
<span class="screen-reader-text">
/* translators: Hidden accessibility text. */
<span class="customize-action">
/* translators: ▸ is the unicode right-pointing triangle. %s: Section title in the Customizer. */
printf( __( 'Customizing ▸ %s' ), esc_html( $this->manager->get_panel( 'widgets' )->title ) );
<?php _e( 'Add a Widget' ); ?>
<div id="available-widgets-filter">
<label for="widgets-search">
/* translators: Hidden accessibility text. */
<input type="text" id="widgets-search" aria-describedby="widgets-search-desc" />
<div class="search-icon" aria-hidden="true"></div>
<button type="button" class="clear-results"><span class="screen-reader-text">
/* translators: Hidden accessibility text. */
<p class="screen-reader-text" id="widgets-search-desc">
/* translators: Hidden accessibility text. */
_e( 'The search results will be updated as you type.' );
<div id="available-widgets-list">
<?php foreach ( $this->get_available_widgets() as $available_widget ) : ?>
<div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ); ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ); ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ); ?>" tabindex="0">
<?php echo $available_widget['control_tpl']; ?>
<p class="no-widgets-found-message"><?php _e( 'No widgets found.' ); ?></p>
</div><!-- #available-widgets-list -->
</div><!-- #available-widgets -->
</div><!-- #widgets-left -->
* Calls admin_print_footer_scripts and admin_print_scripts hooks to
* allow custom scripts from plugins.
public function print_footer_scripts() {
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_print_footer_scripts-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_print_footer_scripts' );
/** This action is documented in wp-admin/admin-footer.php */
do_action( 'admin_footer-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
* Retrieves common arguments to supply when constructing a Customizer setting.
* @param string $id Widget setting ID.
* @param array $overrides Array of setting overrides.
* @return array Possibly modified setting arguments.
public function get_setting_args( $id, $overrides = array() ) {
'capability' => 'edit_theme_options',
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 ) ) {
$id_base = $matches['id_base'];
$args['sanitize_callback'] = function ( $value ) use ( $id_base ) {
return $this->sanitize_widget_instance( $value, $id_base );
$args['sanitize_js_callback'] = function ( $value ) use ( $id_base ) {
return $this->sanitize_widget_js_instance( $value, $id_base );
$args['transport'] = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
$args = array_merge( $args, $overrides );
* Filters the common arguments supplied when constructing a Customizer setting.