Make WordPress Core

Ticket #35855: 35855.1.diff

File 35855.1.diff, 60.0 KB (added by westonruter, 9 years ago)

Refresh patch

  • src/wp-admin/js/customize-widgets.js

    diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
    index 91a6516..ded96ef 100644
     
    3434                multi_number: null,
    3535                name: null,
    3636                id_base: null,
    37                 transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
     37                transport: null,
    3838                params: [],
    3939                width: null,
    4040                height: null,
     
    19821982                        isExistingWidget = api.has( settingId );
    19831983                        if ( ! isExistingWidget ) {
    19841984                                settingArgs = {
    1985                                         transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
     1985                                        transport: api.Widgets.data.selectiveRefreshWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
    19861986                                        previewer: this.setting.previewer
    19871987                                };
    19881988                                setting = api.create( settingId, settingId, '', settingArgs );
  • src/wp-content/themes/twentyeleven/functions.php

    diff --git src/wp-content/themes/twentyeleven/functions.php src/wp-content/themes/twentyeleven/functions.php
    index 3ae1756..bd5fef4 100644
    function twentyeleven_setup() { 
    226226                        'description' => __( 'Hanoi Plant', 'twentyeleven' )
    227227                )
    228228        ) );
     229
     230        // Indicate widget sidebars can use selective refresh in the Customizer.
     231        add_theme_support( 'customize-selective-refresh-widgets' );
    229232}
    230233endif; // twentyeleven_setup
    231234
  • src/wp-content/themes/twentyfifteen/functions.php

    diff --git src/wp-content/themes/twentyfifteen/functions.php src/wp-content/themes/twentyfifteen/functions.php
    index e109a2b..dafdf43 100644
    function twentyfifteen_setup() { 
    117117         * specifically font, colors, icons, and column width.
    118118         */
    119119        add_editor_style( array( 'css/editor-style.css', 'genericons/genericons.css', twentyfifteen_fonts_url() ) );
     120
     121        // Indicate widget sidebars can use selective refresh in the Customizer.
     122        add_theme_support( 'customize-selective-refresh-widgets' );
    120123}
    121124endif; // twentyfifteen_setup
    122125add_action( 'after_setup_theme', 'twentyfifteen_setup' );
  • src/wp-content/themes/twentyfourteen/functions.php

    diff --git src/wp-content/themes/twentyfourteen/functions.php src/wp-content/themes/twentyfourteen/functions.php
    index 4e65214..9b7cf0b 100644
    function twentyfourteen_setup() { 
    113113
    114114        // This theme uses its own gallery styles.
    115115        add_filter( 'use_default_gallery_style', '__return_false' );
     116
     117        // Indicate widget sidebars can use selective refresh in the Customizer.
     118        add_theme_support( 'customize-selective-refresh-widgets' );
    116119}
    117120endif; // twentyfourteen_setup
    118121add_action( 'after_setup_theme', 'twentyfourteen_setup' );
  • src/wp-content/themes/twentythirteen/functions.php

    diff --git src/wp-content/themes/twentythirteen/functions.php src/wp-content/themes/twentythirteen/functions.php
    index 33c8efd..e1f27ad 100644
    function twentythirteen_setup() { 
    105105
    106106        // This theme uses its own gallery styles.
    107107        add_filter( 'use_default_gallery_style', '__return_false' );
     108
     109        // Indicate widget sidebars can use selective refresh in the Customizer.
     110        add_theme_support( 'customize-selective-refresh-widgets' );
    108111}
    109112add_action( 'after_setup_theme', 'twentythirteen_setup' );
    110113
  • src/wp-content/themes/twentytwelve/functions.php

    diff --git src/wp-content/themes/twentytwelve/functions.php src/wp-content/themes/twentytwelve/functions.php
    index e305447..8ec0755 100644
    function twentytwelve_setup() { 
    7474        // This theme uses a custom image size for featured images, displayed on "standard" posts.
    7575        add_theme_support( 'post-thumbnails' );
    7676        set_post_thumbnail_size( 624, 9999 ); // Unlimited height, soft crop
     77
     78        // Indicate widget sidebars can use selective refresh in the Customizer.
     79        add_theme_support( 'customize-selective-refresh-widgets' );
    7780}
    7881add_action( 'after_setup_theme', 'twentytwelve_setup' );
    7982
  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index 3699b21..3aaeb1c 100644
    final class WP_Customize_Manager { 
    109109         * @access protected
    110110         * @var array
    111111         */
    112         protected $components = array( 'widgets', 'nav_menus', 'selective_refresh' );
     112        protected $components = array( 'widgets', 'nav_menus' );
    113113
    114114        /**
    115115         * Registered instances of WP_Customize_Section.
    final class WP_Customize_Manager { 
    258258                 */
    259259                $components = apply_filters( 'customize_loaded_components', $this->components, $this );
    260260
     261                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
     262                $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
     263
    261264                if ( in_array( 'widgets', $components, true ) ) {
    262265                        require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
    263266                        $this->widgets = new WP_Customize_Widgets( $this );
    final class WP_Customize_Manager { 
    268271                        $this->nav_menus = new WP_Customize_Nav_Menus( $this );
    269272                }
    270273
    271                 if ( in_array( 'selective_refresh', $components, true ) ) {
    272                         require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
    273                         $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
    274                 }
    275 
    276274                add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
    277275
    278276                add_action( 'setup_theme', array( $this, 'setup_theme' ) );
    final class WP_Customize_Manager { 
    17301728                        'autofocus' => $this->get_autofocus(),
    17311729                        'documentTitleTmpl' => $this->get_document_title_template(),
    17321730                        'previewableDevices' => $this->get_previewable_devices(),
    1733                         'selectiveRefreshEnabled' => isset( $this->selective_refresh ),
    17341731                );
    17351732
    17361733                // Prepare Customize Section objects to pass to JavaScript.
    final class WP_Customize_Manager { 
    19791976                        ),
    19801977                ) ) );
    19811978
    1982                 if ( isset( $this->selective_refresh ) ) {
    1983                         $this->selective_refresh->add_partial( 'custom_logo', array(
    1984                                 'settings'            => array( 'custom_logo' ),
    1985                                 'selector'            => '.custom-logo-link',
    1986                                 'render_callback'     => array( $this, '_render_custom_logo_partial' ),
    1987                                 'container_inclusive' => true,
    1988                         ) );
    1989                 }
     1979                $this->selective_refresh->add_partial( 'site_logo', array(
     1980                        'settings'            => array( 'site_logo' ),
     1981                        'selector'            => '.site-logo-link',
     1982                        'render_callback'     => array( $this, '_render_site_logo_partial' ),
     1983                        'container_inclusive' => true,
     1984                ) );
    19901985
    19911986                /* Colors */
    19921987
  • src/wp-includes/class-wp-customize-nav-menus.php

    diff --git src/wp-includes/class-wp-customize-nav-menus.php src/wp-includes/class-wp-customize-nav-menus.php
    index cb8994c..7784567 100644
    final class WP_Customize_Nav_Menus { 
    393393                                'reorderLabelOn'    => esc_attr__( 'Reorder menu items' ),
    394394                                'reorderLabelOff'   => esc_attr__( 'Close reorder mode' ),
    395395                        ),
    396                         'settingTransport'     => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     396                        'settingTransport'     => 'postMessage',
    397397                        'phpIntMax'            => PHP_INT_MAX,
    398398                        'defaultSettingValues' => array(
    399399                                'nav_menu'      => $temp_nav_menu_setting->default,
    final class WP_Customize_Nav_Menus { 
    445445                if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
    446446                        $setting_args = array(
    447447                                'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
    448                                 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     448                                'transport' => 'postMessage',
    449449                        );
    450450                } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
    451451                        $setting_args = array(
    452452                                'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
    453                                 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     453                                'transport' => 'postMessage',
    454454                        );
    455455                }
    456456                return $setting_args;
    final class WP_Customize_Nav_Menus { 
    535535
    536536                        $setting = $this->manager->get_setting( $setting_id );
    537537                        if ( $setting ) {
    538                                 $setting->transport = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh';
     538                                $setting->transport = 'postMessage';
    539539                                remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
    540540                                add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
    541541                        } else {
    final class WP_Customize_Nav_Menus { 
    543543                                        'sanitize_callback' => array( $this, 'intval_base10' ),
    544544                                        'theme_supports'    => 'menus',
    545545                                        'type'              => 'theme_mod',
    546                                         'transport'         => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     546                                        'transport'         => 'postMessage',
    547547                                        'default'           => 0,
    548548                                ) );
    549549                        }
    final class WP_Customize_Nav_Menus { 
    570570
    571571                        $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
    572572                        $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array(
    573                                 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     573                                'transport' => 'postMessage',
    574574                        ) ) );
    575575
    576576                        // Add the menu contents.
    final class WP_Customize_Nav_Menus { 
    585585                                $value['nav_menu_term_id'] = $menu_id;
    586586                                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
    587587                                        'value'     => $value,
    588                                         'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     588                                        'transport' => 'postMessage',
    589589                                ) ) );
    590590
    591591                                // Create a control for each menu item.
    final class WP_Customize_Nav_Menus { 
    988988         * @access public
    989989         */
    990990        public function customize_preview_enqueue_deps() {
    991                 if ( isset( $this->manager->selective_refresh ) ) {
    992                         $script = wp_scripts()->registered['customize-preview-nav-menus'];
    993                         $script->deps[] = 'customize-selective-refresh';
    994                 }
    995 
    996991                wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
    997992                wp_enqueue_style( 'customize-preview' );
    998993        }
  • src/wp-includes/class-wp-customize-widgets.php

    diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
    index a417c46..eeccce0 100644
    final class WP_Customize_Widgets { 
    6969         * @var array
    7070         */
    7171        protected $setting_id_patterns = array(
    72                 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
    73                 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
     72                'widget_instance' => '/^widget_(?P<id_base>.+?)(?:\[(?P<widget_number>\d+)\])?$/',
     73                'sidebar_widgets' => '/^sidebars_widgets\[(?P<sidebar_id>.+?)\]$/',
    7474        );
    7575
    7676        /**
    final class WP_Customize_Widgets { 
    109109                // Selective Refresh.
    110110                add_filter( 'customize_dynamic_partial_args',          array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
    111111                add_action( 'customize_preview_init',                  array( $this, 'selective_refresh_init' ) );
     112
     113                add_filter( 'current_theme_supports-customize-selective-refresh-widgets', array( $this, 'filter_current_theme_supports' ), 1, 3 );
     114        }
     115
     116        /**
     117         * Filters theme support for customize-selective-refresh-widgets to whitelist widget support.
     118         *
     119         * @since 4.5.0
     120         * @access public
     121         *
     122         * @param bool  $theme_supports Whether the current theme supports selective refresh of widgets.
     123         * @param array $args           Array of arguments for the feature.
     124         * @param array $widgets        The widgets supported by the feature.
     125         * @return bool Whether the theme or widget can use selective refresh in the Customizer.
     126         */
     127        public function filter_current_theme_supports( $theme_supports, $args, $widgets ) {
     128                global $wp_widget_factory;
     129                if ( $theme_supports && isset( $args[0] ) ) {
     130                        $widget_id_base = $args[0];
     131
     132                        if ( is_array( $widgets ) && in_array( $widget_id_base, $widgets, true ) ) {
     133                                $theme_supports = true;
     134                        } else {
     135                                foreach ( $wp_widget_factory->widgets as $widget ) {
     136                                        if ( $widget_id_base === $widget->id_base ) {
     137                                                $theme_supports = ! empty( $widget->widget_options['customize_selective_refresh'] );
     138                                                break;
     139                                        }
     140                                }
     141                        }
     142                }
     143                return $theme_supports;
     144        }
     145
     146        /**
     147         * Gets whether each registered widget can be use selective refresh.
     148         *
     149         * @since 4.5.0
     150         * @access private
     151         *
     152         * @return array Mapping of id_base to support. If theme doesn't support
     153         *               selective refresh, an empty array is returned.
     154         */
     155        protected function get_selective_refreshable_widgets() {
     156                global $wp_widget_factory;
     157
     158                $selective_refreshable_widgets = array();
     159                if ( current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
     160                        foreach ( $wp_widget_factory->widgets as $wp_widget ) {
     161                                $selective_refreshable_widgets[ $wp_widget->id_base ] = current_theme_supports( 'customize-selective-refresh-widgets', $wp_widget->id_base );
     162                        }
     163                }
     164                return $selective_refreshable_widgets;
    112165        }
    113166
    114167        /**
    final class WP_Customize_Widgets { 
    119172         *
    120173         * @staticvar array $cache
    121174         *
    122          * @param $setting_id Setting ID.
     175         * @param string $setting_id Setting ID.
    123176         * @return string|void Setting type.
    124177         */
    125178        protected function get_setting_type( $setting_id ) {
    final class WP_Customize_Widgets { 
    690743                                'widgetReorderNav' => $widget_reorder_nav_tpl,
    691744                                'moveWidgetArea'   => $move_widget_area_tpl,
    692745                        ),
    693                         'selectiveRefresh'     => isset( $this->manager->selective_refresh ),
     746                        'selectiveRefreshWidgets' => $this->get_selective_refreshable_widgets(),
    694747                );
    695748
    696749                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    final class WP_Customize_Widgets { 
    771824                $args = array(
    772825                        'type'       => 'option',
    773826                        'capability' => 'edit_theme_options',
    774                         'transport'  => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    775827                        'default'    => array(),
    776828                );
    777829
    778830                if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
    779831                        $args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
    780832                        $args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
     833                        $args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
    781834                } elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
    782835                        $args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
    783836                        $args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
     837                        $args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets', $matches['id_base'] ) ? 'postMessage' : 'refresh';
    784838                }
    785839
    786840                $args = array_merge( $args, $overrides );
    final class WP_Customize_Widgets { 
    893947                                'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
    894948                                'is_disabled'  => $is_disabled,
    895949                                'id_base'      => $id_base,
    896                                 'transport'    => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     950                                'transport'    => current_theme_supports( 'customize-selective-refresh-widgets', $id_base ) ? 'postMessage' : 'refresh',
    897951                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
    898952                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
    899953                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
    final class WP_Customize_Widgets { 
    10601114         */
    10611115        public function export_preview_data() {
    10621116                global $wp_registered_sidebars, $wp_registered_widgets;
     1117
    10631118                // Prepare Customizer settings to pass to JavaScript.
    10641119                $settings = array(
    10651120                        'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
    final class WP_Customize_Widgets { 
    10691124                        'l10n'               => array(
    10701125                                'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
    10711126                        ),
    1072                         'selectiveRefresh'   => isset( $this->manager->selective_refresh ),
     1127                        'selectiveRefreshWidgets' => $this->get_selective_refreshable_widgets(),
    10731128                );
    10741129                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    10751130                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
    final class WP_Customize_Widgets { 
    14791534         * @return array (Maybe) modified partial arguments.
    14801535         */
    14811536        public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
     1537                if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
     1538                        return $partial_args;
     1539                }
    14821540
    14831541                if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
    14841542                        if ( false === $partial_args ) {
    final class WP_Customize_Widgets { 
    15061564         * @access public
    15071565         */
    15081566        public function selective_refresh_init() {
    1509                 if ( ! isset( $this->manager->selective_refresh ) ) {
     1567                if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
    15101568                        return;
    15111569                }
    15121570
    final class WP_Customize_Widgets { 
    15241582         * @access public
    15251583         */
    15261584        public function customize_preview_enqueue_deps() {
    1527                 if ( isset( $this->manager->selective_refresh ) ) {
    1528                         $script = wp_scripts()->registered['customize-preview-widgets'];
    1529                         $script->deps[] = 'customize-selective-refresh';
    1530                 }
    1531 
    15321585                wp_enqueue_script( 'customize-preview-widgets' );
    15331586                wp_enqueue_style( 'customize-preview' );
    15341587        }
  • src/wp-includes/class-wp-widget.php

    diff --git src/wp-includes/class-wp-widget.php src/wp-includes/class-wp-widget.php
    index e8f4a1e..bb0de50 100644
    class WP_Widget { 
    155155                $this->id_base = empty($id_base) ? preg_replace( '/(wp_)?widget_/', '', strtolower(get_class($this)) ) : strtolower($id_base);
    156156                $this->name = $name;
    157157                $this->option_name = 'widget_' . $this->id_base;
    158                 $this->widget_options = wp_parse_args( $widget_options, array('classname' => $this->option_name) );
    159                 $this->control_options = wp_parse_args( $control_options, array('id_base' => $this->id_base) );
     158                $this->widget_options = wp_parse_args( $widget_options, array( 'classname' => $this->option_name, 'customize_selective_refresh' => false ) );
     159                $this->control_options = wp_parse_args( $control_options, array( 'id_base' => $this->id_base ) );
    160160        }
    161161
    162162        /**
  • src/wp-includes/js/customize-preview-widgets.js

    diff --git src/wp-includes/js/customize-preview-widgets.js src/wp-includes/js/customize-preview-widgets.js
    index 92e7732..3ce1b1a 100644
    wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( 
    1212                preview: null,
    1313                l10n: {
    1414                        widgetTooltip: ''
    15                 }
     15                },
     16                selectiveRefreshWidgets: {}
    1617        };
    1718
    1819        /**
    wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( 
    2425                var self = this;
    2526
    2627                self.preview = api.preview;
    27                 if ( api.selectiveRefresh ) {
     28                if ( ! _.isEmpty( self.selectiveRefreshWidgets ) ) {
    2829                        self.addPartials();
    2930                }
    3031
    wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( 
    3839                } );
    3940        };
    4041
    41         if ( api.selectiveRefresh ) {
     42        /**
     43         * Partial representing a widget instance.
     44         *
     45         * @class
     46         * @augments wp.customize.selectiveRefresh.Partial
     47         * @since 4.5.0
     48         */
     49        self.WidgetPartial = api.selectiveRefresh.Partial.extend({
    4250
    4351                /**
    44                  * Partial representing a widget instance.
     52                 * Constructor.
    4553                 *
    46                  * @class
    47                  * @augments wp.customize.selectiveRefresh.Partial
    4854                 * @since 4.5.0
     55                 * @param {string} id - Partial ID.
     56                 * @param {Object} options
     57                 * @param {Object} options.params
    4958                 */
    50                 self.WidgetPartial = api.selectiveRefresh.Partial.extend({
    51 
    52                         /**
    53                          * Constructor.
    54                          *
    55                          * @since 4.5.0
    56                          * @param {string} id - Partial ID.
    57                          * @param {Object} options
    58                          * @param {Object} options.params
    59                          */
    60                         initialize: function( id, options ) {
    61                                 var partial = this, matches;
    62                                 matches = id.match( /^widget\[(.+)]$/ );
    63                                 if ( ! matches ) {
    64                                         throw new Error( 'Illegal id for widget partial.' );
    65                                 }
     59                initialize: function( id, options ) {
     60                        var partial = this, matches;
     61                        matches = id.match( /^widget\[(.+)]$/ );
     62                        if ( ! matches ) {
     63                                throw new Error( 'Illegal id for widget partial.' );
     64                        }
    6665
    67                                 partial.widgetId = matches[1];
    68                                 options = options || {};
    69                                 options.params = _.extend(
    70                                         {
    71                                                 /* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */
    72                                                 selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]'
    73                                                 settings: [ self.getWidgetSettingId( partial.widgetId ) ],
    74                                                 containerInclusive: true
    75                                         },
    76                                         options.params || {}
    77                                 );
     66                        partial.widgetId = matches[1];
     67                        options = options || {};
     68                        options.params = _.extend(
     69                                {
     70                                        /* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */
     71                                        selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]'
     72                                        settings: [ self.getWidgetSettingId( partial.widgetId ) ],
     73                                        containerInclusive: true
     74                                },
     75                                options.params || {}
     76                        );
     77
     78                        api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     79                },
    7880
    79                                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
    80                         },
    81 
    82                         /**
    83                          * Send widget-updated message to parent so spinner will get removed from widget control.
    84                          *
    85                          * @inheritdoc
    86                          * @param {wp.customize.selectiveRefresh.Placement} placement
    87                          */
    88                         renderContent: function( placement ) {
    89                                 var partial = this;
    90                                 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
    91                                         api.preview.send( 'widget-updated', partial.widgetId );
    92                                         api.selectiveRefresh.trigger( 'widget-updated', partial );
    93                                 }
     81                /**
     82                 * Send widget-updated message to parent so spinner will get removed from widget control.
     83                 *
     84                 * @inheritdoc
     85                 * @param {wp.customize.selectiveRefresh.Placement} placement
     86                 */
     87                renderContent: function( placement ) {
     88                        var partial = this;
     89                        if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
     90                                api.preview.send( 'widget-updated', partial.widgetId );
     91                                api.selectiveRefresh.trigger( 'widget-updated', partial );
    9492                        }
    95                 });
     93                }
     94        });
     95
     96        /**
     97         * Partial representing a widget area.
     98         *
     99         * @class
     100         * @augments wp.customize.selectiveRefresh.Partial
     101         * @since 4.5.0
     102         */
     103        self.SidebarPartial = api.selectiveRefresh.Partial.extend({
    96104
    97105                /**
    98                  * Partial representing a widget area.
     106                 * Constructor.
    99107                 *
    100                  * @class
    101                  * @augments wp.customize.selectiveRefresh.Partial
    102108                 * @since 4.5.0
     109                 * @param {string} id - Partial ID.
     110                 * @param {Object} options
     111                 * @param {Object} options.params
    103112                 */
    104                 self.SidebarPartial = api.selectiveRefresh.Partial.extend({
    105 
    106                         /**
    107                          * Constructor.
    108                          *
    109                          * @since 4.5.0
    110                          * @param {string} id - Partial ID.
    111                          * @param {Object} options
    112                          * @param {Object} options.params
    113                          */
    114                         initialize: function( id, options ) {
    115                                 var partial = this, matches;
    116                                 matches = id.match( /^sidebar\[(.+)]$/ );
    117                                 if ( ! matches ) {
    118                                         throw new Error( 'Illegal id for sidebar partial.' );
    119                                 }
    120                                 partial.sidebarId = matches[1];
    121 
    122                                 options = options || {};
    123                                 options.params = _.extend(
    124                                         {
    125                                                 settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
    126                                         },
    127                                         options.params || {}
    128                                 );
     113                initialize: function( id, options ) {
     114                        var partial = this, matches;
     115                        matches = id.match( /^sidebar\[(.+)]$/ );
     116                        if ( ! matches ) {
     117                                throw new Error( 'Illegal id for sidebar partial.' );
     118                        }
     119                        partial.sidebarId = matches[1];
    129120
    130                                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     121                        options = options || {};
     122                        options.params = _.extend(
     123                                {
     124                                        settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
     125                                },
     126                                options.params || {}
     127                        );
    131128
    132                                 if ( ! partial.params.sidebarArgs ) {
    133                                         throw new Error( 'The sidebarArgs param was not provided.' );
    134                                 }
    135                                 if ( partial.params.settings.length > 1 ) {
    136                                         throw new Error( 'Expected SidebarPartial to only have one associated setting' );
    137                                 }
    138                         },
    139 
    140                         /**
    141                          * Set up the partial.
    142                          *
    143                          * @since 4.5.0
    144                          */
    145                         ready: function() {
    146                                 var sidebarPartial = this;
    147 
    148                                 // Watch for changes to the sidebar_widgets setting.
    149                                 _.each( sidebarPartial.settings(), function( settingId ) {
    150                                         api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
    151                                 } );
     129                        api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
    152130
    153                                 // Trigger an event for this sidebar being updated whenever a widget inside is rendered.
    154                                 api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
    155                                         var isAssignedWidgetPartial = (
    156                                                 placement.partial.extended( self.WidgetPartial ) &&
    157                                                 ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
    158                                         );
    159                                         if ( isAssignedWidgetPartial ) {
    160                                                 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
    161                                         }
    162                                 } );
     131                        if ( ! partial.params.sidebarArgs ) {
     132                                throw new Error( 'The sidebarArgs param was not provided.' );
     133                        }
     134                        if ( partial.params.settings.length > 1 ) {
     135                                throw new Error( 'Expected SidebarPartial to only have one associated setting' );
     136                        }
     137                },
    163138
    164                                 // Make sure that a widget partial has a container in the DOM prior to a refresh.
    165                                 api.bind( 'change', function( widgetSetting ) {
    166                                         var widgetId, parsedId;
    167                                         parsedId = self.parseWidgetSettingId( widgetSetting.id );
    168                                         if ( ! parsedId ) {
    169                                                 return;
    170                                         }
    171                                         widgetId = parsedId.idBase;
    172                                         if ( parsedId.number ) {
    173                                                 widgetId += '-' + String( parsedId.number );
    174                                         }
    175                                         if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
    176                                                 sidebarPartial.ensureWidgetPlacementContainers( widgetId );
    177                                         }
    178                                 } );
    179                         },
    180 
    181                         /**
    182                          * Get the before/after boundary nodes for all instances of this sidebar (usually one).
    183                          *
    184                          * Note that TreeWalker is not implemented in IE8.
    185                          *
    186                          * @since 4.5.0
    187                          * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
    188                          */
    189                         findDynamicSidebarBoundaryNodes: function() {
    190                                 var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
    191                                 regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
    192                                 recursiveCommentTraversal = function( childNodes ) {
    193                                         _.each( childNodes, function( node ) {
    194                                                 var matches;
    195                                                 if ( 8 === node.nodeType ) {
    196                                                         matches = node.nodeValue.match( regExp );
    197                                                         if ( ! matches || matches[2] !== partial.sidebarId ) {
    198                                                                 return;
    199                                                         }
    200                                                         if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
    201                                                                 boundaryNodes[ matches[3] ] = {
    202                                                                         before: null,
    203                                                                         after: null,
    204                                                                         instanceNumber: parseInt( matches[3], 10 )
    205                                                                 };
    206                                                         }
    207                                                         if ( 'dynamic_sidebar_before' === matches[1] ) {
    208                                                                 boundaryNodes[ matches[3] ].before = node;
    209                                                         } else {
    210                                                                 boundaryNodes[ matches[3] ].after = node;
    211                                                         }
    212                                                 } else if ( 1 === node.nodeType ) {
    213                                                         recursiveCommentTraversal( node.childNodes );
    214                                                 }
    215                                         } );
    216                                 };
    217 
    218                                 recursiveCommentTraversal( document.body.childNodes );
    219                                 return _.values( boundaryNodes );
    220                         },
    221 
    222                         /**
    223                          * Get the placements for this partial.
    224                          *
    225                          * @since 4.5.0
    226                          * @returns {Array}
    227                          */
    228                         placements: function() {
    229                                 var partial = this;
    230                                 return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
    231                                         return new api.selectiveRefresh.Placement( {
    232                                                 partial: partial,
    233                                                 container: null,
    234                                                 startNode: boundaryNodes.before,
    235                                                 endNode: boundaryNodes.after,
    236                                                 context: {
    237                                                         instanceNumber: boundaryNodes.instanceNumber
    238                                                 }
    239                                         } );
    240                                 } );
    241                         },
    242 
    243                         /**
    244                          * Get the list of widget IDs associated with this widget area.
    245                          *
    246                          * @since 4.5.0
    247                          *
    248                          * @returns {Array}
    249                          */
    250                         getWidgetIds: function() {
    251                                 var sidebarPartial = this, settingId, widgetIds;
    252                                 settingId = sidebarPartial.settings()[0];
    253                                 if ( ! settingId ) {
    254                                         throw new Error( 'Missing associated setting.' );
     139                /**
     140                 * Set up the partial.
     141                 *
     142                 * @since 4.5.0
     143                 */
     144                ready: function() {
     145                        var sidebarPartial = this;
     146
     147                        // Watch for changes to the sidebar_widgets setting.
     148                        _.each( sidebarPartial.settings(), function( settingId ) {
     149                                api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
     150                        } );
     151
     152                        // Trigger an event for this sidebar being updated whenever a widget inside is rendered.
     153                        api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     154                                var isAssignedWidgetPartial = (
     155                                        placement.partial.extended( self.WidgetPartial ) &&
     156                                        ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
     157                                );
     158                                if ( isAssignedWidgetPartial ) {
     159                                        api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     160                                }
     161                        } );
     162
     163                        // Make sure that a widget partial has a container in the DOM prior to a refresh.
     164                        api.bind( 'change', function( widgetSetting ) {
     165                                var widgetId, parsedId;
     166                                parsedId = self.parseWidgetSettingId( widgetSetting.id );
     167                                if ( ! parsedId ) {
     168                                        return;
    255169                                }
    256                                 if ( ! api.has( settingId ) ) {
    257                                         throw new Error( 'Setting does not exist.' );
     170                                widgetId = parsedId.idBase;
     171                                if ( parsedId.number ) {
     172                                        widgetId += '-' + String( parsedId.number );
    258173                                }
    259                                 widgetIds = api( settingId ).get();
    260                                 if ( ! _.isArray( widgetIds ) ) {
    261                                         throw new Error( 'Expected setting to be array of widget IDs' );
     174                                if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
     175                                        sidebarPartial.ensureWidgetPlacementContainers( widgetId );
    262176                                }
    263                                 return widgetIds.slice( 0 );
    264                         },
    265 
    266                         /**
    267                          * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
    268                          *
    269                          * @since 4.5.0
    270                          *
    271                          * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
    272                          */
    273                         reflowWidgets: function() {
    274                                 var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
    275                                 widgetIds = sidebarPartial.getWidgetIds();
    276                                 sidebarPlacements = sidebarPartial.placements();
    277 
    278                                 widgetPartials = {};
    279                                 _.each( widgetIds, function( widgetId ) {
    280                                         var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
    281                                         if ( widgetPartial ) {
    282                                                 widgetPartials[ widgetId ] = widgetPartial;
     177                        } );
     178                },
     179
     180                /**
     181                 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
     182                 *
     183                 * Note that TreeWalker is not implemented in IE8.
     184                 *
     185                 * @since 4.5.0
     186                 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
     187                 */
     188                findDynamicSidebarBoundaryNodes: function() {
     189                        var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
     190                        regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
     191                        recursiveCommentTraversal = function( childNodes ) {
     192                                _.each( childNodes, function( node ) {
     193                                        var matches;
     194                                        if ( 8 === node.nodeType ) {
     195                                                matches = node.nodeValue.match( regExp );
     196                                                if ( ! matches || matches[2] !== partial.sidebarId ) {
     197                                                        return;
     198                                                }
     199                                                if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
     200                                                        boundaryNodes[ matches[3] ] = {
     201                                                                before: null,
     202                                                                after: null,
     203                                                                instanceNumber: parseInt( matches[3], 10 )
     204                                                        };
     205                                                }
     206                                                if ( 'dynamic_sidebar_before' === matches[1] ) {
     207                                                        boundaryNodes[ matches[3] ].before = node;
     208                                                } else {
     209                                                        boundaryNodes[ matches[3] ].after = node;
     210                                                }
     211                                        } else if ( 1 === node.nodeType ) {
     212                                                recursiveCommentTraversal( node.childNodes );
    283213                                        }
    284214                                } );
     215                        };
    285216
    286                                 _.each( sidebarPlacements, function( sidebarPlacement ) {
    287                                         var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
    288 
    289                                         // Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
    290                                         _.each( widgetPartials, function( widgetPartial ) {
    291                                                 _.each( widgetPartial.placements(), function( widgetPlacement ) {
    292 
    293                                                         if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
    294                                                                 thisPosition = widgetPlacement.container.index();
    295                                                                 sidebarWidgets.push( {
    296                                                                         partial: widgetPartial,
    297                                                                         placement: widgetPlacement,
    298                                                                         position: thisPosition
    299                                                                 } );
    300                                                                 if ( thisPosition < lastPosition ) {
    301                                                                         needsSort = true;
    302                                                                 }
    303                                                                 lastPosition = thisPosition;
    304                                                         }
    305                                                 } );
    306                                         } );
    307 
    308                                         if ( needsSort ) {
    309                                                 _.each( sidebarWidgets, function( sidebarWidget ) {
    310                                                         sidebarPlacement.endNode.parentNode.insertBefore(
    311                                                                 sidebarWidget.placement.container[0],
    312                                                                 sidebarPlacement.endNode
    313                                                         );
     217                        recursiveCommentTraversal( document.body.childNodes );
     218                        return _.values( boundaryNodes );
     219                },
    314220
    315                                                         // @todo Rename partial-placement-moved?
    316                                                         api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
    317                                                 } );
    318 
    319                                                 sortedSidebarContainers.push( sidebarPlacement );
     221                /**
     222                 * Get the placements for this partial.
     223                 *
     224                 * @since 4.5.0
     225                 * @returns {Array}
     226                 */
     227                placements: function() {
     228                        var partial = this;
     229                        return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
     230                                return new api.selectiveRefresh.Placement( {
     231                                        partial: partial,
     232                                        container: null,
     233                                        startNode: boundaryNodes.before,
     234                                        endNode: boundaryNodes.after,
     235                                        context: {
     236                                                instanceNumber: boundaryNodes.instanceNumber
    320237                                        }
    321238                                } );
     239                        } );
     240                },
    322241
    323                                 if ( sortedSidebarContainers.length > 0 ) {
    324                                         api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
    325                                 }
     242                /**
     243                 * Get the list of widget IDs associated with this widget area.
     244                 *
     245                 * @since 4.5.0
     246                 *
     247                 * @returns {Array}
     248                 */
     249                getWidgetIds: function() {
     250                        var sidebarPartial = this, settingId, widgetIds;
     251                        settingId = sidebarPartial.settings()[0];
     252                        if ( ! settingId ) {
     253                                throw new Error( 'Missing associated setting.' );
     254                        }
     255                        if ( ! api.has( settingId ) ) {
     256                                throw new Error( 'Setting does not exist.' );
     257                        }
     258                        widgetIds = api( settingId ).get();
     259                        if ( ! _.isArray( widgetIds ) ) {
     260                                throw new Error( 'Expected setting to be array of widget IDs' );
     261                        }
     262                        return widgetIds.slice( 0 );
     263                },
    326264
    327                                 return sortedSidebarContainers;
    328                         },
    329 
    330                         /**
    331                          * Make sure there is a widget instance container in this sidebar for the given widget ID.
    332                          *
    333                          * @since 4.5.0
    334                          *
    335                          * @param {string} widgetId
    336                          * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
    337                          */
    338                         ensureWidgetPlacementContainers: function( widgetId ) {
    339                                 var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
    340                                 widgetPartial = api.selectiveRefresh.partial( partialId );
    341                                 if ( ! widgetPartial ) {
    342                                         widgetPartial = new self.WidgetPartial( partialId, {
    343                                                 params: {}
    344                                         } );
    345                                         api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
     265                /**
     266                 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
     267                 *
     268                 * @since 4.5.0
     269                 *
     270                 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
     271                 */
     272                reflowWidgets: function() {
     273                        var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
     274                        widgetIds = sidebarPartial.getWidgetIds();
     275                        sidebarPlacements = sidebarPartial.placements();
     276
     277                        widgetPartials = {};
     278                        _.each( widgetIds, function( widgetId ) {
     279                                var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
     280                                if ( widgetPartial ) {
     281                                        widgetPartials[ widgetId ] = widgetPartial;
    346282                                }
     283                        } );
    347284
    348                                 // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
    349                                 _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
    350                                         var foundWidgetPlacement, widgetContainerElement;
    351 
    352                                         foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
    353                                                 return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
     285                        _.each( sidebarPlacements, function( sidebarPlacement ) {
     286                                var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
     287
     288                                // Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
     289                                _.each( widgetPartials, function( widgetPartial ) {
     290                                        _.each( widgetPartial.placements(), function( widgetPlacement ) {
     291
     292                                                if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
     293                                                        thisPosition = widgetPlacement.container.index();
     294                                                        sidebarWidgets.push( {
     295                                                                partial: widgetPartial,
     296                                                                placement: widgetPlacement,
     297                                                                position: thisPosition
     298                                                        } );
     299                                                        if ( thisPosition < lastPosition ) {
     300                                                                needsSort = true;
     301                                                        }
     302                                                        lastPosition = thisPosition;
     303                                                }
    354304                                        } );
    355                                         if ( foundWidgetPlacement ) {
    356                                                 return;
    357                                         }
     305                                } );
    358306
    359                                         widgetContainerElement = $(
    360                                                 sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
    361                                                 sidebarPartial.params.sidebarArgs.after_widget
    362                                         );
    363 
    364                                         widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
    365                                         widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
    366                                         widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
    367 
    368                                         /*
    369                                          * Make sure the widget container element has the customize-container context data.
    370                                          * The sidebar_instance_number is used to disambiguate multiple instances of the
    371                                          * same sidebar are rendered onto the template, and so the same widget is embedded
    372                                          * multiple times.
    373                                          */
    374                                         widgetContainerElement.data( 'customize-partial-placement-context', {
    375                                                 'sidebar_id': sidebarPartial.sidebarId,
    376                                                 'sidebar_instance_number': sidebarPlacement.context.instanceNumber
    377                                         } );
     307                                if ( needsSort ) {
     308                                        _.each( sidebarWidgets, function( sidebarWidget ) {
     309                                                sidebarPlacement.endNode.parentNode.insertBefore(
     310                                                        sidebarWidget.placement.container[0],
     311                                                        sidebarPlacement.endNode
     312                                                );
    378313
    379                                         sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
    380                                         wasInserted = true;
    381                                 } );
     314                                                // @todo Rename partial-placement-moved?
     315                                                api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
     316                                        } );
    382317
    383                                 if ( wasInserted ) {
    384                                         sidebarPartial.reflowWidgets();
     318                                        sortedSidebarContainers.push( sidebarPlacement );
    385319                                }
     320                        } );
    386321
    387                                 return widgetPartial;
    388                         },
    389 
    390                         /**
    391                          * Handle change to the sidebars_widgets[] setting.
    392                          *
    393                          * @since 4.5.0
    394                          *
    395                          * @param {Array} newWidgetIds New widget ids.
    396                          * @param {Array} oldWidgetIds Old widget ids.
    397                          */
    398                         handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
    399                                 var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
    400 
    401                                 needsRefresh = (
    402                                         ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
    403                                         ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
    404                                 );
    405                                 if ( needsRefresh ) {
    406                                         sidebarPartial.fallback();
    407                                         return;
    408                                 }
     322                        if ( sortedSidebarContainers.length > 0 ) {
     323                                api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     324                        }
    409325
    410                                 // Handle removal of widgets.
    411                                 widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
    412                                 _.each( widgetsRemoved, function( removedWidgetId ) {
    413                                         var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
    414                                         if ( widgetPartial ) {
    415                                                 _.each( widgetPartial.placements(), function( placement ) {
    416                                                         var isRemoved = (
    417                                                                 placement.context.sidebar_id === sidebarPartial.sidebarId ||
    418                                                                 ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
    419                                                         );
    420                                                         if ( isRemoved ) {
    421                                                                 placement.container.remove();
    422                                                         }
    423                                                 } );
    424                                         }
    425                                 } );
     326                        return sortedSidebarContainers;
     327                },
    426328
    427                                 // Handle insertion of widgets.
    428                                 widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
    429                                 _.each( widgetsAdded, function( addedWidgetId ) {
    430                                         var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
    431                                         addedWidgetPartials.push( widgetPartial );
     329                /**
     330                 * Make sure there is a widget instance container in this sidebar for the given widget ID.
     331                 *
     332                 * @since 4.5.0
     333                 *
     334                 * @param {string} widgetId
     335                 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
     336                 */
     337                ensureWidgetPlacementContainers: function( widgetId ) {
     338                        var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
     339                        widgetPartial = api.selectiveRefresh.partial( partialId );
     340                        if ( ! widgetPartial ) {
     341                                widgetPartial = new self.WidgetPartial( partialId, {
     342                                        params: {}
    432343                                } );
     344                                api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
     345                        }
    433346
    434                                 _.each( addedWidgetPartials, function( widgetPartial ) {
    435                                         widgetPartial.refresh();
     347                        // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
     348                        _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
     349                                var foundWidgetPlacement, widgetContainerElement;
     350
     351                                foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
     352                                        return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
    436353                                } );
     354                                if ( foundWidgetPlacement ) {
     355                                        return;
     356                                }
    437357
    438                                 api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
    439                         },
    440 
    441                         /**
    442                          * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
    443                          *
    444                          * @since 4.5.0
    445                          */
    446                         refresh: function() {
    447                                 var partial = this, deferred = $.Deferred();
    448 
    449                                 deferred.fail( function() {
    450                                         partial.fallback();
     358                                widgetContainerElement = $(
     359                                        sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
     360                                        sidebarPartial.params.sidebarArgs.after_widget
     361                                );
     362
     363                                widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
     364                                widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
     365                                widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
     366
     367                                /*
     368                                 * Make sure the widget container element has the customize-container context data.
     369                                 * The sidebar_instance_number is used to disambiguate multiple instances of the
     370                                 * same sidebar are rendered onto the template, and so the same widget is embedded
     371                                 * multiple times.
     372                                 */
     373                                widgetContainerElement.data( 'customize-partial-placement-context', {
     374                                        'sidebar_id': sidebarPartial.sidebarId,
     375                                        'sidebar_instance_number': sidebarPlacement.context.instanceNumber
    451376                                } );
    452377
    453                                 if ( 0 === partial.placements().length ) {
    454                                         deferred.reject();
    455                                 } else {
    456                                         _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
    457                                                 api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
    458                                         } );
    459                                         deferred.resolve();
    460                                 }
     378                                sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
     379                                wasInserted = true;
     380                        } );
    461381
    462                                 return deferred.promise();
     382                        if ( wasInserted ) {
     383                                sidebarPartial.reflowWidgets();
    463384                        }
    464                 });
    465385
    466                 api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
    467                 api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
     386                        return widgetPartial;
     387                },
    468388
    469389                /**
    470                  * Add partials for the registered widget areas (sidebars).
     390                 * Handle change to the sidebars_widgets[] setting.
    471391                 *
    472392                 * @since 4.5.0
     393                 *
     394                 * @param {Array} newWidgetIds New widget ids.
     395                 * @param {Array} oldWidgetIds Old widget ids.
    473396                 */
    474                 self.addPartials = function() {
    475                         _.each( self.registeredSidebars, function( registeredSidebar ) {
    476                                 var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
    477                                 partial = api.selectiveRefresh.partial( partialId );
    478                                 if ( ! partial ) {
    479                                         partial = new self.SidebarPartial( partialId, {
    480                                                 params: {
    481                                                         sidebarArgs: registeredSidebar
     397                handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
     398                        var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
     399
     400                        needsRefresh = (
     401                                ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
     402                                ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
     403                        );
     404                        if ( needsRefresh ) {
     405                                sidebarPartial.fallback();
     406                                return;
     407                        }
     408
     409                        // Handle removal of widgets.
     410                        widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
     411                        _.each( widgetsRemoved, function( removedWidgetId ) {
     412                                var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
     413                                if ( widgetPartial ) {
     414                                        _.each( widgetPartial.placements(), function( placement ) {
     415                                                var isRemoved = (
     416                                                        placement.context.sidebar_id === sidebarPartial.sidebarId ||
     417                                                        ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
     418                                                );
     419                                                if ( isRemoved ) {
     420                                                        placement.container.remove();
    482421                                                }
    483422                                        } );
    484                                         api.selectiveRefresh.partial.add( partial.id, partial );
    485423                                }
    486424                        } );
    487                 };
    488425
    489         }
     426                        // Handle insertion of widgets.
     427                        widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
     428                        _.each( widgetsAdded, function( addedWidgetId ) {
     429                                var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
     430                                addedWidgetPartials.push( widgetPartial );
     431                        } );
     432
     433                        _.each( addedWidgetPartials, function( widgetPartial ) {
     434                                widgetPartial.refresh();
     435                        } );
     436
     437                        api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     438                },
     439
     440                /**
     441                 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
     442                 *
     443                 * @since 4.5.0
     444                 */
     445                refresh: function() {
     446                        var partial = this, deferred = $.Deferred();
     447
     448                        deferred.fail( function() {
     449                                partial.fallback();
     450                        } );
     451
     452                        if ( 0 === partial.placements().length ) {
     453                                deferred.reject();
     454                        } else {
     455                                _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
     456                                        api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
     457                                } );
     458                                deferred.resolve();
     459                        }
     460
     461                        return deferred.promise();
     462                }
     463        });
     464
     465        api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
     466        api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
     467
     468        /**
     469         * Add partials for the registered widget areas (sidebars).
     470         *
     471         * @since 4.5.0
     472         */
     473        self.addPartials = function() {
     474                _.each( self.registeredSidebars, function( registeredSidebar ) {
     475                        var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
     476                        partial = api.selectiveRefresh.partial( partialId );
     477                        if ( ! partial ) {
     478                                partial = new self.SidebarPartial( partialId, {
     479                                        params: {
     480                                                sidebarArgs: registeredSidebar
     481                                        }
     482                                } );
     483                                api.selectiveRefresh.partial.add( partial.id, partial );
     484                        }
     485                } );
     486        };
    490487
    491488        /**
    492489         * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index 8f867ec..9ed7737 100644
    function wp_default_scripts( &$scripts ) { 
    453453        $scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
    454454
    455455        $scripts->add( 'customize-widgets', "/wp-admin/js/customize-widgets$suffix.js", array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-droppable', 'wp-backbone', 'customize-controls' ), false, 1 );
    456         $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     456        $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 );
    457457
    458458        $scripts->add( 'customize-nav-menus', "/wp-admin/js/customize-nav-menus$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu' ), false, 1 );
    459         $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     459        $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 );
    460460
    461461        $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
    462462
  • src/wp-includes/theme.php

    diff --git src/wp-includes/theme.php src/wp-includes/theme.php
    index 1727a17..4737338 100644
    function current_theme_supports( $feature ) { 
    18901890         *
    18911891         * The dynamic portion of the hook name, `$feature`, refers to the specific theme
    18921892         * feature. Possible values include 'post-formats', 'post-thumbnails', 'custom-background',
    1893          * 'custom-header', 'menus', 'automatic-feed-links', and 'html5'.
     1893         * 'custom-header', 'menus', 'automatic-feed-links', 'html5', and `customize-selective-refresh-widgets`.
    18941894         *
    18951895         * @since 3.4.0
    18961896         *
  • src/wp-includes/widgets/class-wp-widget-calendar.php

    diff --git src/wp-includes/widgets/class-wp-widget-calendar.php src/wp-includes/widgets/class-wp-widget-calendar.php
    index 2a8911a..9969678 100644
    class WP_Widget_Calendar extends WP_Widget { 
    3333         * @access public
    3434         */
    3535        public function __construct() {
    36                 $widget_ops = array('classname' => 'widget_calendar', 'description' => __( 'A calendar of your site&#8217;s Posts.') );
    37                 parent::__construct('calendar', __('Calendar'), $widget_ops);
     36                $widget_ops = array(
     37                        'classname' => 'widget_calendar',
     38                        'description' => __( 'A calendar of your site&#8217;s Posts.' ),
     39                        'customize_selective_refresh' => true,
     40                );
     41                parent::__construct( 'calendar', __( 'Calendar' ), $widget_ops );
    3842        }
    3943
    4044        /**
  • src/wp-includes/widgets/class-wp-widget-categories.php

    diff --git src/wp-includes/widgets/class-wp-widget-categories.php src/wp-includes/widgets/class-wp-widget-categories.php
    index 058fd90..2fbc62a 100644
    class WP_Widget_Categories extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array( 'classname' => 'widget_categories', 'description' => __( "A list or dropdown of categories." ) );
    27                 parent::__construct('categories', __('Categories'), $widget_ops);
     26                $widget_ops = array(
     27                        'classname' => 'widget_categories',
     28                        'description' => __( 'A list or dropdown of categories.' ),
     29                        'customize_selective_refresh' => true,
     30                );
     31                parent::__construct( 'categories', __( 'Categories' ), $widget_ops );
    2832        }
    2933
    3034        /**
  • src/wp-includes/widgets/class-wp-widget-links.php

    diff --git src/wp-includes/widgets/class-wp-widget-links.php src/wp-includes/widgets/class-wp-widget-links.php
    index 96ecc77..68d86f1 100644
    class WP_Widget_Links extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('description' => __( "Your blogroll" ) );
    27                 parent::__construct('links', __('Links'), $widget_ops);
     26                $widget_ops = array(
     27                        'description' => __( 'Your blogroll' ),
     28                        'customize_selective_refresh' => true,
     29                );
     30                parent::__construct( 'links', __( 'Links' ), $widget_ops );
    2831        }
    2932
    3033        /**
  • src/wp-includes/widgets/class-wp-widget-meta.php

    diff --git src/wp-includes/widgets/class-wp-widget-meta.php src/wp-includes/widgets/class-wp-widget-meta.php
    index c12238f..2de9844 100644
    class WP_Widget_Meta extends WP_Widget { 
    2525         * @access public
    2626         */
    2727        public function __construct() {
    28                 $widget_ops = array('classname' => 'widget_meta', 'description' => __( "Login, RSS, &amp; WordPress.org links.") );
    29                 parent::__construct('meta', __('Meta'), $widget_ops);
     28                $widget_ops = array(
     29                        'classname' => 'widget_meta',
     30                        'description' => __( 'Login, RSS, &amp; WordPress.org links.' ),
     31                        'customize_selective_refresh' => true,
     32                );
     33                parent::__construct( 'meta', __( 'Meta' ), $widget_ops );
    3034        }
    3135
    3236        /**
  • src/wp-includes/widgets/class-wp-widget-pages.php

    diff --git src/wp-includes/widgets/class-wp-widget-pages.php src/wp-includes/widgets/class-wp-widget-pages.php
    index e8737ac..165f334 100644
    class WP_Widget_Pages extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('classname' => 'widget_pages', 'description' => __( 'A list of your site&#8217;s Pages.') );
    27                 parent::__construct('pages', __('Pages'), $widget_ops);
     26                $widget_ops = array(
     27                        'classname' => 'widget_pages',
     28                        'description' => __( 'A list of your site&#8217;s Pages.' ),
     29                        'customize_selective_refresh' => true,
     30                );
     31                parent::__construct( 'pages', __( 'Pages' ), $widget_ops );
    2832        }
    2933
    3034        /**
  • src/wp-includes/widgets/class-wp-widget-recent-comments.php

    diff --git src/wp-includes/widgets/class-wp-widget-recent-comments.php src/wp-includes/widgets/class-wp-widget-recent-comments.php
    index 0f1a3b5..71fb4d2 100644
    class WP_Widget_Recent_Comments extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('classname' => 'widget_recent_comments', 'description' => __( 'Your site&#8217;s most recent comments.' ) );
    27                 parent::__construct('recent-comments', __('Recent Comments'), $widget_ops);
     26                $widget_ops = array(
     27                        'classname' => 'widget_recent_comments',
     28                        'description' => __( 'Your site&#8217;s most recent comments.' ),
     29                        'customize_selective_refresh' => true,
     30                );
     31                parent::__construct( 'recent-comments', __( 'Recent Comments' ), $widget_ops );
    2832                $this->alt_option_name = 'widget_recent_comments';
    2933
    30                 if ( is_active_widget(false, false, $this->id_base) )
    31                         add_action( 'wp_head', array($this, 'recent_comments_style') );
     34                if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
     35                        add_action( 'wp_head', array( $this, 'recent_comments_style' ) );
     36                }
    3237        }
    3338
    3439        /**
  • src/wp-includes/widgets/class-wp-widget-recent-posts.php

    diff --git src/wp-includes/widgets/class-wp-widget-recent-posts.php src/wp-includes/widgets/class-wp-widget-recent-posts.php
    index 8f92bf3..fdb54a0 100644
    class WP_Widget_Recent_Posts extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('classname' => 'widget_recent_entries', 'description' => __( "Your site&#8217;s most recent Posts.") );
    27                 parent::__construct('recent-posts', __('Recent Posts'), $widget_ops);
     26                $widget_ops = array(
     27                        'classname' => 'widget_recent_entries',
     28                        'description' => __( 'Your site&#8217;s most recent Posts.' ),
     29                        'customize_selective_refresh' => true,
     30                );
     31                parent::__construct( 'recent-posts', __( 'Recent Posts' ), $widget_ops );
    2832                $this->alt_option_name = 'widget_recent_entries';
    2933        }
    3034
  • src/wp-includes/widgets/class-wp-widget-rss.php

    diff --git src/wp-includes/widgets/class-wp-widget-rss.php src/wp-includes/widgets/class-wp-widget-rss.php
    index b477a6e..a2548c4 100644
    class WP_Widget_RSS extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array( 'description' => __('Entries from any RSS or Atom feed.') );
     26                $widget_ops = array(
     27                        'description' => __( 'Entries from any RSS or Atom feed.' ),
     28                        'customize_selective_refresh' => true,
     29                );
    2730                $control_ops = array( 'width' => 400, 'height' => 200 );
    28                 parent::__construct( 'rss', __('RSS'), $widget_ops, $control_ops );
     31                parent::__construct( 'rss', __( 'RSS' ), $widget_ops, $control_ops );
    2932        }
    3033
    3134        /**
  • src/wp-includes/widgets/class-wp-widget-search.php

    diff --git src/wp-includes/widgets/class-wp-widget-search.php src/wp-includes/widgets/class-wp-widget-search.php
    index 29bbbf8..f7d67bb 100644
    class WP_Widget_Search extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('classname' => 'widget_search', 'description' => __( "A search form for your site.") );
     26                $widget_ops = array(
     27                        'classname' => 'widget_search',
     28                        'description' => __( 'A search form for your site.' ),
     29                        'customize_selective_refresh' => true,
     30                );
    2731                parent::__construct( 'search', _x( 'Search', 'Search widget' ), $widget_ops );
    2832        }
    2933
  • src/wp-includes/widgets/class-wp-widget-tag-cloud.php

    diff --git src/wp-includes/widgets/class-wp-widget-tag-cloud.php src/wp-includes/widgets/class-wp-widget-tag-cloud.php
    index 4115c79..1251e1d 100644
    class WP_Widget_Tag_Cloud extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array( 'description' => __( "A cloud of your most used tags.") );
    27                 parent::__construct('tag_cloud', __('Tag Cloud'), $widget_ops);
     26                $widget_ops = array(
     27                        'description' => __( 'A cloud of your most used tags.' ),
     28                        'customize_selective_refresh' => true,
     29                );
     30                parent::__construct( 'tag_cloud', __( 'Tag Cloud' ), $widget_ops );
    2831        }
    2932
    3033        /**
  • src/wp-includes/widgets/class-wp-widget-text.php

    diff --git src/wp-includes/widgets/class-wp-widget-text.php src/wp-includes/widgets/class-wp-widget-text.php
    index 5a1a056..a8ec3d4 100644
    class WP_Widget_Text extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('classname' => 'widget_text', 'description' => __('Arbitrary text or HTML.'));
    27                 $control_ops = array('width' => 400, 'height' => 350);
    28                 parent::__construct('text', __('Text'), $widget_ops, $control_ops);
     26                $widget_ops = array(
     27                        'classname' => 'widget_text',
     28                        'description' => __( 'Arbitrary text or HTML.' ),
     29                        'customize_selective_refresh' => true,
     30                );
     31                $control_ops = array( 'width' => 400, 'height' => 350 );
     32                parent::__construct( 'text', __( 'Text' ), $widget_ops, $control_ops );
    2933        }
    3034
    3135        /**
  • tests/phpunit/tests/customize/manager.php

    diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php
    index 6f5789d..0b86b4c 100644
    class Tests_WP_Customize_Manager extends WP_UnitTestCase { 
    425425                $data = json_decode( $json, true );
    426426                $this->assertNotEmpty( $data );
    427427
    428                 $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'selectiveRefreshEnabled' ), array_keys( $data ) );
     428                $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices' ), array_keys( $data ) );
    429429                $this->assertEquals( $autofocus, $data['autofocus'] );
    430430                $this->assertArrayHasKey( 'save', $data['nonce'] );
    431431                $this->assertArrayHasKey( 'preview', $data['nonce'] );
  • tests/phpunit/tests/customize/widgets.php

    diff --git tests/phpunit/tests/customize/widgets.php tests/phpunit/tests/customize/widgets.php
    index c485b36..2e572bf 100644
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    4343                remove_action( 'after_setup_theme', 'twentyfifteen_setup' );
    4444                remove_action( 'after_setup_theme', 'twentysixteen_setup' );
    4545                remove_action( 'customize_register', 'twentysixteen_customize_register', 11 );
     46                add_theme_support( 'customize-selective-refresh-widgets' );
    4647
    4748                $this->backup_registered_sidebars = $GLOBALS['wp_registered_sidebars'];
    4849        }