wp/wp-includes/class-wp-customize-widgets.php
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
--- a/wp/wp-includes/class-wp-customize-widgets.php	Tue Jun 09 11:14:17 2015 +0000
+++ b/wp/wp-includes/class-wp-customize-widgets.php	Mon Oct 14 17:39:30 2019 +0200
@@ -22,7 +22,6 @@
 	 * WP_Customize_Manager instance.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 * @var WP_Customize_Manager
 	 */
 	public $manager;
@@ -31,64 +30,87 @@
 	 * All id_bases for widgets defined in core.
 	 *
 	 * @since 3.9.0
-	 * @access protected
 	 * @var array
 	 */
 	protected $core_widget_id_bases = array(
-		'archives', 'calendar', 'categories', 'links', 'meta',
-		'nav_menu', 'pages', 'recent-comments', 'recent-posts',
-		'rss', 'search', 'tag_cloud', 'text',
+		'archives',
+		'calendar',
+		'categories',
+		'custom_html',
+		'links',
+		'media_audio',
+		'media_image',
+		'media_video',
+		'meta',
+		'nav_menu',
+		'pages',
+		'recent-comments',
+		'recent-posts',
+		'rss',
+		'search',
+		'tag_cloud',
+		'text',
 	);
 
 	/**
 	 * @since 3.9.0
-	 * @access protected
 	 * @var array
 	 */
 	protected $rendered_sidebars = array();
 
 	/**
 	 * @since 3.9.0
-	 * @access protected
 	 * @var array
 	 */
 	protected $rendered_widgets = array();
 
 	/**
 	 * @since 3.9.0
-	 * @access protected
 	 * @var array
 	 */
 	protected $old_sidebars_widgets = array();
 
 	/**
+	 * Mapping of widget ID base to whether it supports selective refresh.
+	 *
+	 * @since 4.5.0
+	 * @var array
+	 */
+	protected $selective_refreshable_widgets;
+
+	/**
 	 * Mapping of setting type to setting ID pattern.
 	 *
 	 * @since 4.2.0
-	 * @access protected
 	 * @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>.+?)\]$/',
 	);
 
 	/**
 	 * Initial loader.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param WP_Customize_Manager $manager Customize manager bootstrap instance.
 	 */
 	public function __construct( $manager ) {
 		$this->manager = $manager;
 
+		// See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L420-L449
 		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( 'customize_register',                      array( $this, 'schedule_customize_register' ), 1 );
+
+		// Skip remaining hooks when the user can't manage widgets anyway.
+		if ( ! current_user_can( 'edit_theme_options' ) ) {
+			return;
+		}
+
 		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( 'customize_controls_enqueue_scripts',      array( $this, 'enqueue_scripts' ) );
 		add_action( 'customize_controls_print_styles',         array( $this, 'print_styles' ) );
 		add_action( 'customize_controls_print_scripts',        array( $this, 'print_scripts' ) );
@@ -100,16 +122,61 @@
 		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' ) );
 	}
 
 	/**
-	 * Get the widget setting type given a setting ID.
+	 * 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
+	 *
+	 * @global WP_Widget_Factory $wp_widget_factory
+	 *
+	 * @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;
+	}
+
+	/**
+	 * Determines if a widget supports selective refresh.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @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.
-	 * @return string|null Setting type. Null otherwise.
+	 * @param string $setting_id Setting ID.
+	 * @return string|void Setting type.
 	 */
 	protected function get_setting_type( $setting_id ) {
 		static $cache = array();
@@ -122,14 +189,13 @@
 				return $type;
 			}
 		}
-		return null;
 	}
 
 	/**
-	 * 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
 	 */
 	public function register_settings() {
 		$widget_setting_ids = array();
@@ -145,12 +211,7 @@
 
 		$settings = $this->manager->add_dynamic_settings( array_unique( $widget_setting_ids ) );
 
-		/*
-		 * Preview settings right away so that widgets and sidebars will get registered properly.
-		 * But don't do this if a customize_save because this will cause WP to think there is nothing
-		 * changed that needs to be saved.
-		 */
-		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
+		if ( $this->manager->settings_previewed() ) {
 			foreach ( $settings as $setting ) {
 				$setting->preview();
 			}
@@ -158,13 +219,12 @@
 	}
 
 	/**
-	 * 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 ) {
@@ -175,12 +235,10 @@
 	}
 
 	/**
-	 * 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.
 	 * @param mixed  $default Default post value.
 	 * @return mixed Unslashed post value or default value.
@@ -203,7 +261,9 @@
 	 * theme gets switched.
 	 *
 	 * @since 3.9.0
-	 * @access public
+	 *
+	 * @global array $sidebars_widgets
+	 * @global array $_wp_sidebars_widgets
 	 */
 	public function override_sidebars_widgets_for_theme_switch() {
 		global $sidebars_widgets;
@@ -214,45 +274,49 @@
 
 		$this->old_sidebars_widgets = wp_get_sidebars_widgets();
 		add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
+		$this->manager->set_post_value( 'old_sidebars_widgets_data', $this->old_sidebars_widgets ); // Override any value cached in changeset.
 
 		// retrieve_widgets() looks at the global $sidebars_widgets
 		$sidebars_widgets = $this->old_sidebars_widgets;
 		$sidebars_widgets = retrieve_widgets( 'customize' );
 		add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
-		unset( $GLOBALS['_wp_sidebars_widgets'] ); // reset global cache var used by wp_get_sidebars_widgets()
+		// reset global cache var used by wp_get_sidebars_widgets()
+		unset( $GLOBALS['_wp_sidebars_widgets'] );
 	}
 
 	/**
-	 * 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.
 	 *
+	 * @since 3.9.0
+	 *
 	 * @see WP_Customize_Widgets::handle_theme_switch()
-	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param array $old_sidebars_widgets
+	 * @return array
 	 */
 	public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
 		return $this->old_sidebars_widgets;
 	}
 
 	/**
-	 * 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.
+	 *
+	 * @since 3.9.0
 	 *
 	 * @see WP_Customize_Widgets::handle_theme_switch()
-	 * @since 3.9.0
-	 * @access public
+	 * @global array $sidebars_widgets
 	 *
 	 * @param array $sidebars_widgets
+	 * @return array
 	 */
 	public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
 		$sidebars_widgets = $GLOBALS['sidebars_widgets'];
@@ -261,12 +325,11 @@
 	}
 
 	/**
-	 * 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().
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 */
 	public function customize_controls_init() {
 		/** This action is documented in wp-admin/includes/ajax-actions.php */
@@ -280,14 +343,12 @@
 	}
 
 	/**
-	 * 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 {@see '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
 	 */
 	public function schedule_customize_register() {
 		if ( is_admin() ) {
@@ -298,17 +359,22 @@
 	}
 
 	/**
-	 * 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
+	 *
+	 * @global array $wp_registered_widgets
+	 * @global array $wp_registered_widget_controls
+	 * @global array $wp_registered_sidebars
 	 */
 	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( $GLOBALS['wp_registered_sidebars'] ), array() ),
+			array_fill_keys( array_keys( $wp_registered_sidebars ), array() ),
 			wp_get_sidebars_widgets()
 		);
 
@@ -330,7 +396,7 @@
 
 		/*
 		 * 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';
@@ -342,9 +408,12 @@
 		}
 
 		$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' ),
+			'auto_expand_sole_section' => true,
 		) );
 
 		foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
@@ -352,7 +421,7 @@
 				$sidebar_widget_ids = array();
 			}
 
-			$is_registered_sidebar = isset( $GLOBALS['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 );
 
@@ -373,15 +442,15 @@
 				if ( $is_active_sidebar ) {
 
 					$section_args = array(
-						'title' => $GLOBALS['wp_registered_sidebars'][ $sidebar_id ]['name'],
-						'description' => $GLOBALS['wp_registered_sidebars'][ $sidebar_id ]['description'],
+						'title' => $wp_registered_sidebars[ $sidebar_id ]['name'],
+						'description' => $wp_registered_sidebars[ $sidebar_id ]['description'],
 						'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
 						'panel' => 'widgets',
 						'sidebar_id' => $sidebar_id,
 					);
 
 					/**
-					 * Filter Customizer widget section arguments for a given sidebar.
+					 * Filters Customizer widget section arguments for a given sidebar.
 					 *
 					 * @since 3.9.0
 					 *
@@ -409,13 +478,13 @@
 			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( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
+				if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) {
 					continue;
 				}
 
-				$registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
+				$registered_widget = $wp_registered_widgets[$widget_id];
 				$setting_id        = $this->get_setting_id( $widget_id );
-				$id_base           = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
+				$id_base           = $wp_registered_widget_controls[$widget_id]['id_base'];
 
 				$control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
 					'label'          => $registered_widget['name'],
@@ -432,20 +501,32 @@
 			}
 		}
 
-		if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
+		if ( $this->manager->settings_previewed() ) {
 			foreach ( $new_setting_ids as $new_setting_id ) {
 				$this->manager->get_setting( $new_setting_id )->preview();
 			}
 		}
-
-		add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
 	}
 
 	/**
-	 * Covert a widget_id into its corresponding Customizer setting ID (option name).
+	 * Determines whether the widgets panel is active, based on whether there are sidebars registered.
+	 *
+	 * @since 4.4.0
+	 *
+	 * @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 );
+	}
+
+	/**
+	 * Converts a widget_id into its corresponding Customizer setting ID (option name).
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param string $widget_id Widget ID.
 	 * @return string Maybe-parsed widget ID.
@@ -461,17 +542,18 @@
 	}
 
 	/**
-	 * 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. This method will
-	 * return all Core widgets as being not wide, but this can be
-	 * overridden with the is_wide_widget_in_customizer filter.
+	 * 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.
 	 *
 	 * @since 3.9.0
-	 * @access public
+	 *
+	 * @global $wp_registered_widget_controls
 	 *
 	 * @param string $widget_id Widget ID.
 	 * @return bool Whether or not the widget is a "wide" widget.
@@ -485,7 +567,7 @@
 		$is_wide          = ( $width > 250 && ! $is_core );
 
 		/**
-		 * Filter whether the given widget is considered "wide".
+		 * Filters whether the given widget is considered "wide".
 		 *
 		 * @since 3.9.0
 		 *
@@ -496,10 +578,9 @@
 	}
 
 	/**
-	 * 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
 	 *
 	 * @param string $widget_id Widget ID.
 	 * @return array Array containing a widget's id_base and number components.
@@ -521,10 +602,9 @@
 	}
 
 	/**
-	 * 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
 	 *
 	 * @param string $setting_id Widget setting ID.
 	 * @return WP_Error|array Array containing a widget's id_base and number components,
@@ -542,11 +622,10 @@
 	}
 
 	/**
-	 * 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
-	 * @access public
 	 */
 	public function print_styles() {
 		/** This action is documented in wp-admin/admin-header.php */
@@ -557,11 +636,10 @@
 	}
 
 	/**
-	 * 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
-	 * @access public
 	 */
 	public function print_scripts() {
 		/** This action is documented in wp-admin/admin-header.php */
@@ -572,12 +650,17 @@
 	}
 
 	/**
-	 * 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
+	 *
+	 * @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' );
 
@@ -616,17 +699,53 @@
 					<% }); %>
 				</ul>
 				<div class="move-widget-actions">
-					<button class="move-widget-btn button-secondary" type="button">{btn}</button>
+					<button class="move-widget-btn button" type="button">{btn}</button>
 				</div>
 			</div>'
 		);
 
-		global $wp_scripts;
+		/*
+		 * 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 doesn&#8217;t display it.' ),
+			ENT_QUOTES,
+			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( sprintf(
+				/* translators: %s: the number of other widget areas registered but not rendered */
+				_n(
+					'Your theme has %s other widget area, but this particular page doesn&#8217;t display it.',
+					'Your theme has %s other widget areas, but this particular page doesn&#8217;t display them.',
+					$non_rendered_count
+				),
+				number_format_i18n( $non_rendered_count )
+			), ENT_QUOTES, get_bloginfo( 'charset' ) );
+		}
+
+		if ( 1 === $registered_sidebar_count ) {
+			$no_areas_shown_message = html_entity_decode( sprintf(
+				__( 'Your theme has 1 widget area, but this particular page doesn&#8217;t display it.' )
+			), ENT_QUOTES, get_bloginfo( 'charset' ) );
+		} else {
+			$no_areas_shown_message = html_entity_decode( sprintf(
+				/* translators: %s: the total number of widget areas registered */
+				_n(
+					'Your theme has %s widget area, but this particular page doesn&#8217;t display it.',
+					'Your theme has %s widget areas, but this particular page doesn&#8217;t display them.',
+					$registered_sidebar_count
+				),
+				number_format_i18n( $registered_sidebar_count )
+			), ENT_QUOTES, get_bloginfo( 'charset' ) );
+		}
 
 		$settings = array(
-			'nonce'                => wp_create_nonce( 'update-widget' ),
-			'registeredSidebars'   => array_values( $GLOBALS['wp_registered_sidebars'] ),
-			'registeredWidgets'    => $GLOBALS['wp_registered_widgets'],
+			'registeredSidebars'   => array_values( $wp_registered_sidebars ),
+			'registeredWidgets'    => $wp_registered_widgets,
 			'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
 			'l10n' => array(
 				'saveBtnLabel'     => __( 'Apply' ),
@@ -636,11 +755,21 @@
 				'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.' ),
 			),
 			'tpl' => array(
 				'widgetReorderNav' => $widget_reorder_nav_tpl,
 				'moveWidgetArea'   => $move_widget_area_tpl,
 			),
+			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
 		);
 
 		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
@@ -655,18 +784,32 @@
 	}
 
 	/**
-	 * Render the widget form control templates into the DOM.
+	 * Renders the widget form control templates into the DOM.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 */
 	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"><?php _e( 'Back' ); ?></span>
+				</button>
+				<h3>
+					<span class="customize-action"><?php
+						/* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
+						echo sprintf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'widgets' )->title ) );
+					?></span>
+					<?php _e( 'Add a Widget' ); ?>
+				</h3>
+			</div>
 			<div id="available-widgets-filter">
 				<label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
-				<input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" />
+				<input type="text" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" aria-describedby="widgets-search-desc" />
+				<div class="search-icon" aria-hidden="true"></div>
+				<button type="button" class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></button>
+				<p class="screen-reader-text" id="widgets-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
 			</div>
 			<div id="available-widgets-list">
 			<?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
@@ -674,6 +817,7 @@
 					<?php echo $available_widget['control_tpl']; ?>
 				</div>
 			<?php endforeach; ?>
+			<p class="no-widgets-found-message"><?php _e( 'No widgets found.' ); ?></p>
 			</div><!-- #available-widgets-list -->
 		</div><!-- #available-widgets -->
 		</div><!-- #widgets-left -->
@@ -681,14 +825,16 @@
 	}
 
 	/**
-	 * 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
-	 * @access public
 	 */
 	public function print_footer_scripts() {
 		/** This action is documented in wp-admin/admin-footer.php */
+		do_action( 'admin_print_footer_scripts-widgets.php' );
+
+		/** 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 */
@@ -696,10 +842,9 @@
 	}
 
 	/**
-	 * 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
 	 *
 	 * @param string $id        Widget setting ID.
 	 * @param array  $overrides Array of setting overrides.
@@ -709,22 +854,23 @@
 		$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' );
-		} else if ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
+			$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 );
 
 		/**
-		 * Filter the common arguments supplied when constructing a Customizer setting.
+		 * Filters the common arguments supplied when constructing a Customizer setting.
 		 *
 		 * @since 3.9.0
 		 *
@@ -737,12 +883,11 @@
 	}
 
 	/**
-	 * 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.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param array $widget_ids Array of widget IDs.
 	 * @return array Array of sanitized widget IDs.
@@ -757,10 +902,13 @@
 	}
 
 	/**
-	 * 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
+	 *
+	 * @global array $wp_registered_widgets
+	 * @global array $wp_registered_widget_controls
+	 * @staticvar array $available_widgets
 	 *
 	 * @see wp_list_widgets()
 	 *
@@ -827,7 +975,7 @@
 				'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'] ),
@@ -840,11 +988,9 @@
 	}
 
 	/**
-	 * Naturally order available widgets by name.
+	 * Naturally orders available widgets by name.
 	 *
 	 * @since 3.9.0
-	 * @static
-	 * @access protected
 	 *
 	 * @param array $widget_a The first widget to compare.
 	 * @param array $widget_b The second widget to compare.
@@ -855,35 +1001,58 @@
 	}
 
 	/**
-	 * Get the widget control markup.
+	 * Retrieves the widget control markup.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param array $args Widget control arguments.
 	 * @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();
-
-		$control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
-
 		return $control_tpl;
 	}
 
 	/**
-	 * Add hooks for the Customizer preview.
+	 * Retrieves the widget control markup parts.
+	 *
+	 * @since 4.4.0
+	 *
+	 * @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 compact( 'control', 'content' );
+	}
+
+	/**
+	 * Adds hooks for the Customizer preview.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 */
 	public function customize_preview_init() {
 		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
@@ -892,10 +1061,9 @@
 	}
 
 	/**
-	 * Refresh nonce for widget updates.
+	 * Refreshes the nonce for widget updates.
 	 *
 	 * @since 4.2.0
-	 * @access public
 	 *
 	 * @param  array $nonces Array of nonces.
 	 * @return array $nonces Array of nonces.
@@ -906,44 +1074,39 @@
 	}
 
 	/**
-	 * 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
 	 *
 	 * @param array $sidebars_widgets List of widgets for the current sidebar.
+	 * @return array
 	 */
 	public function preview_sidebars_widgets( $sidebars_widgets ) {
-		$sidebars_widgets = get_option( 'sidebars_widgets' );
+		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
 
 		unset( $sidebars_widgets['array_version'] );
 		return $sidebars_widgets;
 	}
 
 	/**
-	 * 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' );
 	}
 
 	/**
-	 * 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() {
 		?>
@@ -960,23 +1123,33 @@
 	}
 
 	/**
-	 * 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
+     *
+	 * @global array $wp_registered_sidebars
+	 * @global array $wp_registered_widgets
 	 */
 	public function export_preview_data() {
+		global $wp_registered_sidebars, $wp_registered_widgets;
+
+		$switched_locale = switch_to_locale( get_user_locale() );
+		$l10n = array(
+			'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
+		);
+		if ( $switched_locale ) {
+			restore_previous_locale();
+		}
 
 		// Prepare Customizer settings to pass to JavaScript.
 		$settings = array(
 			'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
 			'renderedWidgets'    => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
-			'registeredSidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
-			'registeredWidgets'  => $GLOBALS['wp_registered_widgets'],
-			'l10n'               => array(
-				'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
-			),
+			'registeredSidebars' => array_values( $wp_registered_sidebars ),
+			'registeredWidgets'  => $wp_registered_widgets,
+			'l10n'               => $l10n,
+			'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
 		);
 		foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
 			unset( $registered_widget['callback'] ); // may not be JSON-serializeable
@@ -990,10 +1163,9 @@
 	}
 
 	/**
-	 * Keep track of the widgets that were rendered.
+	 * Tracks the widgets that were rendered.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param array $widget Rendered widget to tally.
 	 */
@@ -1005,7 +1177,6 @@
 	 * Determine if a widget is rendered on the page.
 	 *
 	 * @since 4.0.0
-	 * @access public
 	 *
 	 * @param string $widget_id Widget ID to check.
 	 * @return bool Whether the widget is rendered.
@@ -1015,10 +1186,9 @@
 	}
 
 	/**
-	 * Determine if a sidebar is rendered on the page.
+	 * Determines if a sidebar is rendered on the page.
 	 *
 	 * @since 4.0.0
-	 * @access public
 	 *
 	 * @param string $sidebar_id Sidebar ID to check.
 	 * @return bool Whether the sidebar is rendered.
@@ -1028,21 +1198,20 @@
 	}
 
 	/**
-	 * 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
 	 *
 	 * @param bool   $is_active  Whether the sidebar is active.
 	 * @param string $sidebar_id Sidebar ID.
+	 * @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;
 		}
 		/*
@@ -1054,20 +1223,20 @@
 	}
 
 	/**
-	 * 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
 	 * there were widgets populating it.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param bool   $has_widgets Whether the current sidebar has widgets.
 	 * @param string $sidebar_id  Sidebar ID.
+	 * @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;
 		}
 
@@ -1080,13 +1249,12 @@
 	}
 
 	/**
-	 * 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.
 	 *
 	 * @since 3.9.0
-	 * @access protected
 	 *
 	 * @param string $serialized_instance Widget instance.
 	 * @return string MAC for serialized widget instance.
@@ -1096,16 +1264,15 @@
 	}
 
 	/**
-	 * 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
 	 *
 	 * @param array $value Widget instance to sanitize.
-	 * @return array Sanitized widget instance.
+	 * @return array|void Sanitized widget instance.
 	 */
 	public function sanitize_widget_instance( $value ) {
 		if ( $value === array() ) {
@@ -1116,31 +1283,30 @@
 			|| empty( $value['instance_hash_key'] )
 			|| empty( $value['encoded_serialized_instance'] ) )
 		{
-			return null;
+			return;
 		}
 
 		$decoded = base64_decode( $value['encoded_serialized_instance'], true );
 		if ( false === $decoded ) {
-			return null;
+			return;
 		}
 
-		if ( $this->get_instance_hash_key( $decoded ) !== $value['instance_hash_key'] ) {
-			return null;
+		if ( ! hash_equals( $this->get_instance_hash_key( $decoded ), $value['instance_hash_key'] ) ) {
+			return;
 		}
 
 		$instance = unserialize( $decoded );
 		if ( false === $instance ) {
-			return null;
+			return;
 		}
 
 		return $instance;
 	}
 
 	/**
-	 * Convert widget instance into JSON-representable format.
+	 * Converts a widget instance into JSON-representable format.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param array $value Widget instance to convert to JSON.
 	 * @return array JSON-converted widget instance.
@@ -1160,13 +1326,14 @@
 	}
 
 	/**
-	 * 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.
 	 *
 	 * @since 3.9.0
-	 * @access public
+	 *
+	 * @global array $wp_registered_widgets
 	 *
 	 * @param array $widget_ids List of widget IDs.
 	 * @return array Parsed list of widget IDs.
@@ -1178,12 +1345,14 @@
 	}
 
 	/**
-	 * 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
+	 *
+	 * @global array $wp_registered_widget_updates
+	 * @global array $wp_registered_widget_controls
 	 *
 	 * @param  string $widget_id Widget ID.
 	 * @return WP_Error|array Array containing the updated widget information.
@@ -1192,6 +1361,20 @@
 	public function call_widget_update( $widget_id ) {
 		global $wp_registered_widget_updates, $wp_registered_widget_controls;
 
+		$setting_id = $this->get_setting_id( $widget_id );
+
+		/*
+		 * Make sure that other setting changes have previewed since this widget
+		 * may depend on them (e.g. Menus being present for Navigation Menu widget).
+		 */
+		if ( ! did_action( 'customize_preview_init' ) ) {
+			foreach ( $this->manager->settings() as $setting ) {
+				if ( $setting->id !== $setting_id ) {
+					$setting->preview();
+				}
+			}
+		}
+
 		$this->start_capturing_option_updates();
 		$parsed_id   = $this->parse_widget_id( $widget_id );
 		$option_name = 'widget_' . $parsed_id['id_base'];
@@ -1272,8 +1455,7 @@
 		 * in place from WP_Customize_Setting::preview() will use this value
 		 * instead of the default widget instance value (an empty array).
 		 */
-		$setting_id = $this->get_setting_id( $widget_id );
-		$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();
@@ -1289,18 +1471,16 @@
 	}
 
 	/**
-	 * 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() {
 
@@ -1344,7 +1524,7 @@
 
 		$updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
 		if ( is_wp_error( $updated_widget ) ) {
-			wp_send_json_error( $updated_widget->get_error_message() );
+			wp_send_json_error( $updated_widget->get_error_code() );
 		}
 
 		$form = $updated_widget['form'];
@@ -1353,15 +1533,308 @@
 		wp_send_json_success( compact( 'form', 'instance' ) );
 	}
 
-	/***************************************************************************
-	 * Option Update Capturing
-	 ***************************************************************************/
+	/*
+	 * Selective Refresh Methods
+	 */
+
+	/**
+	 * Filters arguments for dynamic widget partials.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @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
+	 */
+	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
+	 * @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
+	 *
+	 * @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
+	 * @var array
+	 */
+	protected $sidebar_instance_count = array();
+
+	/**
+	 * The current request's sidebar_instance_number context.
+	 *
+	 * @since 4.5.0
+	 * @var int
+	 */
+	protected $context_sidebar_instance_number;
+
+	/**
+	 * Current sidebar ID being rendered.
+	 *
+	 * @since 4.5.0
+	 * @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
+	 *
+	 * @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
+	 *
+	 * @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
+	 * @var string
+	 */
+	protected $rendering_widget_id;
+
+	/**
+	 * Current widget being rendered.
+	 *
+	 * @since 4.5.0
+	 * @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
+	 *
+	 * @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
+	 *
+	 * @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.
 	 *
 	 * @since 3.9.0
-	 * @access protected
 	 * @var array $_captured_options Values updated while option capture is happening.
 	 */
 	protected $_captured_options = array();
@@ -1370,29 +1843,26 @@
 	 * Whether option capture is currently happening.
 	 *
 	 * @since 3.9.0
-	 * @access protected
 	 * @var bool $_is_current Whether option capture is currently happening or not.
 	 */
 	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
 	 *
 	 * @param string $option_name Option name.
-	 * @return boolean Whether the option capture is ignored.
+	 * @return bool Whether the option capture is ignored.
 	 */
 	protected function is_option_capture_ignored( $option_name ) {
 		return ( 0 === strpos( $option_name, '_transient_' ) );
 	}
 
 	/**
-	 * Retrieve captured widget option updates.
+	 * Retrieves captured widget option updates.
 	 *
 	 * @since 3.9.0
-	 * @access protected
 	 *
 	 * @return array Array of captured options.
 	 */
@@ -1401,13 +1871,12 @@
 	}
 
 	/**
-	 * 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 ) {
@@ -1420,10 +1889,9 @@
 	}
 
 	/**
-	 * Get the number of captured widget option updates.
+	 * Retrieves the number of captured widget option updates.
 	 *
 	 * @since 3.9.0
-	 * @access protected
 	 *
 	 * @return int Number of updated options.
 	 */
@@ -1432,10 +1900,9 @@
 	}
 
 	/**
-	 * 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
 	 */
 	protected function start_capturing_option_updates() {
 		if ( $this->_is_capturing_option_updates ) {
@@ -1448,10 +1915,9 @@
 	}
 
 	/**
-	 * Pre-filter captured option values before updating.
+	 * Pre-filters captured option values before updating.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param mixed  $new_value   The new option value.
 	 * @param string $option_name Name of the option.
@@ -1473,10 +1939,9 @@
 	}
 
 	/**
-	 * Pre-filter captured option values before retrieving.
+	 * Pre-filters captured option values before retrieving.
 	 *
 	 * @since 3.9.0
-	 * @access public
 	 *
 	 * @param mixed $value Value to return instead of the option value.
 	 * @return mixed Filtered option value.
@@ -1495,17 +1960,16 @@
 	}
 
 	/**
-	 * 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
 	 */
 	protected function stop_capturing_option_updates() {
 		if ( ! $this->_is_capturing_option_updates ) {
 			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' ) );
@@ -1516,34 +1980,50 @@
 	}
 
 	/**
+	 * {@internal Missing Summary}
+	 *
+	 * See the {@see 'customize_dynamic_setting_args'} filter.
+	 *
 	 * @since 3.9.0
-	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
 	 */
 	public function setup_widget_addition_previews() {
-		_deprecated_function( __METHOD__, '4.2.0' );
+		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
 	}
 
 	/**
+	 * {@internal Missing Summary}
+	 *
+	 * See the {@see 'customize_dynamic_setting_args'} filter.
+	 *
 	 * @since 3.9.0
-	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
 	 */
 	public function prepreview_added_sidebars_widgets() {
-		_deprecated_function( __METHOD__, '4.2.0' );
+		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
 	}
 
 	/**
+	 * {@internal Missing Summary}
+	 *
+	 * See the {@see 'customize_dynamic_setting_args'} filter.
+	 *
 	 * @since 3.9.0
-	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
 	 */
 	public function prepreview_added_widget_instance() {
-		_deprecated_function( __METHOD__, '4.2.0' );
+		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
 	}
 
 	/**
+	 * {@internal Missing Summary}
+	 *
+	 * See the {@see 'customize_dynamic_setting_args'} filter.
+	 *
 	 * @since 3.9.0
-	 * @deprecated 4.2.0 Deprecated in favor of customize_dynamic_setting_args filter.
+	 * @deprecated 4.2.0 Deprecated in favor of the {@see 'customize_dynamic_setting_args'} filter.
 	 */
 	public function remove_prepreview_filters() {
-		_deprecated_function( __METHOD__, '4.2.0' );
+		_deprecated_function( __METHOD__, '4.2.0', 'customize_dynamic_setting_args' );
 	}
 }