Make WordPress Core

Changeset 37040


Ignore:
Timestamp:
03/21/2016 09:58:02 PM (8 years ago)
Author:
westonruter
Message:

Customize: Require opt-in for selective refresh of widgets.

  • Introduces customize-selective-refresh-widgets theme support feature and adds to themes.
  • Introduces customize_selective_refresh arg for WP_Widget::$widget_options and adds to all core widgets.
  • Remove selective_refresh from being a component that can be removed via customize_loaded_components filter.
  • Add WP_Customize_Widgets::get_selective_refreshable_widgets() and WP_Customize_Widgets::is_widget_selective_refreshable().
  • Fix default selector for Partial instances.
  • Implement and improve Masronry sidebar refresh logic in Twenty Thirteen and Twenty Fourteen, including preservation of initial widget position after refresh.
  • Re-initialize ME.js when refreshing Twenty_Fourteen_Ephemera_Widget.

See #27355.
Fixes #35855.

Location:
trunk
Files:
34 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/js/customize-widgets.js

    r36586 r37040  
    3535        name: null,
    3636        id_base: null,
    37         transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
     37        transport: null,
    3838        params: [],
    3939        width: null,
     
    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                };
  • trunk/src/wp-content/themes/twentyeleven/functions.php

    r32116 r37040  
    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
  • trunk/src/wp-content/themes/twentyeleven/inc/widgets.php

    r33086 r37040  
    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';
  • trunk/src/wp-content/themes/twentyfifteen/functions.php

    r36913 r37040  
    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
  • trunk/src/wp-content/themes/twentyfourteen/functions.php

    r32843 r37040  
    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
  • trunk/src/wp-content/themes/twentyfourteen/inc/widgets.php

    r32116 r37040  
    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
  • trunk/src/wp-content/themes/twentyfourteen/js/functions.js

    r36999 r37040  
    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 ) {
     
    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
  • trunk/src/wp-content/themes/twentythirteen/functions.php

    r36797 r37040  
    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' );
  • trunk/src/wp-content/themes/twentythirteen/js/functions.js

    r36999 r37040  
    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,
     
    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 );
  • trunk/src/wp-content/themes/twentythirteen/js/theme-customizer.js

    r36586 r37040  
    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 );
  • trunk/src/wp-content/themes/twentytwelve/functions.php

    r36797 r37040  
    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' );
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r36915 r37040  
    110110     * @var array
    111111     */
    112     protected $components = array( 'widgets', 'nav_menus', 'selective_refresh' );
     112    protected $components = array( 'widgets', 'nav_menus' );
    113113
    114114    /**
     
    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' );
     
    267270            require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
    268271            $this->nav_menus = new WP_Customize_Nav_Menus( $this );
    269         }
    270 
    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 );
    274272        }
    275273
     
    17311729            'documentTitleTmpl' => $this->get_document_title_template(),
    17321730            'previewableDevices' => $this->get_previewable_devices(),
    1733             'selectiveRefreshEnabled' => isset( $this->selective_refresh ),
    17341731        );
    17351732
     
    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 */
  • trunk/src/wp-includes/class-wp-customize-nav-menus.php

    r36889 r37040  
    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(
     
    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        }
     
    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' ) );
     
    544544                    'theme_supports'    => 'menus',
    545545                    'type'              => 'theme_mod',
    546                     'transport'         => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     546                    'transport'         => 'postMessage',
    547547                    'default'           => 0,
    548548                ) );
     
    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
     
    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
     
    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' );
  • trunk/src/wp-includes/class-wp-customize-widgets.php

    r36842 r37040  
    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     *
     
    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
     
    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     *
     
    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     */
     
    691740                'moveWidgetArea'   => $move_widget_area_tpl,
    692741            ),
    693             'selectiveRefresh'     => isset( $this->manager->selective_refresh ),
     742            'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
    694743        );
    695744
     
    772821            'type'       => 'option',
    773822            'capability' => 'edit_theme_options',
    774             'transport'  => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    775823            'default'    => array(),
    776824        );
     
    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
     
    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'],
     
    10261076    public function customize_preview_enqueue() {
    10271077        wp_enqueue_script( 'customize-preview-widgets' );
     1078        wp_enqueue_style( 'customize-preview' );
    10281079    }
    10291080
     
    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(
     
    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 ) {
     
    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 ) ) {
     
    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' ) );
    15171570        add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
    1518     }
    1519 
    1520     /**
    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' );
    15341571    }
    15351572
  • trunk/src/wp-includes/class-wp-widget.php

    r36541 r37040  
    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
  • trunk/src/wp-includes/js/customize-preview-widgets.js

    r36586 r37040  
    1313        l10n: {
    1414            widgetTooltip: ''
    15         }
     15        },
     16        selectiveRefreshableWidgets: {}
    1617    };
    1718
     
    2526
    2627        self.preview = api.preview;
    27         if ( api.selectiveRefresh ) {
     28        if ( ! _.isEmpty( self.selectiveRefreshableWidgets ) ) {
    2829            self.addPartials();
    2930        }
     
    3940    };
    4041
    41     if ( api.selectiveRefresh ) {
    42 
    43         /**
    44          * Partial representing a widget instance.
    45          *
    46          * @class
    47          * @augments wp.customize.selectiveRefresh.Partial
    48          * @since 4.5.0
    49          */
    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                 }
    66 
    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 || {}
     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({
     50
     51        /**
     52         * Constructor.
     53         *
     54         * @since 4.5.0
     55         * @param {string} id - Partial ID.
     56         * @param {Object} options
     57         * @param {Object} options.params
     58         */
     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            }
     65
     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            );
     76
     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 );
     94            }
     95        },
     96
     97        /**
     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.
     123         *
     124         * @since 4.5.0
     125         * @param {string} id - Partial ID.
     126         * @param {Object} options
     127         * @param {Object} 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];
     136
     137            options = options || {};
     138            options.params = _.extend(
     139                {
     140                    settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
     141                },
     142                options.params || {}
     143            );
     144
     145            api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     146
     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        },
     154
     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 ) )
    77173                );
    78 
    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                 }
    94             }
    95         });
    96 
    97         /**
    98          * Partial representing a widget area.
    99          *
    100          * @class
    101          * @augments wp.customize.selectiveRefresh.Partial
    102          * @since 4.5.0
    103          */
    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 
    130                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
    131 
    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                 } );
    152 
    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 );
     174                if ( isAssignedWidgetPartial ) {
     175                    api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     176                }
     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;
     185                }
     186                widgetId = parsedId.idBase;
     187                if ( parsedId.number ) {
     188                    widgetId += '-' + String( parsedId.number );
     189                }
     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 );
    161229                    }
    162230                } );
    163 
    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;
     231            };
     232
     233            recursiveCommentTraversal( document.body.childNodes );
     234            return _.values( boundaryNodes );
     235        },
     236
     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
    170253                    }
    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;
     254                } );
     255            } );
     256        },
     257
     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        },
     280
     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;
     298                }
     299            } );
     300
     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;
    199317                            }
    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 );
     318                            lastPosition = thisPosition;
    214319                        }
    215320                    } );
    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
     321                } );
     322
     323                if ( needsSort ) {
     324                    _.each( sidebarWidgets, function( sidebarWidget ) {
     325                        sidebarPlacement.endNode.parentNode.insertBefore(
     326                            sidebarWidget.placement.container[0],
     327                            sidebarPlacement.endNode
     328                        );
     329
     330                        // @todo Rename partial-placement-moved?
     331                        api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
     332                    } );
     333
     334                    sortedSidebarContainers.push( sidebarPlacement );
     335                }
     336            } );
     337
     338            if ( sortedSidebarContainers.length > 0 ) {
     339                api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     340            }
     341
     342            return sortedSidebarContainers;
     343        },
     344
     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: {}
     359                } );
     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;
     366
     367                foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
     368                    return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
     369                } );
     370                if ( foundWidgetPlacement ) {
     371                    return;
     372                }
     373
     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
     392                } );
     393
     394                sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
     395                wasInserted = true;
     396            } );
     397
     398            if ( wasInserted ) {
     399                sidebarPartial.reflowWidgets();
     400            }
     401
     402            return widgetPartial;
     403        },
     404
     405        /**
     406         * Handle change to the sidebars_widgets[] setting.
     407         *
     408         * @since 4.5.0
     409         *
     410         * @param {Array} newWidgetIds New widget ids.
     411         * @param {Array} oldWidgetIds Old widget ids.
     412         */
     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();
    238437                        }
    239438                    } );
    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.' );
    255                 }
    256                 if ( ! api.has( settingId ) ) {
    257                     throw new Error( 'Setting does not exist.' );
    258                 }
    259                 widgetIds = api( settingId ).get();
    260                 if ( ! _.isArray( widgetIds ) ) {
    261                     throw new Error( 'Expected setting to be array of widget IDs' );
    262                 }
    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;
     439                }
     440            } );
     441
     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
    283497                    }
    284498                } );
    285 
    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                         } );
    318 
    319                         sortedSidebarContainers.push( sidebarPlacement );
    320                     }
    321                 } );
    322 
    323                 if ( sortedSidebarContainers.length > 0 ) {
    324                     api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
    325                 }
    326 
    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 );
    346                 }
    347 
    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 );
    354                     } );
    355                     if ( foundWidgetPlacement ) {
    356                         return;
    357                     }
    358 
    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                     } );
    378 
    379                     sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
    380                     wasInserted = true;
    381                 } );
    382 
    383                 if ( wasInserted ) {
    384                     sidebarPartial.reflowWidgets();
    385                 }
    386 
    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                 }
    409 
    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                 } );
    426 
    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 );
    432                 } );
    433 
    434                 _.each( addedWidgetPartials, function( widgetPartial ) {
    435                     widgetPartial.refresh();
    436                 } );
    437 
    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();
    451                 } );
    452 
    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                 }
    461 
    462                 return deferred.promise();
    463             }
    464         });
    465 
    466         api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
    467         api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;
    468 
    469         /**
    470          * Add partials for the registered widget areas (sidebars).
    471          *
    472          * @since 4.5.0
    473          */
    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
    482                         }
    483                     } );
    484                     api.selectiveRefresh.partial.add( partial.id, partial );
    485                 }
    486             } );
    487         };
    488 
    489     }
     499                api.selectiveRefresh.partial.add( partial.id, partial );
     500            }
     501        } );
     502    };
    490503
    491504    /**
  • trunk/src/wp-includes/js/customize-selective-refresh.js

    r36892 r37040  
    110110            var partial = this, selector;
    111111
    112             selector = partial.params.selector;
     112            selector = partial.params.selector || '';
    113113            if ( selector ) {
    114114                selector += ', ';
  • trunk/src/wp-includes/script-loader.php

    r36984 r37040  
    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 );
  • trunk/src/wp-includes/theme.php

    r36915 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-nav-menu-widget.php

    r36622 r37040  
    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    }
  • trunk/src/wp-includes/widgets/class-wp-widget-archives.php

    r34620 r37040  
    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    }
  • trunk/src/wp-includes/widgets/class-wp-widget-calendar.php

    r34619 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-categories.php

    r35278 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-links.php

    r34617 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-meta.php

    r34616 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-pages.php

    r34615 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-recent-comments.php

    r34622 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-recent-posts.php

    r34613 r37040  
    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    }
  • trunk/src/wp-includes/widgets/class-wp-widget-rss.php

    r35978 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-search.php

    r34611 r37040  
    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    }
  • trunk/src/wp-includes/widgets/class-wp-widget-tag-cloud.php

    r36622 r37040  
    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
  • trunk/src/wp-includes/widgets/class-wp-widget-text.php

    r36622 r37040  
    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
  • trunk/tests/phpunit/tests/customize/manager.php

    r36586 r37040  
    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'] );
  • trunk/tests/phpunit/tests/customize/widgets.php

    r36611 r37040  
    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 );
     
    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;
     
    7689        $this->assertInstanceOf( 'WP_Customize_Widgets', $this->manager->widgets );
    7790        $this->assertEquals( $this->manager, $this->manager->widgets->manager );
     91    }
     92
     93    /**
     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' ) );
    78150    }
    79151
     
    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 );
     195
     196        $default_args = array(
     197            'type' => 'option',
     198            'capability' => 'edit_theme_options',
     199            'transport' => 'refresh',
     200            'default' => array(),
     201            'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
     202            'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
     203        );
     204        $args = $this->manager->widgets->get_setting_args( 'widget_foo[2]' );
     205        foreach ( $default_args as $key => $default_value ) {
     206            $this->assertEquals( $default_value, $args[ $key ] );
     207        }
     208        $this->assertEquals( 'WIDGET_FOO[2]', $args['uppercase_id_set_by_filter'] );
    121209
    122210        $default_args = array(
     
    128216            'sanitize_js_callback' => array( $this->manager->widgets, 'sanitize_widget_js_instance' ),
    129217        );
    130 
    131         $args = $this->manager->widgets->get_setting_args( 'widget_foo[2]' );
     218        $args = $this->manager->widgets->get_setting_args( 'widget_search[2]' );
    132219        foreach ( $default_args as $key => $default_value ) {
    133220            $this->assertEquals( $default_value, $args[ $key ] );
    134221        }
    135         $this->assertEquals( 'WIDGET_FOO[2]', $args['uppercase_id_set_by_filter'] );
     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' );
    136227
    137228        $override_args = array(
     
    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
     
    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' ) ) );
     
    386481
    387482    /**
    388      * Test WP_Customize_Widgets::customize_preview_enqueue_deps().
    389      *
    390      * @see WP_Customize_Widgets::customize_preview_enqueue_deps()
    391      */
    392     function test_customize_preview_enqueue_deps() {
    393         $this->manager->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().
     498     *
     499     * @see WP_Customize_Widgets::customize_preview_enqueue()
     500     */
     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' ) );
     
    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
Note: See TracChangeset for help on using the changeset viewer.