Make WordPress Core

Ticket #35855: 35855.5.diff

File 35855.5.diff, 78.3 KB (added by westonruter, 9 years ago)

https://github.com/xwp/wordpress-develop/compare/2916ef2...36d0bd9

  • 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..fee74cf 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.selectiveRefreshableWidgets[ 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/twentyeleven/inc/widgets.php

    diff --git src/wp-content/themes/twentyeleven/inc/widgets.php src/wp-content/themes/twentyeleven/inc/widgets.php
    index 501301e..979b654 100644
    class Twenty_Eleven_Ephemera_Widget extends WP_Widget { 
    2121                parent::__construct( 'widget_twentyeleven_ephemera', __( 'Twenty Eleven Ephemera', 'twentyeleven' ), array(
    2222                        'classname'   => 'widget_twentyeleven_ephemera',
    2323                        'description' => __( 'Use this widget to list your recent Aside, Status, Quote, and Link posts', 'twentyeleven' ),
     24                        'customize_selective_refresh' => true,
    2425                ) );
    2526                $this->alt_option_name = 'widget_twentyeleven_ephemera';
    2627
  • src/wp-content/themes/twentyfifteen/functions.php

    diff --git src/wp-content/themes/twentyfifteen/functions.php src/wp-content/themes/twentyfifteen/functions.php
    index 7daeefa..618906f 100644
    function twentyfifteen_setup() { 
    125125         * specifically font, colors, icons, and column width.
    126126         */
    127127        add_editor_style( array( 'css/editor-style.css', 'genericons/genericons.css', twentyfifteen_fonts_url() ) );
     128
     129        // Indicate widget sidebars can use selective refresh in the Customizer.
     130        add_theme_support( 'customize-selective-refresh-widgets' );
    128131}
    129132endif; // twentyfifteen_setup
    130133add_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/twentyfourteen/inc/widgets.php

    diff --git src/wp-content/themes/twentyfourteen/inc/widgets.php src/wp-content/themes/twentyfourteen/inc/widgets.php
    index fee265e..ee766cb 100644
    class Twenty_Fourteen_Ephemera_Widget extends WP_Widget { 
    3434                parent::__construct( 'widget_twentyfourteen_ephemera', __( 'Twenty Fourteen Ephemera', 'twentyfourteen' ), array(
    3535                        'classname'   => 'widget_twentyfourteen_ephemera',
    3636                        'description' => __( 'Use this widget to list your recent Aside, Quote, Video, Audio, Image, Gallery, and Link posts.', 'twentyfourteen' ),
     37                        'customize_selective_refresh' => true,
    3738                ) );
     39
     40                if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
     41                        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
     42                }
     43        }
     44
     45        /**
     46         * Enqueue scripts.
     47         *
     48         * @since Twenty Fourteen 1.7
     49         */
     50        public function enqueue_scripts() {
     51                /** This filter is documented in wp-includes/media.php */
     52                $audio_library = apply_filters( 'wp_audio_shortcode_library', 'mediaelement' );
     53                /** This filter is documented in wp-includes/media.php */
     54                $video_library = apply_filters( 'wp_video_shortcode_library', 'mediaelement' );
     55                if ( in_array( 'mediaelement', array( $video_library, $audio_library ), true ) ) {
     56                        wp_enqueue_style( 'wp-mediaelement' );
     57                        wp_enqueue_script( 'wp-mediaelement' );
     58                }
    3859        }
    3960
    4061        /**
  • src/wp-content/themes/twentyfourteen/js/functions.js

    diff --git src/wp-content/themes/twentyfourteen/js/functions.js src/wp-content/themes/twentyfourteen/js/functions.js
    index 1355204..a21849e 100644
     
    146146        } );
    147147
    148148        _window.load( function() {
     149                var footerSidebar,
     150                        isCustomizeSelectiveRefresh = ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh );
     151
    149152                // Arrange footer widgets vertically.
    150153                if ( $.isFunction( $.fn.masonry ) ) {
    151                         $( '#footer-sidebar' ).masonry( {
     154                        footerSidebar = $( '#footer-sidebar' );
     155                        footerSidebar.masonry( {
    152156                                itemSelector: '.widget',
    153157                                columnWidth: function( containerWidth ) {
    154158                                        return containerWidth / 4;
     
    157161                                isResizable: true,
    158162                                isRTL: $( 'body' ).is( '.rtl' )
    159163                        } );
     164
     165                        if ( isCustomizeSelectiveRefresh ) {
     166
     167                                // Retain previous masonry-brick initial position.
     168                                wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     169                                        var copyPosition = (
     170                                                placement.partial.extended( wp.customize.widgetsPreview.WidgetPartial ) &&
     171                                                placement.removedNodes instanceof jQuery &&
     172                                                placement.removedNodes.is( '.masonry-brick' ) &&
     173                                                placement.container instanceof jQuery
     174                                        );
     175                                        if ( copyPosition ) {
     176                                                placement.container.css( {
     177                                                        position: placement.removedNodes.css( 'position' ),
     178                                                        top: placement.removedNodes.css( 'top' ),
     179                                                        left: placement.removedNodes.css( 'left' )
     180                                                } );
     181                                        }
     182                                } );
     183
     184                                // Re-arrange footer widgets after selective refresh event.
     185                                wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) {
     186                                        if ( 'sidebar-3' === sidebarPartial.sidebarId ) {
     187                                                footerSidebar.masonry( 'reloadItems' );
     188                                                footerSidebar.masonry( 'layout' );
     189                                        }
     190                                } );
     191                        }
     192                }
     193
     194                // Initialize audio and video players in Twenty_Fourteen_Ephemera_Widget widget when selectively refreshed in Customizer.
     195                if ( isCustomizeSelectiveRefresh && wp.mediaelement ) {
     196                        wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function() {
     197                                wp.mediaelement.initialize();
     198                        } );
    160199                }
    161200
    162201                // Initialize Featured Content slider.
  • 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/twentythirteen/js/functions.js

    diff --git src/wp-content/themes/twentythirteen/js/functions.js src/wp-content/themes/twentythirteen/js/functions.js
    index d152855..e0d759b 100644
     
    120120         * Arranges footer widgets vertically.
    121121         */
    122122        if ( $.isFunction( $.fn.masonry ) ) {
    123                 var columnWidth = body.is( '.sidebar' ) ? 228 : 245;
     123                var columnWidth = body.is( '.sidebar' ) ? 228 : 245,
     124                        widgetArea = $( '#secondary .widget-area' );
    124125
    125                 $( '#secondary .widget-area' ).masonry( {
     126                widgetArea.masonry( {
    126127                        itemSelector: '.widget',
    127128                        columnWidth: columnWidth,
    128129                        gutterWidth: 20,
    129130                        isRTL: body.is( '.rtl' )
    130131                } );
     132
     133                if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) {
     134
     135                        // Retain previous masonry-brick initial position.
     136                        wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     137                                var copyPosition = (
     138                                        placement.partial.extended( wp.customize.widgetsPreview.WidgetPartial ) &&
     139                                        placement.removedNodes instanceof jQuery &&
     140                                        placement.removedNodes.is( '.masonry-brick' ) &&
     141                                        placement.container instanceof jQuery
     142                                );
     143                                if ( copyPosition ) {
     144                                        placement.container.css( {
     145                                                position: placement.removedNodes.css( 'position' ),
     146                                                top: placement.removedNodes.css( 'top' ),
     147                                                left: placement.removedNodes.css( 'left' )
     148                                        } );
     149                                }
     150                        } );
     151
     152                        // Re-arrange footer widgets when sidebar is updated via selective refresh in the Customizer.
     153                        wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) {
     154                                if ( 'sidebar-1' === sidebarPartial.sidebarId ) {
     155                                        widgetArea.masonry( 'reloadItems' );
     156                                        widgetArea.masonry( 'layout' );
     157                                }
     158                        } );
     159                }
    131160        }
    132161} )( jQuery );
     162 No newline at end of file
  • src/wp-content/themes/twentythirteen/js/theme-customizer.js

    diff --git src/wp-content/themes/twentythirteen/js/theme-customizer.js src/wp-content/themes/twentythirteen/js/theme-customizer.js
    index 8519752..6072104 100644
     
    3838                        }
    3939                } );
    4040        } );
    41 
    42         if ( wp.customize.selectiveRefresh ) {
    43                 wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) {
    44                         var widgetArea;
    45                         if ( 'sidebar-1' === sidebarPartial.sidebarId && $.isFunction( $.fn.masonry ) ) {
    46                                 widgetArea = $( '#secondary .widget-area' );
    47                                 widgetArea.masonry( 'destroy' );
    48                                 widgetArea.masonry();
    49                         }
    50                 } );
    51         }
    52 
    5341} )( jQuery );
  • 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 ea30b93..9a13b6d 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 { 
    19781975                        ),
    19791976                ) ) );
    19801977
    1981                 if ( isset( $this->selective_refresh ) ) {
    1982                         $this->selective_refresh->add_partial( 'custom_logo', array(
    1983                                 'settings'            => array( 'custom_logo' ),
    1984                                 'selector'            => '.custom-logo-link',
    1985                                 'render_callback'     => array( $this, '_render_custom_logo_partial' ),
    1986                                 'container_inclusive' => true,
    1987                         ) );
    1988                 }
     1978                $this->selective_refresh->add_partial( 'site_logo', array(
     1979                        'settings'            => array( 'site_logo' ),
     1980                        'selector'            => '.site-logo-link',
     1981                        'render_callback'     => array( $this, '_render_site_logo_partial' ),
     1982                        'container_inclusive' => true,
     1983                ) );
    19891984
    19901985                /* Colors */
    19911986
  • 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..0b7b7ce 100644
    final class WP_Customize_Widgets { 
    6262        protected $old_sidebars_widgets = array();
    6363
    6464        /**
     65         * Mapping of widget ID base to whether it supports selective refresh.
     66         *
     67         * @since 4.5.0
     68         * @access protected
     69         * @var array
     70         */
     71        protected $selective_refreshable_widgets;
     72
     73        /**
    6574         * Mapping of setting type to setting ID pattern.
    6675         *
    6776         * @since 4.2.0
    final class WP_Customize_Widgets { 
    6978         * @var array
    7079         */
    7180        protected $setting_id_patterns = array(
    72                 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
    73                 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
     81                'widget_instance' => '/^widget_(?P<id_base>.+?)(?:\[(?P<widget_number>\d+)\])?$/',
     82                'sidebar_widgets' => '/^sidebars_widgets\[(?P<sidebar_id>.+?)\]$/',
    7483        );
    7584
    7685        /**
    final class WP_Customize_Widgets { 
    112121        }
    113122
    114123        /**
     124         * List whether each registered widget can be use selective refresh.
     125         *
     126         * If the theme does not support the customize-selective-refresh-widgets feature,
     127         * then this will always return an empty array.
     128         *
     129         * @since 4.5.0
     130         * @access public
     131         *
     132         * @return array Mapping of id_base to support. If theme doesn't support
     133         *               selective refresh, an empty array is returned.
     134         */
     135        public function get_selective_refreshable_widgets() {
     136                global $wp_widget_factory;
     137                if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
     138                        return array();
     139                }
     140                if ( ! isset( $this->selective_refreshable_widgets ) ) {
     141                        $this->selective_refreshable_widgets = array();
     142                        foreach ( $wp_widget_factory->widgets as $wp_widget ) {
     143                                $this->selective_refreshable_widgets[ $wp_widget->id_base ] = ! empty( $wp_widget->widget_options['customize_selective_refresh'] );
     144                        }
     145                }
     146                return $this->selective_refreshable_widgets;
     147        }
     148
     149        /**
     150         * Determines if a widget supports selective refresh.
     151         *
     152         * @since 4.5.0
     153         * @access public
     154         *
     155         * @param string $id_base Widget ID Base.
     156         * @return bool Whether the widget can be selective refreshed.
     157         */
     158        public function is_widget_selective_refreshable( $id_base ) {
     159                $selective_refreshable_widgets = $this->get_selective_refreshable_widgets();
     160                return ! empty( $selective_refreshable_widgets[ $id_base ] );
     161        }
     162
     163        /**
    115164         * Retrieves the widget setting type given a setting ID.
    116165         *
    117166         * @since 4.2.0
    final class WP_Customize_Widgets { 
    119168         *
    120169         * @staticvar array $cache
    121170         *
    122          * @param $setting_id Setting ID.
     171         * @param string $setting_id Setting ID.
    123172         * @return string|void Setting type.
    124173         */
    125174        protected function get_setting_type( $setting_id ) {
    final class WP_Customize_Widgets { 
    690739                                'widgetReorderNav' => $widget_reorder_nav_tpl,
    691740                                'moveWidgetArea'   => $move_widget_area_tpl,
    692741                        ),
    693                         'selectiveRefresh'     => isset( $this->manager->selective_refresh ),
     742                        'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
    694743                );
    695744
    696745                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    final class WP_Customize_Widgets { 
    771820                $args = array(
    772821                        'type'       => 'option',
    773822                        'capability' => 'edit_theme_options',
    774                         'transport'  => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    775823                        'default'    => array(),
    776824                );
    777825
    778826                if ( preg_match( $this->setting_id_patterns['sidebar_widgets'], $id, $matches ) ) {
    779827                        $args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
    780828                        $args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
     829                        $args['transport'] = current_theme_supports( 'customize-selective-refresh-widgets' ) ? 'postMessage' : 'refresh';
    781830                } elseif ( preg_match( $this->setting_id_patterns['widget_instance'], $id, $matches ) ) {
    782831                        $args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
    783832                        $args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
     833                        $args['transport'] = $this->is_widget_selective_refreshable( $matches['id_base'] ) ? 'postMessage' : 'refresh';
    784834                }
    785835
    786836                $args = array_merge( $args, $overrides );
    final class WP_Customize_Widgets { 
    893943                                'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
    894944                                'is_disabled'  => $is_disabled,
    895945                                'id_base'      => $id_base,
    896                                 'transport'    => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     946                                'transport'    => $this->is_widget_selective_refreshable( $id_base ) ? 'postMessage' : 'refresh',
    897947                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
    898948                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
    899949                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
    final class WP_Customize_Widgets { 
    10251075         */
    10261076        public function customize_preview_enqueue() {
    10271077                wp_enqueue_script( 'customize-preview-widgets' );
     1078                wp_enqueue_style( 'customize-preview' );
    10281079        }
    10291080
    10301081        /**
    final class WP_Customize_Widgets { 
    10601111         */
    10611112        public function export_preview_data() {
    10621113                global $wp_registered_sidebars, $wp_registered_widgets;
     1114
    10631115                // Prepare Customizer settings to pass to JavaScript.
    10641116                $settings = array(
    10651117                        'renderedSidebars'   => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
    final class WP_Customize_Widgets { 
    10691121                        'l10n'               => array(
    10701122                                'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
    10711123                        ),
    1072                         'selectiveRefresh'   => isset( $this->manager->selective_refresh ),
     1124                        'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
    10731125                );
    10741126                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    10751127                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
    final class WP_Customize_Widgets { 
    14791531         * @return array (Maybe) modified partial arguments.
    14801532         */
    14811533        public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
     1534                if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
     1535                        return $partial_args;
     1536                }
    14821537
    14831538                if ( preg_match( '/^widget\[(?P<widget_id>.+)\]$/', $partial_id, $matches ) ) {
    14841539                        if ( false === $partial_args ) {
    final class WP_Customize_Widgets { 
    15061561         * @access public
    15071562         */
    15081563        public function selective_refresh_init() {
    1509                 if ( ! isset( $this->manager->selective_refresh ) ) {
     1564                if ( ! current_theme_supports( 'customize-selective-refresh-widgets' ) ) {
    15101565                        return;
    15111566                }
    1512 
    1513                 add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
    15141567                add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
    15151568                add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
    15161569                add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
    final class WP_Customize_Widgets { 
    15181571        }
    15191572
    15201573        /**
    1521          * Enqueues scripts for the Customizer preview.
    1522          *
    1523          * @since 4.5.0
    1524          * @access public
    1525          */
    1526         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 
    1532                 wp_enqueue_script( 'customize-preview-widgets' );
    1533                 wp_enqueue_style( 'customize-preview' );
    1534         }
    1535 
    1536         /**
    15371574         * Inject selective refresh data attributes into widget container elements.
    15381575         *
    15391576         * @param array $params {
  • 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..b22087b 100644
    wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( 
    1212                preview: null,
    1313                l10n: {
    1414                        widgetTooltip: ''
    15                 }
     15                },
     16                selectiveRefreshableWidgets: {}
    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.selectiveRefreshableWidgets ) ) {
    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                        partial.widgetIdParts = self.parseWidgetId( partial.widgetId );
     68                        options = options || {};
     69                        options.params = _.extend(
     70                                {
     71                                        settings: [ self.getWidgetSettingId( partial.widgetId ) ],
     72                                        containerInclusive: true
     73                                },
     74                                options.params || {}
     75                        );
    7876
    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                                 }
     77                        api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     78                },
     79
     80                /**
     81                 * Refresh widget partial.
     82                 *
     83                 * @returns {Promise}
     84                 */
     85                refresh: function() {
     86                        var partial = this, refreshDeferred;
     87                        if ( ! self.selectiveRefreshableWidgets[ partial.widgetIdParts.idBase ] ) {
     88                                refreshDeferred = $.Deferred();
     89                                refreshDeferred.reject();
     90                                partial.fallback();
     91                                return refreshDeferred.promise();
     92                        } else {
     93                                return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
    9494                        }
    95                 });
     95                },
    9696
    9797                /**
    98                  * Partial representing a widget area.
     98                 * Send widget-updated message to parent so spinner will get removed from widget control.
     99                 *
     100                 * @inheritdoc
     101                 * @param {wp.customize.selectiveRefresh.Placement} placement
     102                 */
     103                renderContent: function( placement ) {
     104                        var partial = this;
     105                        if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
     106                                api.preview.send( 'widget-updated', partial.widgetId );
     107                                api.selectiveRefresh.trigger( 'widget-updated', partial );
     108                        }
     109                }
     110        });
     111
     112        /**
     113         * Partial representing a widget area.
     114         *
     115         * @class
     116         * @augments wp.customize.selectiveRefresh.Partial
     117         * @since 4.5.0
     118         */
     119        self.SidebarPartial = api.selectiveRefresh.Partial.extend({
     120
     121                /**
     122                 * Constructor.
    99123                 *
    100                  * @class
    101                  * @augments wp.customize.selectiveRefresh.Partial
    102124                 * @since 4.5.0
     125                 * @param {string} id - Partial ID.
     126                 * @param {Object} options
     127                 * @param {Object} options.params
    103128                 */
    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                                 );
     129                initialize: function( id, options ) {
     130                        var partial = this, matches;
     131                        matches = id.match( /^sidebar\[(.+)]$/ );
     132                        if ( ! matches ) {
     133                                throw new Error( 'Illegal id for sidebar partial.' );
     134                        }
     135                        partial.sidebarId = matches[1];
    129136
    130                                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     137                        options = options || {};
     138                        options.params = _.extend(
     139                                {
     140                                        settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
     141                                },
     142                                options.params || {}
     143                        );
    131144
    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                                 } );
     145                        api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
    152146
    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                                 } );
     147                        if ( ! partial.params.sidebarArgs ) {
     148                                throw new Error( 'The sidebarArgs param was not provided.' );
     149                        }
     150                        if ( partial.params.settings.length > 1 ) {
     151                                throw new Error( 'Expected SidebarPartial to only have one associated setting' );
     152                        }
     153                },
    163154
    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.' );
     155                /**
     156                 * Set up the partial.
     157                 *
     158                 * @since 4.5.0
     159                 */
     160                ready: function() {
     161                        var sidebarPartial = this;
     162
     163                        // Watch for changes to the sidebar_widgets setting.
     164                        _.each( sidebarPartial.settings(), function( settingId ) {
     165                                api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
     166                        } );
     167
     168                        // Trigger an event for this sidebar being updated whenever a widget inside is rendered.
     169                        api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     170                                var isAssignedWidgetPartial = (
     171                                        placement.partial.extended( self.WidgetPartial ) &&
     172                                        ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
     173                                );
     174                                if ( isAssignedWidgetPartial ) {
     175                                        api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
    255176                                }
    256                                 if ( ! api.has( settingId ) ) {
    257                                         throw new Error( 'Setting does not exist.' );
     177                        } );
     178
     179                        // Make sure that a widget partial has a container in the DOM prior to a refresh.
     180                        api.bind( 'change', function( widgetSetting ) {
     181                                var widgetId, parsedId;
     182                                parsedId = self.parseWidgetSettingId( widgetSetting.id );
     183                                if ( ! parsedId ) {
     184                                        return;
    258185                                }
    259                                 widgetIds = api( settingId ).get();
    260                                 if ( ! _.isArray( widgetIds ) ) {
    261                                         throw new Error( 'Expected setting to be array of widget IDs' );
     186                                widgetId = parsedId.idBase;
     187                                if ( parsedId.number ) {
     188                                        widgetId += '-' + String( parsedId.number );
    262189                                }
    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;
     190                                if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
     191                                        sidebarPartial.ensureWidgetPlacementContainers( widgetId );
     192                                }
     193                        } );
     194                },
     195
     196                /**
     197                 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
     198                 *
     199                 * Note that TreeWalker is not implemented in IE8.
     200                 *
     201                 * @since 4.5.0
     202                 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
     203                 */
     204                findDynamicSidebarBoundaryNodes: function() {
     205                        var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
     206                        regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
     207                        recursiveCommentTraversal = function( childNodes ) {
     208                                _.each( childNodes, function( node ) {
     209                                        var matches;
     210                                        if ( 8 === node.nodeType ) {
     211                                                matches = node.nodeValue.match( regExp );
     212                                                if ( ! matches || matches[2] !== partial.sidebarId ) {
     213                                                        return;
     214                                                }
     215                                                if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
     216                                                        boundaryNodes[ matches[3] ] = {
     217                                                                before: null,
     218                                                                after: null,
     219                                                                instanceNumber: parseInt( matches[3], 10 )
     220                                                        };
     221                                                }
     222                                                if ( 'dynamic_sidebar_before' === matches[1] ) {
     223                                                        boundaryNodes[ matches[3] ].before = node;
     224                                                } else {
     225                                                        boundaryNodes[ matches[3] ].after = node;
     226                                                }
     227                                        } else if ( 1 === node.nodeType ) {
     228                                                recursiveCommentTraversal( node.childNodes );
    283229                                        }
    284230                                } );
     231                        };
    285232
    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                                                         );
    314 
    315                                                         // @todo Rename partial-placement-moved?
    316                                                         api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
    317                                                 } );
     233                        recursiveCommentTraversal( document.body.childNodes );
     234                        return _.values( boundaryNodes );
     235                },
    318236
    319                                                 sortedSidebarContainers.push( sidebarPlacement );
     237                /**
     238                 * Get the placements for this partial.
     239                 *
     240                 * @since 4.5.0
     241                 * @returns {Array}
     242                 */
     243                placements: function() {
     244                        var partial = this;
     245                        return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
     246                                return new api.selectiveRefresh.Placement( {
     247                                        partial: partial,
     248                                        container: null,
     249                                        startNode: boundaryNodes.before,
     250                                        endNode: boundaryNodes.after,
     251                                        context: {
     252                                                instanceNumber: boundaryNodes.instanceNumber
    320253                                        }
    321254                                } );
     255                        } );
     256                },
    322257
    323                                 if ( sortedSidebarContainers.length > 0 ) {
    324                                         api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
    325                                 }
     258                /**
     259                 * Get the list of widget IDs associated with this widget area.
     260                 *
     261                 * @since 4.5.0
     262                 *
     263                 * @returns {Array}
     264                 */
     265                getWidgetIds: function() {
     266                        var sidebarPartial = this, settingId, widgetIds;
     267                        settingId = sidebarPartial.settings()[0];
     268                        if ( ! settingId ) {
     269                                throw new Error( 'Missing associated setting.' );
     270                        }
     271                        if ( ! api.has( settingId ) ) {
     272                                throw new Error( 'Setting does not exist.' );
     273                        }
     274                        widgetIds = api( settingId ).get();
     275                        if ( ! _.isArray( widgetIds ) ) {
     276                                throw new Error( 'Expected setting to be array of widget IDs' );
     277                        }
     278                        return widgetIds.slice( 0 );
     279                },
    326280
    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 );
     281                /**
     282                 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
     283                 *
     284                 * @since 4.5.0
     285                 *
     286                 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
     287                 */
     288                reflowWidgets: function() {
     289                        var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
     290                        widgetIds = sidebarPartial.getWidgetIds();
     291                        sidebarPlacements = sidebarPartial.placements();
     292
     293                        widgetPartials = {};
     294                        _.each( widgetIds, function( widgetId ) {
     295                                var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
     296                                if ( widgetPartial ) {
     297                                        widgetPartials[ widgetId ] = widgetPartial;
    346298                                }
     299                        } );
    347300
    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 );
     301                        _.each( sidebarPlacements, function( sidebarPlacement ) {
     302                                var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;
     303
     304                                // Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
     305                                _.each( widgetPartials, function( widgetPartial ) {
     306                                        _.each( widgetPartial.placements(), function( widgetPlacement ) {
     307
     308                                                if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
     309                                                        thisPosition = widgetPlacement.container.index();
     310                                                        sidebarWidgets.push( {
     311                                                                partial: widgetPartial,
     312                                                                placement: widgetPlacement,
     313                                                                position: thisPosition
     314                                                        } );
     315                                                        if ( thisPosition < lastPosition ) {
     316                                                                needsSort = true;
     317                                                        }
     318                                                        lastPosition = thisPosition;
     319                                                }
    354320                                        } );
    355                                         if ( foundWidgetPlacement ) {
    356                                                 return;
    357                                         }
     321                                } );
    358322
    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                                         } );
     323                                if ( needsSort ) {
     324                                        _.each( sidebarWidgets, function( sidebarWidget ) {
     325                                                sidebarPlacement.endNode.parentNode.insertBefore(
     326                                                        sidebarWidget.placement.container[0],
     327                                                        sidebarPlacement.endNode
     328                                                );
    378329
    379                                         sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
    380                                         wasInserted = true;
    381                                 } );
     330                                                // @todo Rename partial-placement-moved?
     331                                                api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
     332                                        } );
    382333
    383                                 if ( wasInserted ) {
    384                                         sidebarPartial.reflowWidgets();
     334                                        sortedSidebarContainers.push( sidebarPlacement );
    385335                                }
     336                        } );
    386337
    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                                 }
     338                        if ( sortedSidebarContainers.length > 0 ) {
     339                                api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     340                        }
    409341
    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                                 } );
     342                        return sortedSidebarContainers;
     343                },
    426344
    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 );
     345                /**
     346                 * Make sure there is a widget instance container in this sidebar for the given widget ID.
     347                 *
     348                 * @since 4.5.0
     349                 *
     350                 * @param {string} widgetId
     351                 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
     352                 */
     353                ensureWidgetPlacementContainers: function( widgetId ) {
     354                        var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
     355                        widgetPartial = api.selectiveRefresh.partial( partialId );
     356                        if ( ! widgetPartial ) {
     357                                widgetPartial = new self.WidgetPartial( partialId, {
     358                                        params: {}
    432359                                } );
     360                                api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
     361                        }
     362
     363                        // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
     364                        _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
     365                                var foundWidgetPlacement, widgetContainerElement;
    433366
    434                                 _.each( addedWidgetPartials, function( widgetPartial ) {
    435                                         widgetPartial.refresh();
     367                                foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
     368                                        return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
    436369                                } );
     370                                if ( foundWidgetPlacement ) {
     371                                        return;
     372                                }
    437373
    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();
     374                                widgetContainerElement = $(
     375                                        sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
     376                                        sidebarPartial.params.sidebarArgs.after_widget
     377                                );
     378
     379                                widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
     380                                widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
     381                                widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
     382
     383                                /*
     384                                 * Make sure the widget container element has the customize-container context data.
     385                                 * The sidebar_instance_number is used to disambiguate multiple instances of the
     386                                 * same sidebar are rendered onto the template, and so the same widget is embedded
     387                                 * multiple times.
     388                                 */
     389                                widgetContainerElement.data( 'customize-partial-placement-context', {
     390                                        'sidebar_id': sidebarPartial.sidebarId,
     391                                        'sidebar_instance_number': sidebarPlacement.context.instanceNumber
    451392                                } );
    452393
    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                                 }
     394                                sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
     395                                wasInserted = true;
     396                        } );
    461397
    462                                 return deferred.promise();
     398                        if ( wasInserted ) {
     399                                sidebarPartial.reflowWidgets();
    463400                        }
    464                 });
    465401
    466                 api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
    467                 api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
     402                        return widgetPartial;
     403                },
    468404
    469405                /**
    470                  * Add partials for the registered widget areas (sidebars).
     406                 * Handle change to the sidebars_widgets[] setting.
    471407                 *
    472408                 * @since 4.5.0
     409                 *
     410                 * @param {Array} newWidgetIds New widget ids.
     411                 * @param {Array} oldWidgetIds Old widget ids.
    473412                 */
    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
     413                handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
     414                        var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
     415
     416                        needsRefresh = (
     417                                ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
     418                                ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
     419                        );
     420                        if ( needsRefresh ) {
     421                                sidebarPartial.fallback();
     422                                return;
     423                        }
     424
     425                        // Handle removal of widgets.
     426                        widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
     427                        _.each( widgetsRemoved, function( removedWidgetId ) {
     428                                var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
     429                                if ( widgetPartial ) {
     430                                        _.each( widgetPartial.placements(), function( placement ) {
     431                                                var isRemoved = (
     432                                                        placement.context.sidebar_id === sidebarPartial.sidebarId ||
     433                                                        ( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
     434                                                );
     435                                                if ( isRemoved ) {
     436                                                        placement.container.remove();
    482437                                                }
    483438                                        } );
    484                                         api.selectiveRefresh.partial.add( partial.id, partial );
    485439                                }
    486440                        } );
    487                 };
    488441
    489         }
     442                        // Handle insertion of widgets.
     443                        widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
     444                        _.each( widgetsAdded, function( addedWidgetId ) {
     445                                var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
     446                                addedWidgetPartials.push( widgetPartial );
     447                        } );
     448
     449                        _.each( addedWidgetPartials, function( widgetPartial ) {
     450                                widgetPartial.refresh();
     451                        } );
     452
     453                        api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     454                },
     455
     456                /**
     457                 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
     458                 *
     459                 * @since 4.5.0
     460                 */
     461                refresh: function() {
     462                        var partial = this, deferred = $.Deferred();
     463
     464                        deferred.fail( function() {
     465                                partial.fallback();
     466                        } );
     467
     468                        if ( 0 === partial.placements().length ) {
     469                                deferred.reject();
     470                        } else {
     471                                _.each( partial.reflowWidgets(), function( sidebarPlacement ) {
     472                                        api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
     473                                } );
     474                                deferred.resolve();
     475                        }
     476
     477                        return deferred.promise();
     478                }
     479        });
     480
     481        api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
     482        api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
     483
     484        /**
     485         * Add partials for the registered widget areas (sidebars).
     486         *
     487         * @since 4.5.0
     488         */
     489        self.addPartials = function() {
     490                _.each( self.registeredSidebars, function( registeredSidebar ) {
     491                        var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
     492                        partial = api.selectiveRefresh.partial( partialId );
     493                        if ( ! partial ) {
     494                                partial = new self.SidebarPartial( partialId, {
     495                                        params: {
     496                                                sidebarArgs: registeredSidebar
     497                                        }
     498                                } );
     499                                api.selectiveRefresh.partial.add( partial.id, partial );
     500                        }
     501                } );
     502        };
    490503
    491504        /**
    492505         * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
  • src/wp-includes/js/customize-selective-refresh.js

    diff --git src/wp-includes/js/customize-selective-refresh.js src/wp-includes/js/customize-selective-refresh.js
    index 8927716..7efee3d 100644
    wp.customize.selectiveRefresh = ( function( $, api ) { 
    109109                placements: function() {
    110110                        var partial = this, selector;
    111111
    112                         selector = partial.params.selector;
     112                        selector = partial.params.selector || '';
    113113                        if ( selector ) {
    114114                                selector += ', ';
    115115                        }
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index 9b4643f..63bbab2 100644
    function wp_default_scripts( &$scripts ) { 
    455455        $scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
    456456
    457457        $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 );
    458         $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     458        $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 );
    459459
    460460        $scripts->add( 'customize-nav-menus', "/wp-admin/js/customize-nav-menus$suffix.js", array( 'jquery', 'wp-backbone', 'customize-controls', 'accordion', 'nav-menu' ), false, 1 );
    461         $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
     461        $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 );
    462462
    463463        $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
    464464
  • src/wp-includes/theme.php

    diff --git src/wp-includes/theme.php src/wp-includes/theme.php
    index 15681e5..1ad720c 100644
    function current_theme_supports( $feature ) { 
    19141914         *
    19151915         * The dynamic portion of the hook name, `$feature`, refers to the specific theme
    19161916         * feature. Possible values include 'post-formats', 'post-thumbnails', 'custom-background',
    1917          * 'custom-header', 'menus', 'automatic-feed-links', and 'html5'.
     1917         * 'custom-header', 'menus', 'automatic-feed-links', 'html5', and `customize-selective-refresh-widgets`.
    19181918         *
    19191919         * @since 3.4.0
    19201920         *
  • src/wp-includes/widgets/class-wp-nav-menu-widget.php

    diff --git src/wp-includes/widgets/class-wp-nav-menu-widget.php src/wp-includes/widgets/class-wp-nav-menu-widget.php
    index d6ac26c..d465525 100644
     
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array( 'description' => __('Add a custom menu to your sidebar.') );
     26                $widget_ops = array(
     27                        'description' => __( 'Add a custom menu to your sidebar.' ),
     28                        'customize_selective_refresh' => true,
     29                );
    2730                parent::__construct( 'nav_menu', __('Custom Menu'), $widget_ops );
    2831        }
    2932
  • src/wp-includes/widgets/class-wp-widget-archives.php

    diff --git src/wp-includes/widgets/class-wp-widget-archives.php src/wp-includes/widgets/class-wp-widget-archives.php
    index 30e4ee0..ba12572 100644
    class WP_Widget_Archives extends WP_Widget { 
    2323         * @access public
    2424         */
    2525        public function __construct() {
    26                 $widget_ops = array('classname' => 'widget_archive', 'description' => __( 'A monthly archive of your site&#8217;s Posts.') );
     26                $widget_ops = array(
     27                        'classname' => 'widget_archive',
     28                        'description' => __( 'A monthly archive of your site&#8217;s Posts.' ),
     29                        'customize_selective_refresh' => true,
     30                );
    2731                parent::__construct('archives', __('Archives'), $widget_ops);
    2832        }
    2933
  • 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..28392fd 100644
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    2424                parent::setUp();
    2525                require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
    2626
     27                add_theme_support( 'customize-selective-refresh-widgets' );
    2728                $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
    2829                wp_set_current_user( $user_id );
    2930                $GLOBALS['wp_customize'] = new WP_Customize_Manager();
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    4748                $this->backup_registered_sidebars = $GLOBALS['wp_registered_sidebars'];
    4849        }
    4950
     51        function clean_up_global_scope() {
     52                global $wp_widget_factory, $wp_registered_sidebars, $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates;
     53
     54                $wp_registered_sidebars = array();
     55                $wp_registered_widgets = array();
     56                $wp_registered_widget_controls = array();
     57                $wp_registered_widget_updates = array();
     58                $wp_widget_factory->widgets = array();
     59
     60                parent::clean_up_global_scope();
     61        }
     62
    5063        function tearDown() {
    5164                $this->manager = null;
    5265                unset( $GLOBALS['wp_customize'] );
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    7891        }
    7992
    8093        /**
     94         * Tests WP_Customize_Widgets::get_selective_refreshable_widgets().
     95         *
     96         * @see WP_Customize_Widgets::get_selective_refreshable_widgets()
     97         */
     98        function test_get_selective_refreshable_widgets_when_theme_supports() {
     99                global $wp_widget_factory;
     100                add_action( 'widgets_init', array( $this, 'override_search_widget_customize_selective_refresh' ), 90 );
     101                add_theme_support( 'customize-selective-refresh-widgets' );
     102                $this->do_customize_boot_actions();
     103
     104                $selective_refreshable_widgets = $this->manager->widgets->get_selective_refreshable_widgets();
     105                $this->assertInternalType( 'array', $selective_refreshable_widgets );
     106                $this->assertEquals( count( $wp_widget_factory->widgets ), count( $selective_refreshable_widgets ) );
     107                $this->assertArrayHasKey( 'text', $selective_refreshable_widgets );
     108                $this->assertTrue( $selective_refreshable_widgets['text'] );
     109                $this->assertArrayHasKey( 'search', $selective_refreshable_widgets );
     110                $this->assertFalse( $selective_refreshable_widgets['search'] );
     111        }
     112
     113        /**
     114         * Tests WP_Customize_Widgets::get_selective_refreshable_widgets().
     115         *
     116         * @see WP_Customize_Widgets::get_selective_refreshable_widgets()
     117         */
     118        function test_get_selective_refreshable_widgets_when_no_theme_supports() {
     119                add_action( 'widgets_init', array( $this, 'override_search_widget_customize_selective_refresh' ), 90 );
     120                remove_theme_support( 'customize-selective-refresh-widgets' );
     121                $this->do_customize_boot_actions();
     122                $selective_refreshable_widgets = $this->manager->widgets->get_selective_refreshable_widgets();
     123                $this->assertEmpty( $selective_refreshable_widgets );
     124        }
     125
     126        /**
     127         * Hook into widgets_init to override the search widget's customize_selective_refresh widget option.
     128         *
     129         * @see Tests_WP_Customize_Widgets::test_get_selective_refreshable_widgets_when_theme_supports()
     130         * @see Tests_WP_Customize_Widgets::test_get_selective_refreshable_widgets_when_no_theme_supports()
     131         */
     132        function override_search_widget_customize_selective_refresh() {
     133                global $wp_widget_factory;
     134                $wp_widget_factory->widgets['WP_Widget_Search']->widget_options['customize_selective_refresh'] = false;
     135        }
     136
     137        /**
     138         * Tests WP_Customize_Widgets::is_widget_selective_refreshable().
     139         *
     140         * @see WP_Customize_Widgets::is_widget_selective_refreshable()
     141         */
     142        function test_is_widget_selective_refreshable() {
     143                add_action( 'widgets_init', array( $this, 'override_search_widget_customize_selective_refresh' ), 90 );
     144                add_theme_support( 'customize-selective-refresh-widgets' );
     145                $this->do_customize_boot_actions();
     146                $this->assertFalse( $this->manager->widgets->is_widget_selective_refreshable( 'search' ) );
     147                $this->assertTrue( $this->manager->widgets->is_widget_selective_refreshable( 'text' ) );
     148                remove_theme_support( 'customize-selective-refresh-widgets' );
     149                $this->assertFalse( $this->manager->widgets->is_widget_selective_refreshable( 'text' ) );
     150        }
     151
     152        /**
    81153         * Test WP_Customize_Widgets::register_settings()
    82154         *
    83155         * @ticket 30988
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    116188         * Test WP_Customize_Widgets::get_setting_args()
    117189         */
    118190        function test_get_setting_args() {
     191                add_theme_support( 'customize-selective-refresh-widgets' );
     192                $this->do_customize_boot_actions();
    119193
    120194                add_filter( 'widget_customizer_setting_args', array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 );
    121195
    122196                $default_args = array(
    123197                        'type' => 'option',
    124198                        'capability' => 'edit_theme_options',
    125                         'transport' => 'postMessage',
     199                        'transport' => 'refresh',
    126200                        'default' => array(),
    127201                        'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
    128202                        'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
    129203                );
    130 
    131204                $args = $this->manager->widgets->get_setting_args( 'widget_foo[2]' );
    132205                foreach ( $default_args as $key => $default_value ) {
    133206                        $this->assertEquals( $default_value, $args[ $key ] );
    134207                }
    135208                $this->assertEquals( 'WIDGET_FOO[2]', $args['uppercase_id_set_by_filter'] );
    136209
     210                $default_args = array(
     211                        'type' => 'option',
     212                        'capability' => 'edit_theme_options',
     213                        'transport' => 'postMessage',
     214                        'default' => array(),
     215                        'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
     216                        'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
     217                );
     218                $args = $this->manager->widgets->get_setting_args( 'widget_search[2]' );
     219                foreach ( $default_args as $key => $default_value ) {
     220                        $this->assertEquals( $default_value, $args[ $key ] );
     221                }
     222
     223                remove_theme_support( 'customize-selective-refresh-widgets' );
     224                $args = $this->manager->widgets->get_setting_args( 'widget_search[2]' );
     225                $this->assertEquals( 'refresh', $args['transport'] );
     226                add_theme_support( 'customize-selective-refresh-widgets' );
     227
    137228                $override_args = array(
    138229                        'type' => 'theme_mod',
    139230                        'capability' => 'edit_posts',
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    368459                $this->assertEquals( array( $this->manager->widgets, 'render_widget_partial' ), $args['render_callback'] );
    369460                $this->assertTrue( $args['container_inclusive'] );
    370461                $this->assertFalse( $args['fallback_refresh'] );
     462
     463                remove_theme_support( 'customize-selective-refresh-widgets' );
     464                $args = apply_filters( 'customize_dynamic_partial_args', false, 'widget[search-2]' );
     465                $this->assertFalse( $args );
    371466        }
    372467
    373468        /**
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    375470         *
    376471         * @see WP_Customize_Widgets::selective_refresh_init()
    377472         */
    378         function test_selective_refresh_init() {
     473        function test_selective_refresh_init_with_theme_support() {
     474                add_theme_support( 'customize-selective-refresh-widgets' );
    379475                $this->manager->widgets->selective_refresh_init();
    380                 $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $this->manager->widgets, 'customize_preview_enqueue_deps' ) ) );
    381476                $this->assertEquals( 10, has_action( 'dynamic_sidebar_before', array( $this->manager->widgets, 'start_dynamic_sidebar' ) ) );
    382477                $this->assertEquals( 10, has_action( 'dynamic_sidebar_after', array( $this->manager->widgets, 'end_dynamic_sidebar' ) ) );
    383478                $this->assertEquals( 10, has_filter( 'dynamic_sidebar_params', array( $this->manager->widgets, 'filter_dynamic_sidebar_params' ) ) );
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    385480        }
    386481
    387482        /**
    388          * Test WP_Customize_Widgets::customize_preview_enqueue_deps().
     483         * Test WP_Customize_Widgets::selective_refresh_init().
     484         *
     485         * @see WP_Customize_Widgets::selective_refresh_init()
     486         */
     487        function test_selective_refresh_init_without_theme_support() {
     488                remove_theme_support( 'customize-selective-refresh-widgets' );
     489                $this->manager->widgets->selective_refresh_init();
     490                $this->assertFalse( has_action( 'dynamic_sidebar_before', array( $this->manager->widgets, 'start_dynamic_sidebar' ) ) );
     491                $this->assertFalse( has_action( 'dynamic_sidebar_after', array( $this->manager->widgets, 'end_dynamic_sidebar' ) ) );
     492                $this->assertFalse( has_filter( 'dynamic_sidebar_params', array( $this->manager->widgets, 'filter_dynamic_sidebar_params' ) ) );
     493                $this->assertFalse( has_filter( 'wp_kses_allowed_html', array( $this->manager->widgets, 'filter_wp_kses_allowed_data_attributes' ) ) );
     494        }
     495
     496        /**
     497         * Test WP_Customize_Widgets::customize_preview_enqueue().
    389498         *
    390          * @see WP_Customize_Widgets::customize_preview_enqueue_deps()
     499         * @see WP_Customize_Widgets::customize_preview_enqueue()
    391500         */
    392         function test_customize_preview_enqueue_deps() {
    393                 $this->manager->widgets->customize_preview_enqueue_deps();
     501        function test_customize_preview_enqueue() {
     502                $this->manager->widgets->customize_preview_enqueue();
    394503                $this->assertTrue( wp_script_is( 'customize-preview-widgets', 'enqueued' ) );
    395504                $this->assertTrue( wp_style_is( 'customize-preview', 'enqueued' ) );
    396505                $script = wp_scripts()->registered['customize-preview-widgets'];
    class Tests_WP_Customize_Widgets extends WP_UnitTestCase { 
    458567         * @see WP_Customize_Widgets::render_widget_partial()
    459568         */
    460569        function test_render_widget_partial() {
     570                add_theme_support( 'customize-selective-refresh-widgets' );
     571                $this->do_customize_boot_actions();
    461572                $this->manager->widgets->selective_refresh_init();
    462573
    463574                $partial_id = 'widget[search-2]';