Make WordPress Core

Ticket #27355: 27355.4.diff

File 27355.4.diff, 179.9 KB (added by westonruter, 9 years ago)

https://github.com/xwp/wordpress-develop/compare/6966c1426...86cea96b

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

    diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
    index 0a61a86..270770d 100644
     
    37863786                        });
    37873787                });
    37883788
     3789                // Focus on the control that is associated with the given setting.
     3790                api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
     3791                        var matchedControl;
     3792                        api.control.each( function( control ) {
     3793                                var settingIds = _.pluck( control.settings, 'id' );
     3794                                if ( -1 !== _.indexOf( settingIds, settingId ) ) {
     3795                                        matchedControl = control;
     3796                                }
     3797                        } );
     3798
     3799                        if ( matchedControl ) {
     3800                                matchedControl.focus();
     3801                        }
     3802                } );
     3803
     3804                // Refresh the preview when it requests.
     3805                api.previewer.bind( 'refresh', function() {
     3806                        api.previewer.refresh();
     3807                });
     3808
    37893809                api.trigger( 'ready' );
    37903810
    37913811                // Make sure left column gets focus
  • src/wp-admin/js/customize-nav-menus.js

    diff --git src/wp-admin/js/customize-nav-menus.js src/wp-admin/js/customize-nav-menus.js
    index 3bcbb2c..066ec0d 100644
     
    1919        api.Menus.data = {
    2020                itemTypes: [],
    2121                l10n: {},
    22                 menuItemTransport: 'postMessage',
     22                settingTransport: 'refresh',
    2323                phpIntMax: 0,
    2424                defaultSettingValues: {
    2525                        nav_menu: {},
     
    23102310                        customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
    23112311                        settingArgs = {
    23122312                                type: 'nav_menu_item',
    2313                                 transport: 'postMessage',
     2313                                transport: api.Menus.data.settingTransport,
    23142314                                previewer: api.previewer
    23152315                        };
    23162316                        setting = api.create( customizeId, customizeId, {}, settingArgs );
     
    23992399                        // Register the menu control setting.
    24002400                        api.create( customizeId, customizeId, {}, {
    24012401                                type: 'nav_menu',
    2402                                 transport: 'postMessage',
     2402                                transport: api.Menus.data.settingTransport,
    24032403                                previewer: api.previewer
    24042404                        } );
    24052405                        api( customizeId ).set( $.extend(
     
    24862486                        }
    24872487                } );
    24882488
    2489                 api.previewer.bind( 'refresh', function() {
    2490                         api.previewer.refresh();
    2491                 });
    2492 
    24932489                // Open and focus menu control.
    24942490                api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl );
    24952491        } );
     
    25352531                                newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
    25362532                                newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
    25372533                                        type: 'nav_menu',
    2538                                         transport: 'postMessage',
     2534                                        transport: api.Menus.data.settingTransport,
    25392535                                        previewer: api.previewer
    25402536                                } );
    25412537
     
    26832679                                newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
    26842680                                newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
    26852681                                        type: 'nav_menu_item',
    2686                                         transport: 'postMessage',
     2682                                        transport: api.Menus.data.settingTransport,
    26872683                                        previewer: api.previewer
    26882684                                } );
    26892685
  • src/wp-admin/js/customize-widgets.js

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

    diff --git src/wp-content/themes/twentythirteen/js/theme-customizer.js src/wp-content/themes/twentythirteen/js/theme-customizer.js
    index 6072104..8519752 100644
     
    3838                        }
    3939                } );
    4040        } );
     41
     42        if ( wp.customize.selectiveRefresh ) {
     43                wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) {
     44                        var widgetArea;
     45                        if ( 'sidebar-1' === sidebarPartial.sidebarId && $.isFunction( $.fn.masonry ) ) {
     46                                widgetArea = $( '#secondary .widget-area' );
     47                                widgetArea.masonry( 'destroy' );
     48                                widgetArea.masonry();
     49                        }
     50                } );
     51        }
     52
    4153} )( jQuery );
  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index 942d907..937689b 100644
    final class WP_Customize_Manager { 
    6767        public $nav_menus;
    6868
    6969        /**
     70         * Methods and properties dealing with selective refresh in the Customizer preview.
     71         *
     72         * @since 4.5.0
     73         * @access public
     74         * @var WP_Customize_Selective_Refresh
     75         */
     76        public $selective_refresh;
     77
     78        /**
    7079         * Registered instances of WP_Customize_Setting.
    7180         *
    7281         * @since 3.4.0
    final class WP_Customize_Manager { 
    100109         * @access protected
    101110         * @var array
    102111         */
    103         protected $components = array( 'widgets', 'nav_menus' );
     112        protected $components = array( 'widgets', 'nav_menus', 'selective_refresh' );
    104113
    105114        /**
    106115         * Registered instances of WP_Customize_Section.
    final class WP_Customize_Manager { 
    249258                 */
    250259                $components = apply_filters( 'customize_loaded_components', $this->components, $this );
    251260
    252                 if ( in_array( 'widgets', $components ) ) {
     261                if ( in_array( 'widgets', $components, true ) ) {
    253262                        require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
    254263                        $this->widgets = new WP_Customize_Widgets( $this );
    255264                }
    256                 if ( in_array( 'nav_menus', $components ) ) {
     265
     266                if ( in_array( 'nav_menus', $components, true ) ) {
    257267                        require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
    258268                        $this->nav_menus = new WP_Customize_Nav_Menus( $this );
    259269                }
    260270
     271                if ( in_array( 'selective_refresh', $components, true ) ) {
     272                        require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
     273                        $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
     274                }
     275
    261276                add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
    262277
    263278                add_action( 'setup_theme', array( $this, 'setup_theme' ) );
    final class WP_Customize_Manager { 
    17111726                        'autofocus' => array(),
    17121727                        'documentTitleTmpl' => $this->get_document_title_template(),
    17131728                        'previewableDevices' => $this->get_previewable_devices(),
     1729                        'selectiveRefreshEnabled' => isset( $this->selective_refresh ),
    17141730                );
    17151731
    17161732                // Prepare Customize Section objects to pass to JavaScript.
  • src/wp-includes/class-wp-customize-nav-menus.php

    diff --git src/wp-includes/class-wp-customize-nav-menus.php src/wp-includes/class-wp-customize-nav-menus.php
    index 8d2b6d5..2438432 100644
    final class WP_Customize_Nav_Menus { 
    6161                add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
    6262                add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
    6363                add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
     64
     65                // Selective Refresh partials.
     66                add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
    6467        }
    6568
    6669        /**
    final class WP_Customize_Nav_Menus { 
    375378                                'reorderLabelOn'    => esc_attr__( 'Reorder menu items' ),
    376379                                'reorderLabelOff'   => esc_attr__( 'Close reorder mode' ),
    377380                        ),
    378                         'menuItemTransport'    => 'postMessage',
     381                        'settingTransport'     => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    379382                        'phpIntMax'            => PHP_INT_MAX,
    380383                        'defaultSettingValues' => array(
    381384                                'nav_menu'      => $temp_nav_menu_setting->default,
    final class WP_Customize_Nav_Menus { 
    426429        public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
    427430                if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
    428431                        $setting_args = array(
    429                                 'type' => WP_Customize_Nav_Menu_Setting::TYPE,
     432                                'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
     433                                'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    430434                        );
    431435                } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
    432436                        $setting_args = array(
    433                                 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
     437                                'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
     438                                'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    434439                        );
    435440                }
    436441                return $setting_args;
    final class WP_Customize_Nav_Menus { 
    515520
    516521                        $setting = $this->manager->get_setting( $setting_id );
    517522                        if ( $setting ) {
    518                                 $setting->transport = 'postMessage';
     523                                $setting->transport = isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh';
    519524                                remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
    520525                                add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
    521526                        } else {
    final class WP_Customize_Nav_Menus { 
    523528                                        'sanitize_callback' => array( $this, 'intval_base10' ),
    524529                                        'theme_supports'    => 'menus',
    525530                                        'type'              => 'theme_mod',
    526                                         'transport'         => 'postMessage',
     531                                        'transport'         => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    527532                                        'default'           => 0,
    528533                                ) );
    529534                        }
    final class WP_Customize_Nav_Menus { 
    549554                        ) ) );
    550555
    551556                        $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
    552                         $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
     557                        $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array(
     558                                'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
     559                        ) ) );
    553560
    554561                        // Add the menu contents.
    555562                        $menu_items = (array) wp_get_nav_menu_items( $menu_id );
    final class WP_Customize_Nav_Menus { 
    562569                                $value = (array) $item;
    563570                                $value['nav_menu_term_id'] = $menu_id;
    564571                                $this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
    565                                         'value' => $value,
     572                                        'value'     => $value,
     573                                        'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    566574                                ) ) );
    567575
    568576                                // Create a control for each menu item.
    final class WP_Customize_Nav_Menus { 
    586594                $this->manager->add_setting( 'new_menu_name', array(
    587595                        'type'      => 'new_menu',
    588596                        'default'   => '',
    589                         'transport' => 'postMessage',
     597                        'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    590598                ) );
    591599
    592600                $this->manager->add_control( 'new_menu_name', array(
    final class WP_Customize_Nav_Menus { 
    802810        <?php
    803811        }
    804812
     813        //
    805814        // Start functionality specific to partial-refresh of menu changes in Customizer preview.
    806         const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
    807         const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
    808         const RENDER_QUERY_VAR = 'wp_customize_menu_render';
     815        //
    809816
    810817        /**
    811          * The number of wp_nav_menu() calls which have happened in the preview.
     818         * Filters arguments for dynamic nav_menu selective refresh partials.
    812819         *
    813          * @since 4.3.0
     820         * @since 4.5.0
    814821         * @access public
    815          * @var int
    816          */
    817         public $preview_nav_menu_instance_number = 0;
    818 
    819         /**
    820          * Nav menu args used for each instance.
    821822         *
    822          * @since 4.3.0
    823          * @access public
    824          * @var array
     823         * @param array|false $partial_args Partial args.
     824         * @param string      $partial_id   Partial ID.
     825         * @return array Partial args
    825826         */
    826         public $preview_nav_menu_instance_args = array();
     827        public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
     828
     829                if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
     830                        if ( false === $partial_args ) {
     831                                $partial_args = array();
     832                        }
     833                        $partial_args = array_merge(
     834                                $partial_args,
     835                                array(
     836                                        'type'                => 'nav_menu_instance',
     837                                        'render_callback'     => array( $this, 'render_nav_menu_partial' ),
     838                                        'container_inclusive' => true,
     839                                )
     840                        );
     841                }
     842
     843                return $partial_args;
     844        }
    827845
    828846        /**
    829847         * Add hooks for the Customizer preview.
    final class WP_Customize_Nav_Menus { 
    832850         * @access public
    833851         */
    834852        public function customize_preview_init() {
    835                 add_action( 'template_redirect', array( $this, 'render_menu' ) );
    836853                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
    837 
    838                 if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
    839                         add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
    840                         add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
    841                 }
     854                add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
     855                add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
    842856        }
    843857
    844858        /**
    final class WP_Customize_Nav_Menus { 
    846860         *
    847861         * @since 4.3.0
    848862         * @access public
    849          *
    850863         * @see wp_nav_menu()
     864         * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params()
    851865         *
    852866         * @param array $args An array containing wp_nav_menu() arguments.
    853867         * @return array Arguments.
    854868         */
    855869        public function filter_wp_nav_menu_args( $args ) {
    856                 $this->preview_nav_menu_instance_number += 1;
    857                 $args['instance_number'] = $this->preview_nav_menu_instance_number;
    858 
    859                 $can_partial_refresh = (
     870                /*
     871                 * The following conditions determine whether or not this instance of
     872                 * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
     873                 * selective refreshed if...
     874                 */
     875                $can_selective_refresh = (
     876                        // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
    860877                        ! empty( $args['echo'] )
    861878                        &&
     879                        // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
    862880                        ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
    863881                        &&
     882                        // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
    864883                        ( empty( $args['walker'] ) || is_string( $args['walker'] ) )
    865                         &&
    866                         (
     884                        // ...and if it has a theme location assigned or an assigned menu to display,
     885                        && (
    867886                                ! empty( $args['theme_location'] )
    868887                                ||
    869888                                ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
    870889                        )
     890                        &&
     891                        // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
     892                        (
     893                                ! empty( $args['container'] )
     894                                ||
     895                                ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
     896                        )
    871897                );
    872                 $args['can_partial_refresh'] = $can_partial_refresh;
    873 
    874                 $hashed_args = $args;
    875898
    876                 if ( ! $can_partial_refresh ) {
    877                         $hashed_args['fallback_cb'] = '';
    878                         $hashed_args['walker'] = '';
     899                if ( ! $can_selective_refresh ) {
     900                        return $args;
    879901                }
    880902
    881                 // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
    882                 if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
    883                         $hashed_args['menu'] = $hashed_args['menu']->term_id;
     903                $exported_args = $args;
     904
     905                /*
     906                 * Replace object menu arg with a term_id menu arg, as this exports better
     907                 * to JS and is easier to compare hashes.
     908                 */
     909                if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
     910                        $exported_args['menu'] = $exported_args['menu']->term_id;
    884911                }
    885912
    886                 ksort( $hashed_args );
    887                 $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
     913                ksort( $exported_args );
     914                $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
     915
     916                $args['customize_preview_nav_menus_args'] = $exported_args;
    888917
    889                 $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
    890918                return $args;
    891919        }
    892920
    893921        /**
    894          * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
     922         * Prepares wp_nav_menu() calls for partial refresh.
     923         *
     924         * Injects attributes into container element.
    895925         *
    896926         * @since 4.3.0
    897927         * @access public
    final class WP_Customize_Nav_Menus { 
    903933         * @return null
    904934         */
    905935        public function filter_wp_nav_menu( $nav_menu_content, $args ) {
    906                 if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
    907                         $nav_menu_content = preg_replace(
    908                                 '/(?<=class=")/',
    909                                 sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
    910                                 $nav_menu_content,
    911                                 1 // Only update the class on the first element found, the menu container.
    912                         );
     936                if ( ! empty( $args->customize_preview_nav_menus_args ) ) {
     937                        $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
     938                        $attributes .= ' data-customize-partial-type="nav_menu_instance"';
     939                        $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
     940                        $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 );
    913941                }
    914942                return $nav_menu_content;
    915943        }
    916944
    917945        /**
    918          * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
    919          * are not tampered with when submitted in the Ajax request.
     946         * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
     947         * submitted in the Ajax request.
     948         *
     949         * Note that the array is expected to be pre-sorted.
    920950         *
    921951         * @since 4.3.0
    922952         * @access public
    923953         *
    924954         * @param array $args The arguments to hash.
    925          * @return string
     955         * @return string Hashed nav menu arguments.
    926956         */
    927957        public function hash_nav_menu_args( $args ) {
    928                 return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
     958                return wp_hash( serialize( $args ) );
    929959        }
    930960
    931961        /**
    final class WP_Customize_Nav_Menus { 
    935965         * @access public
    936966         */
    937967        public function customize_preview_enqueue_deps() {
    938                 wp_enqueue_script( 'customize-preview-nav-menus' );
    939                 wp_enqueue_style( 'customize-preview' );
     968                if ( isset( $this->manager->selective_refresh ) ) {
     969                        $script = wp_scripts()->registered['customize-preview-nav-menus'];
     970                        $script->deps[] = 'customize-selective-refresh';
     971                }
    940972
    941                 add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
     973                wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
     974                wp_enqueue_style( 'customize-preview' );
    942975        }
    943976
    944977        /**
    945          * Export data from PHP to JS.
     978         * Exports data from PHP to JS.
    946979         *
    947980         * @since 4.3.0
     981         * @deprecated 4.5.0 Obsolete
    948982         * @access public
    949983         */
    950984        public function export_preview_data() {
    951 
    952                 // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
    953                 $exports = array(
    954                         'renderQueryVar'        => self::RENDER_QUERY_VAR,
    955                         'renderNonceValue'      => wp_create_nonce( self::RENDER_AJAX_ACTION ),
    956                         'renderNoncePostKey'    => self::RENDER_NONCE_POST_KEY,
    957                         'navMenuInstanceArgs'   => $this->preview_nav_menu_instance_args,
    958                         'l10n'                  => array(
    959                                 'editNavMenuItemTooltip' => __( 'Shift-click to edit this menu item.' ),
    960                         ),
    961                 );
    962 
    963                 printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
     985                _deprecated_function( __METHOD__, '4.5.0' );
    964986        }
    965987
    966988        /**
    final class WP_Customize_Nav_Menus { 
    970992         * @access public
    971993         *
    972994         * @see wp_nav_menu()
     995         *
     996         * @param WP_Customize_Partial $partial       Partial.
     997         * @param array                $nav_menu_args Nav menu args supplied as container context.
     998         * @return string|false
    973999         */
    974         public function render_menu() {
    975                 if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
    976                         return;
    977                 }
    978 
    979                 $this->manager->remove_preview_signature();
    980 
    981                 if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
    982                         wp_send_json_error( 'missing_nonce_param' );
    983                 }
    984 
    985                 if ( ! is_customize_preview() ) {
    986                         wp_send_json_error( 'expected_customize_preview' );
    987                 }
     1000        public function render_nav_menu_partial( $partial, $nav_menu_args ) {
     1001                unset( $partial );
    9881002
    989                 if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
    990                         wp_send_json_error( 'nonce_check_fail' );
     1003                if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
     1004                        // Error: missing_args_hmac.
     1005                        return false;
    9911006                }
    9921007
    993                 if ( ! current_user_can( 'edit_theme_options' ) ) {
    994                         wp_send_json_error( 'unauthorized' );
    995                 }
    996 
    997                 if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
    998                         wp_send_json_error( 'missing_param' );
    999                 }
     1008                $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
     1009                unset( $nav_menu_args['args_hmac'] );
    10001010
    1001                 if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
    1002                         wp_send_json_error( 'missing_param' );
     1011                ksort( $nav_menu_args );
     1012                if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
     1013                        // Error: args_hmac_mismatch.
     1014                        return false;
    10031015                }
    10041016
    1005                 $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
    1006                 if ( ! is_array( $wp_nav_menu_args ) ) {
    1007                         wp_send_json_error( 'wp_nav_menu_args_not_array' );
    1008                 }
    1009 
    1010                 $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
    1011                 if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
    1012                         wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
    1013                 }
     1017                ob_start();
     1018                wp_nav_menu( $nav_menu_args );
     1019                $content = ob_get_clean();
    10141020
    1015                 $wp_nav_menu_args['echo'] = false;
    1016                 wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
     1021                return $content;
    10171022        }
    10181023}
  • src/wp-includes/class-wp-customize-widgets.php

    diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
    index 5a0e62b..28eaf12 100644
    final class WP_Customize_Widgets { 
    100100                add_action( 'dynamic_sidebar',                         array( $this, 'tally_rendered_widgets' ) );
    101101                add_filter( 'is_active_sidebar',                       array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
    102102                add_filter( 'dynamic_sidebar_has_widgets',             array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
     103
     104                // Selective Refresh.
     105                add_filter( 'customize_dynamic_partial_args',          array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
     106                add_action( 'customize_preview_init',                  array( $this, 'selective_refresh_init' ) );
    103107        }
    104108
    105109        /**
    final class WP_Customize_Widgets { 
    682686                                'widgetReorderNav' => $widget_reorder_nav_tpl,
    683687                                'moveWidgetArea'   => $move_widget_area_tpl,
    684688                        ),
     689                        'selectiveRefresh'     => isset( $this->manager->selective_refresh ),
    685690                );
    686691
    687692                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    final class WP_Customize_Widgets { 
    762767                $args = array(
    763768                        'type'       => 'option',
    764769                        'capability' => 'edit_theme_options',
    765                         'transport'  => 'refresh',
     770                        'transport'  => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    766771                        'default'    => array(),
    767772                );
    768773
    final class WP_Customize_Widgets { 
    884889                                'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
    885890                                'is_disabled'  => $is_disabled,
    886891                                'id_base'      => $id_base,
    887                                 'transport'    => 'refresh',
     892                                'transport'    => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    888893                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
    889894                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
    890895                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
    final class WP_Customize_Widgets { 
    10611066                        'registeredSidebars' => array_values( $wp_registered_sidebars ),
    10621067                        'registeredWidgets'  => $wp_registered_widgets,
    10631068                        'l10n'               => array(
    1064                                 'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
     1069                                'widgetTooltip'  => __( 'Shift-click to edit this widget.' ),
    10651070                        ),
     1071                        'selectiveRefresh'   => isset( $this->manager->selective_refresh ),
    10661072                );
    10671073                foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    10681074                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
    final class WP_Customize_Widgets { 
    14591465                wp_send_json_success( compact( 'form', 'instance' ) );
    14601466        }
    14611467
    1462         /***************************************************************************
    1463          * Option Update Capturing
    1464          ***************************************************************************/
     1468        /*
     1469         * Selective Refresh Methods
     1470         */
     1471
     1472        /**
     1473         * Filter args for dynamic widget partials.
     1474         *
     1475         * @since 4.5.0
     1476         *
     1477         * @param array|false $partial_args Partial args.
     1478         * @param string      $partial_id  Partial ID.
     1479         * @return array Partial args
     1480         */
     1481        public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
     1482
     1483                if ( preg_match( '/^widget\[.+\]$/', $partial_id ) ) {
     1484                        if ( false === $partial_args ) {
     1485                                $partial_args = array();
     1486                        }
     1487                        $partial_args = array_merge(
     1488                                $partial_args,
     1489                                array(
     1490                                        'type' => 'widget',
     1491                                        'render_callback' => array( $this, 'render_widget_partial' ),
     1492                                        'container_inclusive' => true,
     1493                                )
     1494                        );
     1495                }
     1496
     1497                return $partial_args;
     1498        }
     1499
     1500        /**
     1501         * Add hooks for selective refresh.
     1502         *
     1503         * @since 4.5.0
     1504         * @access public
     1505         */
     1506        public function selective_refresh_init() {
     1507                if ( ! isset( $this->manager->selective_refresh ) ) {
     1508                        return;
     1509                }
     1510
     1511                add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
     1512                add_filter( 'dynamic_sidebar_params', array( $this, 'filter_dynamic_sidebar_params' ) );
     1513                add_filter( 'wp_kses_allowed_html', array( $this, 'filter_wp_kses_allowed_data_attributes' ) );
     1514                add_action( 'dynamic_sidebar_before', array( $this, 'start_dynamic_sidebar' ) );
     1515                add_action( 'dynamic_sidebar_after', array( $this, 'end_dynamic_sidebar' ) );
     1516        }
     1517
     1518        /**
     1519         * Enqueue scripts for the Customizer preview.
     1520         *
     1521         * @since 4.5.0
     1522         * @access public
     1523         */
     1524        public function customize_preview_enqueue_deps() {
     1525                if ( isset( $this->manager->selective_refresh ) ) {
     1526                        $script = wp_scripts()->registered['customize-preview-widgets'];
     1527                        $script->deps[] = 'customize-selective-refresh';
     1528                }
     1529
     1530                wp_enqueue_script( 'customize-preview-widgets' );
     1531                wp_enqueue_style( 'customize-preview' );
     1532        }
     1533
     1534        /**
     1535         * Inject selective refresh data attributes into widget container elements.
     1536         *
     1537         * @param array $params {
     1538         *     Dynamic sidebar params.
     1539         *
     1540         *     @type array $args        Sidebar args.
     1541         *     @type array $widget_args Widget args.
     1542         * }
     1543         * @see WP_Customize_Nav_Menus_Partial_Refresh::filter_wp_nav_menu_args()
     1544         *
     1545         * @return array Params.
     1546         */
     1547        public function filter_dynamic_sidebar_params( $params ) {
     1548                $sidebar_args = array_merge(
     1549                        array(
     1550                                'before_widget' => '',
     1551                                'after_widget' => '',
     1552                        ),
     1553                        $params[0]
     1554                );
     1555
     1556                // Skip widgets not in a registered sidebar or ones which lack a proper wrapper element to attach the data-* attributes to.
     1557                $matches = array();
     1558                $is_valid = (
     1559                        isset( $sidebar_args['id'] )
     1560                        &&
     1561                        is_registered_sidebar( $sidebar_args['id'] )
     1562                        &&
     1563                        ( isset( $this->current_dynamic_sidebar_id_stack[0] ) && $this->current_dynamic_sidebar_id_stack[0] === $sidebar_args['id'] )
     1564                        &&
     1565                        preg_match( '#^<(?P<tag_name>\w+)#', $sidebar_args['before_widget'], $matches )
     1566                );
     1567                if ( ! $is_valid ) {
     1568                        return $params;
     1569                }
     1570                $this->before_widget_tags_seen[ $matches['tag_name'] ] = true;
     1571
     1572                $context = array(
     1573                        'sidebar_id' => $sidebar_args['id'],
     1574                );
     1575                if ( isset( $this->context_sidebar_instance_number ) ) {
     1576                        $context['sidebar_instance_number'] = $this->context_sidebar_instance_number;
     1577                } else if ( isset( $sidebar_args['id'] ) && isset( $this->sidebar_instance_count[ $sidebar_args['id'] ] ) ) {
     1578                        $context['sidebar_instance_number'] = $this->sidebar_instance_count[ $sidebar_args['id'] ];
     1579                }
     1580
     1581                $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'widget[' . $sidebar_args['widget_id'] . ']' ) );
     1582                $attributes .= ' data-customize-partial-type="widget"';
     1583                $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $context ) ) );
     1584                $attributes .= sprintf( ' data-customize-widget-id="%s"', esc_attr( $sidebar_args['widget_id'] ) );
     1585                $sidebar_args['before_widget'] = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $sidebar_args['before_widget'] );
     1586
     1587                $params[0] = $sidebar_args;
     1588                return $params;
     1589        }
     1590
     1591        /**
     1592         * List of the tag names seen for before_widget strings.
     1593         *
     1594         * This is used in the filter_wp_kses_allowed_html filter to ensure that the
     1595         * data-* attributes can be whitelisted.
     1596         *
     1597         * @since 4.5.0
     1598         * @access private
     1599         * @var array
     1600         */
     1601        protected $before_widget_tags_seen = array();
     1602
     1603        /**
     1604         * Ensure that the HTML data-* attributes for selective refresh are allowed by kses.
     1605         *
     1606         * This is needed in case the $before_widget is run through wp_kses() when printed.
     1607         *
     1608         * @since 4.5.0
     1609         * @access public
     1610         *
     1611         * @param array $allowed_html Allowed HTML.
     1612         * @return array Allowed HTML.
     1613         */
     1614        public function filter_wp_kses_allowed_data_attributes( $allowed_html ) {
     1615                foreach ( array_keys( $this->before_widget_tags_seen ) as $tag_name ) {
     1616                        if ( ! isset( $allowed_html[ $tag_name ] ) ) {
     1617                                $allowed_html[ $tag_name ] = array();
     1618                        }
     1619                        $allowed_html[ $tag_name ] = array_merge(
     1620                                $allowed_html[ $tag_name ],
     1621                                array_fill_keys( array(
     1622                                        'data-customize-partial-id',
     1623                                        'data-customize-partial-type',
     1624                                        'data-customize-partial-placement-context',
     1625                                        'data-customize-partial-widget-id',
     1626                                        'data-customize-partial-options',
     1627                                ), true )
     1628                        );
     1629                }
     1630                return $allowed_html;
     1631        }
     1632
     1633        /**
     1634         * Keep track of the number of times that dynamic_sidebar() was called for a given sidebar index.
     1635         *
     1636         * This helps facilitate the uncommon scenario where a single sidebar is rendered multiple times on a template.
     1637         *
     1638         * @since 4.5.0
     1639         * @access private
     1640         * @var array
     1641         */
     1642        protected $sidebar_instance_count = array();
     1643
     1644        /**
     1645         * The current request's sidebar_instance_number context.
     1646         *
     1647         * @since 4.5.0
     1648         * @access private
     1649         * @var int
     1650         */
     1651        protected $context_sidebar_instance_number;
     1652
     1653        /**
     1654         * Current sidebar ID being rendered.
     1655         *
     1656         * @since 4.5.0
     1657         * @access private
     1658         * @var array
     1659         */
     1660        protected $current_dynamic_sidebar_id_stack = array();
     1661
     1662        /**
     1663         * Start keeping track of the current sidebar being rendered.
     1664         *
     1665         * Insert marker before widgets are rendered in a dynamic sidebar.
     1666         *
     1667         * @since 4.5.0
     1668         *
     1669         * @param int|string $index Index, name, or ID of the dynamic sidebar.
     1670         */
     1671        public function start_dynamic_sidebar( $index ) {
     1672                array_unshift( $this->current_dynamic_sidebar_id_stack, $index );
     1673                if ( ! isset( $this->sidebar_instance_count[ $index ] ) ) {
     1674                        $this->sidebar_instance_count[ $index ] = 0;
     1675                }
     1676                $this->sidebar_instance_count[ $index ] += 1;
     1677                if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
     1678                        printf( "\n<!--dynamic_sidebar_before:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
     1679                }
     1680        }
     1681
     1682        /**
     1683         * Finish keeping track of the current sidebar being rendered.
     1684         *
     1685         * Insert marker after widgets are rendered in a dynamic sidebar.
     1686         *
     1687         * @since 4.5.0
     1688         *
     1689         * @param int|string $index Index, name, or ID of the dynamic sidebar.
     1690         */
     1691        public function end_dynamic_sidebar( $index ) {
     1692                if ( ! $this->manager->selective_refresh->is_render_partials_request() ) {
     1693                        printf( "\n<!--dynamic_sidebar_after:%s:%d-->\n", esc_html( $index ), intval( $this->sidebar_instance_count[ $index ] ) );
     1694                }
     1695        }
     1696
     1697        /**
     1698         * Current sidebar being rendered.
     1699         *
     1700         * @since 4.5.0
     1701         * @access private
     1702         * @var string
     1703         */
     1704        protected $rendering_widget_id;
     1705
     1706        /**
     1707         * Current widget being rendered.
     1708         *
     1709         * @since 4.5.0
     1710         * @access private
     1711         * @var string
     1712         */
     1713        protected $rendering_sidebar_id;
     1714
     1715        /**
     1716         * Filter sidebars_widgets to ensure the currently-rendered widget is the only widget in the current sidebar.
     1717         *
     1718         * @since 4.5.0
     1719         * @access private
     1720         *
     1721         * @param array $sidebars_widgets Sidebars widgets.
     1722         * @return array Sidebars widgets.
     1723         */
     1724        public function filter_sidebars_widgets_for_rendering_widget( $sidebars_widgets ) {
     1725                $sidebars_widgets[ $this->rendering_sidebar_id ] = array( $this->rendering_widget_id );
     1726                return $sidebars_widgets;
     1727        }
     1728
     1729        /**
     1730         * Render a specific widget using the supplied sidebar arguments.
     1731         *
     1732         * @since 4.5.0
     1733         * @access public
     1734         *
     1735         * @see dynamic_sidebar()
     1736         *
     1737         * @param WP_Customize_Partial $partial      Partial.
     1738         * @param array                $context {
     1739         *     Sidebar args supplied as container context.
     1740         *
     1741         *     @type string $sidebar_id                ID for sidebar for widget to render into.
     1742         *     @type int    [$sidebar_instance_number] Disambiguating instance number.
     1743         * }
     1744         * @return string|false
     1745         */
     1746        public function render_widget_partial( $partial, $context ) {
     1747                $id_data   = $partial->id_data();
     1748                $widget_id = array_shift( $id_data['keys'] );
     1749
     1750                if ( ! is_array( $context )
     1751                        || empty( $context['sidebar_id'] )
     1752                        || ! is_registered_sidebar( $context['sidebar_id'] )
     1753                ) {
     1754                        return false;
     1755                }
     1756
     1757                $this->rendering_sidebar_id = $context['sidebar_id'];
     1758
     1759                if ( isset( $context['sidebar_instance_number'] ) ) {
     1760                        $this->context_sidebar_instance_number = intval( $context['sidebar_instance_number'] );
     1761                }
     1762
     1763                // Filter sidebars_widgets so that only the queried widget is in the sidebar.
     1764                $this->rendering_widget_id = $widget_id;
     1765
     1766                $filter_callback = array( $this, 'filter_sidebars_widgets_for_rendering_widget' );
     1767                add_filter( 'sidebars_widgets', $filter_callback, 1000 );
     1768
     1769                // Render the widget.
     1770                ob_start();
     1771                dynamic_sidebar( $this->rendering_sidebar_id = $context['sidebar_id'] );
     1772                $container = ob_get_clean();
     1773
     1774                // Reset variables for next partial render.
     1775                remove_filter( 'sidebars_widgets', $filter_callback, 1000 );
     1776
     1777                $this->context_sidebar_instance_number = null;
     1778                $this->rendering_sidebar_id = null;
     1779                $this->rendering_widget_id = null;
     1780
     1781                return $container;
     1782        }
     1783
     1784        //
     1785        // Option Update Capturing
     1786        //
    14651787
    14661788        /**
    14671789         * List of captured widget option updates.
    final class WP_Customize_Widgets { 
    16111933                        return;
    16121934                }
    16131935
    1614                 remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
     1936                remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10 );
    16151937
    16161938                foreach ( array_keys( $this->_captured_options ) as $option_name ) {
    16171939                        remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
  • src/wp-includes/css/customize-preview.css

    diff --git src/wp-includes/css/customize-preview.css src/wp-includes/css/customize-preview.css
    index bc4a6fe..75251ea 100644
     
    44        transition: opacity 0.25s;
    55        cursor: progress;
    66}
     7
     8/* Override highlight when refreshing */
     9.customize-partial-refreshing.widget-customizer-highlighted-widget {
     10        -webkit-box-shadow: none;
     11        box-shadow: none;
     12}
  • src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php

    diff --git src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
    index 073423e..b89b56c 100644
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    6767         * Default transport.
    6868         *
    6969         * @since 4.3.0
     70         * @since 4.5.0 Default changed to 'refresh'
    7071         * @access public
    7172         * @var string
    7273         */
    73         public $transport = 'postMessage';
     74        public $transport = 'refresh';
    7475
    7576        /**
    7677         * The post ID represented by this setting instance. This is the db_id.
  • new file src/wp-includes/customize/class-wp-customize-partial.php

    diff --git src/wp-includes/customize/class-wp-customize-partial.php src/wp-includes/customize/class-wp-customize-partial.php
    new file mode 100644
    index 0000000..f6e5e44
    - +  
     1<?php
     2/**
     3 * WordPress Customize Partial class
     4 *
     5 * @package WordPress
     6 * @subpackage Customize
     7 * @since 4.5.0
     8 */
     9
     10/**
     11 * Customize Partial class.
     12 *
     13 * Representation of a rendered region in the previewed page that gets
     14 * selectively refreshed when an associated setting is changed.
     15 * This class is analogous of WP_Customize_Control.
     16 *
     17 * @since 4.5.0
     18 */
     19class WP_Customize_Partial {
     20
     21        /**
     22         * Component.
     23         *
     24         * @since 4.5.0
     25         * @access public
     26         * @var WP_Customize_Selective_Refresh
     27         */
     28        public $component;
     29
     30        /**
     31         * Unique identifier for the partial.
     32         *
     33         * If the partial is used to display a single setting, this would generally
     34         * be the same as the associated setting's ID.
     35         *
     36         * @since 4.5.0
     37         * @access public
     38         * @var string
     39         */
     40        public $id;
     41
     42        /**
     43         * Parsed ID.
     44         *
     45         * @since 4.5.0
     46         * @access private
     47         * @var array {
     48         *     @type string $base ID base.
     49         *     @type array  $keys Keys for multidimensional.
     50         * }
     51         */
     52        protected $id_data = array();
     53
     54        /**
     55         * Type of this partial.
     56         *
     57         * @since 4.5.0
     58         * @access public
     59         * @var string
     60         */
     61        public $type = 'default';
     62
     63        /**
     64         * The jQuery selector to find the container element for the partial.
     65         *
     66         * @since 4.5.0
     67         * @access public
     68         * @var string
     69         */
     70        public $selector;
     71
     72        /**
     73         * All settings tied to the partial.
     74         *
     75         * @access public
     76         * @since 4.5.0
     77         * @var WP_Customize_Setting[]
     78         */
     79        public $settings;
     80
     81        /**
     82         * The ID for the setting that this partial is primarily responsible for rendering.
     83         *
     84         * If not supplied, it will default to the ID of the first setting.
     85         *
     86         * @since 4.5.0
     87         * @access public
     88         * @var string
     89         */
     90        public $primary_setting;
     91
     92        /**
     93         * Render callback.
     94         *
     95         * @since 4.5.0
     96         * @access public
     97         * @see WP_Customize_Partial::render()
     98         * @var callable Callback is called with one argument, the instance of
     99         *                 WP_Customize_Partial. The callback can either echo the
     100         *                 partial or return the partial as a string, or return false if error.
     101         */
     102        public $render_callback;
     103
     104        /**
     105         * Whether the container element is included in the partial, or if only the contents are rendered.
     106         *
     107         * @since 4.5.0
     108         * @access public
     109         * @var bool
     110         */
     111        public $container_inclusive = false;
     112
     113        /**
     114         * Whether to refresh the entire preview in case a partial cannot be refreshed.
     115         *
     116         * A partial render is considered a failure if the render_callback returns false.
     117         *
     118         * @since 4.5.0
     119         * @access public
     120         * @var bool
     121         */
     122        public $fallback_refresh = true;
     123
     124        /**
     125         * Constructor.
     126         *
     127         * Supplied `$args` override class property defaults.
     128         *
     129         * If `$args['settings']` is not defined, use the $id as the setting ID.
     130         *
     131         * @since 4.5.0
     132         * @access public
     133         *
     134         * @param WP_Customize_Selective_Refresh $component Customize Partial Refresh plugin instance.
     135         * @param string                         $id        Control ID.
     136         * @param array                          $args      {
     137         *     Optional. Arguments to override class property defaults.
     138         *
     139         *     @type array|string $settings All settings IDs tied to the partial. If undefined, `$id` will be used.
     140         * }
     141         */
     142        public function __construct( WP_Customize_Selective_Refresh $component, $id, $args = array() ) {
     143                $keys = array_keys( get_object_vars( $this ) );
     144                foreach ( $keys as $key ) {
     145                        if ( isset( $args[ $key ] ) ) {
     146                                $this->$key = $args[ $key ];
     147                        }
     148                }
     149
     150                $this->component       = $component;
     151                $this->id              = $id;
     152                $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
     153                $this->id_data['base'] = array_shift( $this->id_data['keys'] );
     154
     155                if ( empty( $this->render_callback ) ) {
     156                        $this->render_callback = array( $this, 'render_callback' );
     157                }
     158
     159                // Process settings.
     160                if ( empty( $this->settings ) ) {
     161                        $this->settings = array( $id );
     162                } else if ( is_string( $this->settings ) ) {
     163                        $this->settings = array( $this->settings );
     164                }
     165
     166                if ( empty( $this->primary_setting ) ) {
     167                        $this->primary_setting = current( $this->settings );
     168                }
     169        }
     170
     171        /**
     172         * Retrieves parsed ID data for multidimensional setting.
     173         *
     174         * @since 4.5.0
     175         * @access public
     176         *
     177         * @return array {
     178         *     ID data for multidimensional partial.
     179         *
     180         *     @type string $base ID base.
     181         *     @type array  $keys Keys for multidimensional array.
     182         * }
     183         */
     184        final public function id_data() {
     185                return $this->id_data;
     186        }
     187
     188        /**
     189         * Renders the template partial involving the associated settings.
     190         *
     191         * @since 4.5.0
     192         * @access public
     193         *
     194         * @param array $container_context Optional. Array of context data associated with the target container (placement).
     195         *                                 Default empty array.
     196         * @return string|array|false The rendered partial as a string, raw data array (for client-side JS template),
     197         *                            or false if no render applied.
     198         */
     199        final public function render( $container_context = array() ) {
     200                $partial  = $this;
     201                $rendered = false;
     202
     203                if ( ! empty( $this->render_callback ) ) {
     204                        ob_start();
     205                        $return_render = call_user_func( $this->render_callback, $this, $container_context );
     206                        $ob_render = ob_get_clean();
     207
     208                        if ( null !== $return_render && '' !== $ob_render ) {
     209                                _doing_it_wrong( __FUNCTION__, __( 'Partial render must echo the content or return the content string (or array), but not both.' ), '4.5.0' );
     210                        }
     211
     212                        /*
     213                         * Note that the string return takes precedence because the $ob_render may just\
     214                         * include PHP warnings or notices.
     215                         */
     216                        $rendered = null !== $return_render ? $return_render : $ob_render;
     217                }
     218
     219                /**
     220                 * Filters partial rendering.
     221                 *
     222                 * @since 4.5.0
     223                 *
     224                 * @param string|array|false   $rendered          The partial value. Default false.
     225                 * @param WP_Customize_Partial $partial           WP_Customize_Setting instance.
     226                 * @param array                $container_context Optional array of context data associated with
     227                 *                                                the target container.
     228                 */
     229                $rendered = apply_filters( 'customize_partial_render', $rendered, $partial, $container_context );
     230
     231                /**
     232                 * Filters partial rendering for a specific partial.
     233                 *
     234                 * The dynamic portion of the hook name, `$partial->ID` refers to the partial ID.
     235                 *
     236                 * @since 4.5.0
     237                 *
     238                 * @param string|array|false   $rendered          The partial value. Default false.
     239                 * @param WP_Customize_Partial $partial           WP_Customize_Setting instance.
     240                 * @param array                $container_context Optional array of context data associated with
     241                 *                                                the target container.
     242                 */
     243                $rendered = apply_filters( "customize_partial_render_{$partial->id}", $rendered, $partial, $container_context );
     244
     245                return $rendered;
     246        }
     247
     248        /**
     249         * Default callback used when invoking WP_Customize_Control::render().
     250         *
     251         * Note that this method may echo the partial *or* return the partial as
     252         * a string or array, but not both. Output buffering is performed when this
     253         * is called. Subclasses can override this with their specific logic, or they
     254         * may provide an 'render_callback' argument to the constructor.
     255         *
     256         * This method may return an HTML string for straight DOM injection, or it
     257         * may return an array for supporting Partial JS subclasses to render by
     258         * applying to client-side templating.
     259         *
     260         * @since 4.5.0
     261         * @access public
     262         *
     263         * @return string|array|false
     264         */
     265        public function render_callback() {
     266                return false;
     267        }
     268
     269        /**
     270         * Retrieves the data to export to the client via JSON.
     271         *
     272         * @since 4.5.0
     273         * @access public
     274         *
     275         * @return array Array of parameters passed to the JavaScript.
     276         */
     277        public function json() {
     278                $exports = array(
     279                        'settings'           => $this->settings,
     280                        'primarySetting'     => $this->primary_setting,
     281                        'selector'           => $this->selector,
     282                        'type'               => $this->type,
     283                        'fallbackRefresh'    => $this->fallback_refresh,
     284                        'containerInclusive' => $this->container_inclusive,
     285                );
     286                return $exports;
     287        }
     288}
  • new file src/wp-includes/customize/class-wp-customize-selective-refresh.php

    diff --git src/wp-includes/customize/class-wp-customize-selective-refresh.php src/wp-includes/customize/class-wp-customize-selective-refresh.php
    new file mode 100644
    index 0000000..f6f72c1
    - +  
     1<?php
     2/**
     3 * WordPress Customize Selective Refresh class
     4 *
     5 * @package WordPress
     6 * @subpackage Customize
     7 * @since 4.5.0
     8 */
     9
     10/**
     11 * WordPress Customize Selective Refresh class.
     12 *
     13 * @since 4.5.0
     14 */
     15class WP_Customize_Selective_Refresh {
     16
     17        /**
     18         * Query var used in requests to render partials.
     19         *
     20         * @since 4.5.0
     21         */
     22        const RENDER_QUERY_VAR = 'wp_customize_render_partials';
     23
     24        /**
     25         * Customize manager.
     26         *
     27         * @var WP_Customize_Manager
     28         */
     29        public $manager;
     30
     31        /**
     32         * Registered instances of WP_Customize_Partial.
     33         *
     34         * @since 4.5.0
     35         * @access protected
     36         * @var WP_Customize_Partial[]
     37         */
     38        protected $partials = array();
     39
     40        /**
     41         * Log of errors triggered when partials are rendered.
     42         *
     43         * @since 4.5.0
     44         * @access private
     45         * @var array
     46         */
     47        protected $triggered_errors = array();
     48
     49        /**
     50         * Keep track of the current partial being rendered.
     51         *
     52         * @since 4.5.0
     53         * @access private
     54         * @var string
     55         */
     56        protected $current_partial_id;
     57
     58        /**
     59         * Plugin bootstrap for Partial Refresh functionality.
     60         *
     61         * @since 4.5.0
     62         * @access public
     63         *
     64         * @param WP_Customize_Manager $manager Manager instance.
     65         */
     66        public function __construct( WP_Customize_Manager $manager ) {
     67                $this->manager = $manager;
     68                require_once( ABSPATH . WPINC . '/customize/class-wp-customize-partial.php' );
     69
     70                add_action( 'customize_preview_init', array( $this, 'init_preview' ) );
     71        }
     72
     73        /**
     74         * Retrieves the registered partials.
     75         *
     76         * @since 4.5.0
     77         * @access public
     78         *
     79         * @return array Partials.
     80         */
     81        public function partials() {
     82                return $this->partials;
     83        }
     84
     85        /**
     86         * Adds a partial.
     87         *
     88         * @since 4.5.0
     89         * @access public
     90         *
     91         * @param WP_Customize_Partial|string $id   Customize Partial object, or Panel ID.
     92         * @param array                       $args Optional. Partial arguments. Default empty array.
     93         * @return WP_Customize_Partial             The instance of the panel that was added.
     94         */
     95        public function add_partial( $id, $args = array() ) {
     96                if ( $id instanceof WP_Customize_Partial ) {
     97                        $partial = $id;
     98                } else {
     99                        $class = 'WP_Customize_Partial';
     100
     101                        /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */
     102                        $args = apply_filters( 'customize_dynamic_partial_args', $args, $id );
     103
     104                        /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */
     105                        $class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args );
     106
     107                        $partial = new $class( $this, $id, $args );
     108                }
     109
     110                $this->partials[ $partial->id ] = $partial;
     111                return $partial;
     112        }
     113
     114        /**
     115         * Retrieves a partial.
     116         *
     117         * @since 4.5.0
     118         * @access public
     119         *
     120         * @param string $id Customize Partial ID.
     121         * @return WP_Customize_Partial|null The partial, if set. Otherwise null.
     122         */
     123        public function get_partial( $id ) {
     124                if ( isset( $this->partials[ $id ] ) ) {
     125                        return $this->partials[ $id ];
     126                } else {
     127                        return null;
     128                }
     129        }
     130
     131        /**
     132         * Removes a partial.
     133         *
     134         * @since 4.5.0
     135         * @access public
     136         *
     137         * @param string $id Customize Partial ID.
     138         */
     139        public function remove_partial( $id ) {
     140                unset( $this->partials[ $id ] );
     141        }
     142
     143        /**
     144         * Initializes the Customizer preview.
     145         *
     146         * @since 4.5.0
     147         * @access public
     148         */
     149        public function init_preview() {
     150                add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) );
     151                add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
     152        }
     153
     154        /**
     155         * Enqueues preview scripts.
     156         *
     157         * @since 4.5.0
     158         * @access public
     159         */
     160        public function enqueue_preview_scripts() {
     161                wp_enqueue_script( 'customize-selective-refresh' );
     162                add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 );
     163        }
     164
     165        /**
     166         * Exports data in preview after it has finished rendering so that partials can be added at runtime.
     167         *
     168         * @since 4.5.0
     169         * @access public
     170         */
     171        public function export_preview_data() {
     172                $partials = array();
     173
     174                foreach ( $this->partials() as $partial ) {
     175                        $partials[ $partial->id ] = $partial->json();
     176                }
     177
     178                $exports = array(
     179                        'partials'       => $partials,
     180                        'renderQueryVar' => self::RENDER_QUERY_VAR,
     181                        'l10n'           => array(
     182                                'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
     183                        ),
     184                );
     185
     186                // Export data to JS.
     187                echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) );
     188        }
     189
     190        /**
     191         * Registers dynamically-created partials.
     192         *
     193         * @since 4.5.0
     194         * @access public
     195         *
     196         * @see WP_Customize_Manager::add_dynamic_settings()
     197         *
     198         * @param array $partial_ids The partial ID to add.
     199         * @return array Added WP_Customize_Partial instances.
     200         */
     201        public function add_dynamic_partials( $partial_ids ) {
     202                $new_partials = array();
     203
     204                foreach ( $partial_ids as $partial_id ) {
     205
     206                        // Skip partials already created.
     207                        $partial = $this->get_partial( $partial_id );
     208                        if ( $partial ) {
     209                                continue;
     210                        }
     211
     212                        $partial_args = false;
     213                        $partial_class = 'WP_Customize_Partial';
     214
     215                        /**
     216                         * Filters a dynamic partial's constructor arguments.
     217                         *
     218                         * For a dynamic partial to be registered, this filter must be employed
     219                         * to override the default false value with an array of args to pass to
     220                         * the WP_Customize_Partial constructor.
     221                         *
     222                         * @since 4.5.0
     223                         *
     224                         * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor.
     225                         * @param string      $partial_id   ID for dynamic partial.
     226                         */
     227                        $partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id );
     228                        if ( false === $partial_args ) {
     229                                continue;
     230                        }
     231
     232                        /**
     233                         * Filters the class used to construct partials.
     234                         *
     235                         * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass.
     236                         *
     237                         * @since 4.5.0
     238                         *
     239                         * @param string $partial_class WP_Customize_Partial or a subclass.
     240                         * @param string $partial_id    ID for dynamic partial.
     241                         * @param array  $partial_args  The arguments to the WP_Customize_Partial constructor.
     242                         */
     243                        $partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args );
     244
     245                        $partial = new $partial_class( $this, $partial_id, $partial_args );
     246
     247                        $this->add_partial( $partial );
     248                        $new_partials[] = $partial;
     249                }
     250                return $new_partials;
     251        }
     252
     253        /**
     254         * Checks whether the request is for rendering partials.
     255         *
     256         * Note that this will not consider whether the request is authorized or valid,
     257         * just that essentially the route is a match.
     258         *
     259         * @since 4.5.0
     260         * @access public
     261         *
     262         * @return bool Whether the request is for rendering partials.
     263         */
     264        public function is_render_partials_request() {
     265                return ! empty( $_POST[ self::RENDER_QUERY_VAR ] );
     266        }
     267
     268        /**
     269         * Handles PHP errors triggered during rendering the partials.
     270         *
     271         * These errors will be relayed back to the client in the Ajax response.
     272         *
     273         * @since 4.5.0
     274         * @access private
     275         *
     276         * @param int    $errno   Error number.
     277         * @param string $errstr  Error string.
     278         * @param string $errfile Error file.
     279         * @param string $errline Error line.
     280         * @return true Always true.
     281         */
     282        public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) {
     283                $this->triggered_errors[] = array(
     284                        'partial'      => $this->current_partial_id,
     285                        'error_number' => $errno,
     286                        'error_string' => $errstr,
     287                        'error_file'   => $errfile,
     288                        'error_line'   => $errline,
     289                );
     290                return true;
     291        }
     292
     293        /**
     294         * Handles the Ajax request to return the rendered partials for the requested placements.
     295         *
     296         * @since 4.5.0
     297         * @access public
     298         */
     299        public function handle_render_partials_request() {
     300                if ( ! $this->is_render_partials_request() ) {
     301                        return;
     302                }
     303
     304                $this->manager->remove_preview_signature();
     305
     306                /*
     307                 * Note that is_customize_preview() returning true will entail that the
     308                 * user passed the 'customize' capability check and the nonce check, since
     309                 * WP_Customize_Manager::setup_theme() is where the previewing flag is set.
     310                 */
     311                if ( ! is_customize_preview() ) {
     312                        status_header( 403 );
     313                        wp_send_json_error( 'expected_customize_preview' );
     314                } else if ( ! isset( $_POST['partials'] ) ) {
     315                        status_header( 400 );
     316                        wp_send_json_error( 'missing_partials' );
     317                }
     318
     319                $partials = json_decode( wp_unslash( $_POST['partials'] ), true );
     320
     321                if ( ! is_array( $partials ) ) {
     322                        wp_send_json_error( 'malformed_partials' );
     323                }
     324
     325                $this->add_dynamic_partials( array_keys( $partials ) );
     326
     327                /**
     328                 * Fires immediately before partials are rendered.
     329                 *
     330                 * Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts
     331                 * and styles which may get enqueued in the response.
     332                 *
     333                 * @since 4.5.0
     334                 *
     335                 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.
     336                 * @param array                          $partials Placements' context data for the partials rendered in the request.
     337                 *                                                 The array is keyed by partial ID, with each item being an array of
     338                 *                                                 the placements' context data.
     339                 */
     340                do_action( 'customize_render_partials_before', $this, $partials );
     341
     342                set_error_handler( array( $this, 'handle_error' ), error_reporting() );
     343
     344                $contents = array();
     345
     346                foreach ( $partials as $partial_id => $container_contexts ) {
     347                        $this->current_partial_id = $partial_id;
     348
     349                        if ( ! is_array( $container_contexts ) ) {
     350                                wp_send_json_error( 'malformed_container_contexts' );
     351                        }
     352
     353                        $partial = $this->get_partial( $partial_id );
     354
     355                        if ( ! $partial ) {
     356                                $contents[ $partial_id ] = null;
     357                                continue;
     358                        }
     359
     360                        $contents[ $partial_id ] = array();
     361
     362                        // @todo The array should include not only the contents, but also whether the container is included?
     363                        if ( empty( $container_contexts ) ) {
     364                                // Since there are no container contexts, render just once.
     365                                $contents[ $partial_id ][] = $partial->render( null );
     366                        } else {
     367                                foreach ( $container_contexts as $container_context ) {
     368                                        $contents[ $partial_id ][] = $partial->render( $container_context );
     369                                }
     370                        }
     371                }
     372                $this->current_partial_id = null;
     373
     374                restore_error_handler();
     375
     376                /**
     377                 * Fires immediately after partials are rendered.
     378                 *
     379                 * Plugins may do things like call wp_footer() to scrape scripts output and return them
     380                 * via the {@see 'customize_render_partials_response'} filter.
     381                 *
     382                 * @since 4.5.0
     383                 *
     384                 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.
     385                 * @param array                          $partials Placements' context data for the partials rendered in the request.
     386                 *                                                 The array is keyed by partial ID, with each item being an array of
     387                 *                                                 the placements' context data.
     388                 */
     389                do_action( 'customize_render_partials_after', $this, $partials );
     390
     391                $response = array(
     392                        'contents' => $contents,
     393                );
     394
     395                if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
     396                        $response['errors'] = $this->triggered_errors;
     397                }
     398
     399                /**
     400                 * Filters the response from rendering the partials.
     401                 *
     402                 * Plugins may use this filter to inject `$scripts` and `$styles`, which are dependencies
     403                 * for the partials being rendered. The response data will be available to the client via
     404                 * the `render-partials-response` JS event, so the client can then inject the scripts and
     405                 * styles into the DOM if they have not already been enqueued there.
     406                 *
     407                 * If plugins do this, they'll need to take care for any scripts that do `document.write()`
     408                 * and make sure that these are not injected, or else to override the function to no-op,
     409                 * or else the page will be destroyed.
     410                 *
     411                 * Plugins should be aware that `$scripts` and `$styles` may eventually be included by
     412                 * default in the response.
     413                 *
     414                 * @since 4.5.0
     415                 *
     416                 * @param array $response {
     417                 *     Response.
     418                 *
     419                 *     @type array $contents Associative array mapping a partial ID its corresponding array of contents
     420                 *                           for the containers requested.
     421                 *     @type array $errors   List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY`
     422                 *                           is enabled.
     423                 * }
     424                 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.
     425                 * @param array                          $partials Placements' context data for the partials rendered in the request.
     426                 *                                                 The array is keyed by partial ID, with each item being an array of
     427                 *                                                 the placements' context data.
     428                 */
     429                $response = apply_filters( 'customize_render_partials_response', $response, $this, $partials );
     430
     431                wp_send_json_success( $response );
     432        }
     433}
  • src/wp-includes/js/customize-preview-nav-menus.js

    diff --git src/wp-includes/js/customize-preview-nav-menus.js src/wp-includes/js/customize-preview-nav-menus.js
    index 9e84494..c61e620 100644
     
    1 /* global JSON, _wpCustomizePreviewNavMenusExports */
    2 
    3 ( function( $, _, wp ) {
     1wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
    42        'use strict';
    53
    6         if ( ! wp || ! wp.customize ) { return; }
    7 
    8         var api = wp.customize,
    9                 currentRefreshDebounced = {},
    10                 refreshDebounceDelay = 200,
    11                 settings = {},
    12                 defaultSettings = {
    13                         renderQueryVar: null,
    14                         renderNonceValue: null,
    15                         renderNoncePostKey: null,
    16                         requestUri: '/',
    17                         navMenuInstanceArgs: {},
    18                         l10n: {}
    19                 };
    20 
    21         api.MenusCustomizerPreview = {
    22                 /**
    23                  * Bootstrap functionality.
    24                  */
    25                 init : function() {
    26                         var self = this, initializedSettings = {};
    27 
    28                         settings = _.extend( {}, defaultSettings );
    29                         if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
    30                                 _.extend( settings, _wpCustomizePreviewNavMenusExports );
    31                         }
    32 
    33                         api.each( function( setting, id ) {
    34                                 setting.id = id;
    35                                 initializedSettings[ setting.id ] = true;
    36                                 self.bindListener( setting );
    37                         } );
    38 
    39                         api.preview.bind( 'setting', function( args ) {
    40                                 var id, value, setting;
    41                                 args = args.slice();
    42                                 id = args.shift();
    43                                 value = args.shift();
     4        var self = {};
    445
    45                                 setting = api( id );
    46                                 if ( ! setting ) {
    47                                         // Currently customize-preview.js is not creating settings for dynamically-created settings in the pane, so we have to do it.
    48                                         setting = api.create( id, value ); // @todo This should be in core
    49                                 }
    50                                 if ( ! setting.id ) {
    51                                         // Currently customize-preview.js doesn't set the id property for each setting, like customize-controls.js does.
    52                                         setting.id = id;
    53                                 }
     6        /**
     7         * Initialize nav menus preview.
     8         */
     9        self.init = function() {
     10                var self = this;
    5411
    55                                 if ( ! initializedSettings[ setting.id ] ) {
    56                                         initializedSettings[ setting.id ] = true;
    57                                         if ( self.bindListener( setting ) ) {
    58                                                 setting.callbacks.fireWith( setting, [ setting(), null ] );
    59                                         }
    60                                 }
    61                         } );
     12                if ( api.selectiveRefresh ) {
     13                        self.watchNavMenuLocationChanges();
     14                }
    6215
     16                api.preview.bind( 'active', function() {
    6317                        self.highlightControls();
    64                 },
    65 
    66                 /**
    67                  *
    68                  * @param {wp.customize.Value} setting
    69                  * @returns {boolean} Whether the setting was bound.
    70                  */
    71                 bindListener : function( setting ) {
    72                         var matches, themeLocation;
    73 
    74                         matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
    75                         if ( matches ) {
    76                                 setting.navMenuId = parseInt( matches[1], 10 );
    77                                 setting.bind( this.onChangeNavMenuSetting );
    78                                 return true;
    79                         }
    80 
    81                         matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
    82                         if ( matches ) {
    83                                 setting.navMenuItemId = parseInt( matches[1], 10 );
    84                                 setting.bind( this.onChangeNavMenuItemSetting );
    85                                 return true;
    86                         }
    87 
    88                         matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
    89                         if ( matches ) {
    90                                 themeLocation = matches[1];
    91                                 setting.bind( _.bind( function() {
    92                                         this.refreshMenuLocation( themeLocation );
    93                                 }, this ) );
    94                                 return true;
    95                         }
    96 
    97                         return false;
    98                 },
    99 
    100                 /**
    101                  * Handle changing of a nav_menu setting.
    102                  *
    103                  * @this {wp.customize.Setting}
    104                  */
    105                 onChangeNavMenuSetting : function() {
    106                         var setting = this;
    107                         if ( ! setting.navMenuId ) {
    108                                 throw new Error( 'Expected navMenuId property to be set.' );
    109                         }
    110                         api.MenusCustomizerPreview.refreshMenu( setting.navMenuId );
    111                 },
     18                } );
     19        };
    11220
    113                 /**
    114                  * Handle changing of a nav_menu_item setting.
    115                  *
    116                  * @this {wp.customize.Setting}
    117                  * @param {object} to
    118                  * @param {object} from
    119                  */
    120                 onChangeNavMenuItemSetting : function( to, from ) {
    121                         if ( from && from.nav_menu_term_id && ( ! to || from.nav_menu_term_id !== to.nav_menu_term_id ) ) {
    122                                 api.MenusCustomizerPreview.refreshMenu( from.nav_menu_term_id );
    123                         }
    124                         if ( to && to.nav_menu_term_id ) {
    125                                 api.MenusCustomizerPreview.refreshMenu( to.nav_menu_term_id );
    126                         }
    127                 },
     21        if ( api.selectiveRefresh ) {
    12822
    12923                /**
    130                  * Update a given menu rendered in the preview.
     24                 * Partial representing an invocation of wp_nav_menu().
    13125                 *
    132                  * @param {int} menuId
     26                 * @class
     27                 * @augments wp.customize.selectiveRefresh.Partial
     28                 * @since 4.5.0
    13329                 */
    134                 refreshMenu : function( menuId ) {
    135                         var assignedLocations = [];
    136 
    137                         api.each(function( setting, id ) {
    138                                 var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
    139                                 if ( matches && menuId === setting() ) {
    140                                         assignedLocations.push( matches[1] );
     30                self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
     31
     32                        /**
     33                         * Constructor.
     34                         *
     35                         * @since 4.5.0
     36                         * @param {string} id - Partial ID.
     37                         * @param {Object} options
     38                         * @param {Object} options.params
     39                         * @param {Object} options.params.navMenuArgs
     40                         * @param {string} options.params.navMenuArgs.args_hmac
     41                         * @param {string} [options.params.navMenuArgs.theme_location]
     42                         * @param {number} [options.params.navMenuArgs.menu]
     43                         * @param {object} [options.constructingContainerContext]
     44                         */
     45                        initialize: function( id, options ) {
     46                                var partial = this, matches, argsHmac;
     47                                matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
     48                                if ( ! matches ) {
     49                                        throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
    14150                                }
    142                         });
    143 
    144                         _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
    145                                 if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) {
    146                                         this.refreshMenuInstanceDebounced( instanceNumber );
     51                                argsHmac = matches[1];
     52
     53                                options = options || {};
     54                                options.params = _.extend(
     55                                        {
     56                                                selector: '[data-customize-partial-id="' + id + '"]',
     57                                                navMenuArgs: options.constructingContainerContext || {},
     58                                                containerInclusive: true
     59                                        },
     60                                        options.params || {}
     61                                );
     62                                api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     63
     64                                if ( ! _.isObject( partial.params.navMenuArgs ) ) {
     65                                        throw new Error( 'Missing navMenuArgs' );
    14766                                }
    148                         }, this );
    149                 },
    150 
    151                 /**
    152                  * Refresh the menu(s) associated with a given nav menu location.
    153                  *
    154                  * @param {string} location
    155                  */
    156                 refreshMenuLocation : function( location ) {
    157                         var foundInstance = false;
    158                         _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
    159                                 if ( location === navMenuArgs.theme_location ) {
    160                                         this.refreshMenuInstanceDebounced( instanceNumber );
    161                                         foundInstance = true;
     67                                if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
     68                                        throw new Error( 'args_hmac mismatch with id' );
    16269                                }
    163                         }, this );
    164                         if ( ! foundInstance ) {
    165                                 api.preview.send( 'refresh' );
    166                         }
    167                 },
    168 
    169                 /**
    170                  * Update a specific instance of a given menu on the page.
    171                  *
    172                  * @param {int} instanceNumber
    173                  */
    174                 refreshMenuInstance : function( instanceNumber ) {
    175                         var data, menuId, customized, container, request, wpNavMenuArgs, instance, containerInstanceClassName;
    176 
    177                         if ( ! settings.navMenuInstanceArgs[ instanceNumber ] ) {
    178                                 throw new Error( 'unknown_instance_number' );
    179                         }
    180                         instance = settings.navMenuInstanceArgs[ instanceNumber ];
    181 
    182                         containerInstanceClassName = 'partial-refreshable-nav-menu-' + String( instanceNumber );
    183                         container = $( '.' + containerInstanceClassName );
    184 
    185                         if ( _.isNumber( instance.menu ) ) {
    186                                 menuId = instance.menu;
    187                         } else if ( instance.theme_location && api.has( 'nav_menu_locations[' + instance.theme_location + ']' ) ) {
    188                                 menuId = api( 'nav_menu_locations[' + instance.theme_location + ']' ).get();
    189                         }
    190 
    191                         if ( ! menuId || ! instance.can_partial_refresh || 0 === container.length ) {
    192                                 api.preview.send( 'refresh' );
    193                                 return;
    194                         }
    195                         menuId = parseInt( menuId, 10 );
    196 
    197                         data = {
    198                                 nonce: wp.customize.settings.nonce.preview,
    199                                 wp_customize: 'on'
    200                         };
    201                         if ( ! wp.customize.settings.theme.active ) {
    202                                 data.theme = wp.customize.settings.theme.stylesheet;
    203                         }
    204                         data[ settings.renderQueryVar ] = '1';
    205 
    206                         // Gather settings to send in partial refresh request.
    207                         customized = {};
    208                         api.each( function( setting, id ) {
    209                                 var value = setting.get(), shouldSend = false;
    210                                 // @todo Core should propagate the dirty state into the Preview as well so we can use that here.
    211 
    212                                 // Send setting if it is a nav_menu_locations[] setting.
    213                                 shouldSend = shouldSend || /^nav_menu_locations\[/.test( id );
    214 
    215                                 // Send setting if it is the setting for this menu.
    216                                 shouldSend = shouldSend || id === 'nav_menu[' + String( menuId ) + ']';
    217 
    218                                 // Send setting if it is one that is associated with this menu, or it is deleted.
    219                                 shouldSend = shouldSend || ( /^nav_menu_item\[/.test( id ) && ( false === value || menuId === value.nav_menu_term_id ) );
    220 
    221                                 if ( shouldSend ) {
    222                                         customized[ id ] = value;
     70                        },
     71
     72                        /**
     73                         * Return whether the setting is related to this partial.
     74                         *
     75                         * @since 4.5.0
     76                         * @param {wp.customize.Value|string} setting  - Object or ID.
     77                         * @param {number|object|false|null}  newValue - New value, or null if the setting was just removed.
     78                         * @param {number|object|false|null}  oldValue - Old value, or null if the setting was just added.
     79                         * @returns {boolean}
     80                         */
     81                        isRelatedSetting: function( setting, newValue, oldValue ) {
     82                                var partial = this, navMenuLocationSetting, navMenuId;
     83                                if ( _.isString( setting ) ) {
     84                                        setting = api( setting );
    22385                                }
    224                         } );
    225                         data.customized = JSON.stringify( customized );
    226                         data[ settings.renderNoncePostKey ] = settings.renderNonceValue;
    22786
    228                         wpNavMenuArgs = $.extend( {}, instance );
    229                         data.wp_nav_menu_args_hash = wpNavMenuArgs.args_hash;
    230                         delete wpNavMenuArgs.args_hash;
    231                         data.wp_nav_menu_args = JSON.stringify( wpNavMenuArgs );
    232 
    233                         container.addClass( 'customize-partial-refreshing' );
    234 
    235                         request = wp.ajax.send( null, {
    236                                 data: data,
    237                                 url: api.settings.url.self
    238                         } );
    239                         request.done( function( data ) {
    240                                 // If the menu is now not visible, refresh since the page layout may have changed.
    241                                 if ( false === data ) {
    242                                         api.preview.send( 'refresh' );
    243                                         return;
     87                                if ( partial.params.navMenuArgs.theme_location ) {
     88                                        if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
     89                                                return true;
     90                                        }
     91                                        navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
    24492                                }
    24593
    246                                 var eventParam, previousContainer = container;
    247                                 container = $( data );
    248                                 container.addClass( containerInstanceClassName );
    249                                 container.addClass( 'partial-refreshable-nav-menu customize-partial-refreshing' );
    250                                 previousContainer.replaceWith( container );
    251                                 eventParam = {
    252                                         instanceNumber: instanceNumber,
    253                                         wpNavArgs: wpNavMenuArgs, // @deprecated
    254                                         wpNavMenuArgs: wpNavMenuArgs,
    255                                         oldContainer: previousContainer,
    256                                         newContainer: container
    257                                 };
    258                                 container.removeClass( 'customize-partial-refreshing' );
    259                                 $( document ).trigger( 'customize-preview-menu-refreshed', [ eventParam ] );
    260                         } );
    261                         request.fail( function() {
    262                                 api.preview.send( 'refresh' );
    263                         } );
    264                 },
     94                                navMenuId = partial.params.navMenuArgs.menu;
     95                                if ( ! navMenuId && navMenuLocationSetting ) {
     96                                        navMenuId = navMenuLocationSetting();
     97                                }
    26598
    266                 refreshMenuInstanceDebounced : function( instanceNumber ) {
    267                         if ( currentRefreshDebounced[ instanceNumber ] ) {
    268                                 clearTimeout( currentRefreshDebounced[ instanceNumber ] );
     99                                if ( ! navMenuId ) {
     100                                        return false;
     101                                }
     102                                return (
     103                                        ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
     104                                        ( /^nav_menu_item\[/.test( setting.id ) &&
     105                                                ( ( newValue && newValue.nav_menu_term_id === navMenuId ) || ( oldValue && oldValue.nav_menu_term_id === navMenuId ) )
     106                                        )
     107                                );
     108                        },
     109
     110                        /**
     111                         * Render content.
     112                         *
     113                         * @inheritdoc
     114                         * @param {wp.customize.selectiveRefresh.Placement} placement
     115                         */
     116                        renderContent: function( placement ) {
     117                                var partial = this, previousContainer = placement.container;
     118                                if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
     119
     120                                        // Trigger deprecated event.
     121                                        $( document ).trigger( 'customize-preview-menu-refreshed', [ {
     122                                                instanceNumber: null, // @deprecated
     123                                                wpNavArgs: placement.context, // @deprecated
     124                                                wpNavMenuArgs: placement.context,
     125                                                oldContainer: previousContainer,
     126                                                newContainer: placement.container
     127                                        } ] );
     128                                }
    269129                        }
    270                         currentRefreshDebounced[ instanceNumber ] = setTimeout(
    271                                 _.bind( function() {
    272                                         this.refreshMenuInstance( instanceNumber );
    273                                 }, this ),
    274                                 refreshDebounceDelay
    275                         );
    276                 },
     130                });
     131
     132                api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
    277133
    278134                /**
    279                  * Connect nav menu items with their corresponding controls in the pane.
     135                 * Watch for changes to nav_menu_locations[] settings.
     136                 *
     137                 * Refresh partials associated with the given nav_menu_locations[] setting,
     138                 * or request an entire preview refresh if there are no containers in the
     139                 * document for a partial associated with the theme location.
     140                 *
     141                 * @since 4.5.0
    280142                 */
    281                 highlightControls: function() {
    282                         var selector = '.menu-item',
    283                                 addTooltips;
    284 
    285                         // Open expand the menu item control when shift+clicking the menu item
    286                         $( document ).on( 'click', selector, function( e ) {
    287                                 var navMenuItemParts;
    288                                 if ( ! e.shiftKey ) {
     143                self.watchNavMenuLocationChanges = function() {
     144                        api.bind( 'change', function( setting ) {
     145                                var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ );
     146                                if ( ! matches ) {
    289147                                        return;
    290148                                }
     149                                themeLocation = matches[1];
     150                                api.selectiveRefresh.partial.each( function( partial ) {
     151                                        if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) {
     152                                                partial.refresh();
     153                                                themeLocationPartialFound = true;
     154                                        }
     155                                } );
    291156
    292                                 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
    293                                 if ( navMenuItemParts ) {
    294                                         e.preventDefault();
    295                                         e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
    296                                         api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
     157                                if ( ! themeLocationPartialFound ) {
     158                                        api.selectiveRefresh.requestFullRefresh();
    297159                                }
    298                         });
    299 
    300                         addTooltips = function( e, params ) {
    301                                 params.newContainer.find( selector ).attr( 'title', settings.l10n.editNavMenuItemTooltip );
    302                         };
     160                        } );
     161                };
     162        }
     163
     164        /**
     165         * Connect nav menu items with their corresponding controls in the pane.
     166         *
     167         * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
     168         * Also this applies even if a nav menu is not partial-refreshable.
     169         *
     170         * @since 4.5.0
     171         */
     172        self.highlightControls = function() {
     173                var selector = '.menu-item';
     174
     175                // Focus on the menu item control when shift+clicking the menu item.
     176                $( document ).on( 'click', selector, function( e ) {
     177                        var navMenuItemParts;
     178                        if ( ! e.shiftKey ) {
     179                                return;
     180                        }
    303181
    304                         addTooltips( null, { newContainer: $( document.body ) } );
    305                         $( document ).on( 'customize-preview-menu-refreshed', addTooltips );
    306                 }
     182                        navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
     183                        if ( navMenuItemParts ) {
     184                                e.preventDefault();
     185                                e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
     186                                api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
     187                        }
     188                });
    307189        };
    308190
    309191        api.bind( 'preview-ready', function() {
    310                 api.preview.bind( 'active', function() {
    311                         api.MenusCustomizerPreview.init();
    312                 } );
     192                self.init();
    313193        } );
    314194
    315 }( jQuery, _, wp ) );
     195        return self;
     196
     197}( jQuery, _, wp, wp.customize ) );
  • src/wp-includes/js/customize-preview-widgets.js

    diff --git src/wp-includes/js/customize-preview-widgets.js src/wp-includes/js/customize-preview-widgets.js
    index f982829..92e7732 100644
     
    1 (function( wp, $ ){
     1/* global _wpWidgetCustomizerPreviewSettings */
     2wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
    23
    3         if ( ! wp || ! wp.customize ) { return; }
     4        var self;
    45
    5         var api = wp.customize;
     6        self = {
     7                renderedSidebars: {},
     8                renderedWidgets: {},
     9                registeredSidebars: [],
     10                registeredWidgets: {},
     11                widgetSelectors: [],
     12                preview: null,
     13                l10n: {
     14                        widgetTooltip: ''
     15                }
     16        };
    617
    718        /**
    8          * wp.customize.WidgetCustomizerPreview
     19         * Init widgets preview.
    920         *
     21         * @since 4.5.0
    1022         */
    11         api.WidgetCustomizerPreview = {
    12                 renderedSidebars: {}, // @todo Make rendered a property of the Backbone model
    13                 renderedWidgets: {}, // @todo Make rendered a property of the Backbone model
    14                 registeredSidebars: [], // @todo Make a Backbone collection
    15                 registeredWidgets: {}, // @todo Make array, Backbone collection
    16                 widgetSelectors: [],
    17                 preview: null,
    18                 l10n: {},
     23        self.init = function() {
     24                var self = this;
    1925
    20                 init: function () {
    21                         var self = this;
     26                self.preview = api.preview;
     27                if ( api.selectiveRefresh ) {
     28                        self.addPartials();
     29                }
    2230
    23                         this.preview = api.preview;
    24                         this.buildWidgetSelectors();
    25                         this.highlightControls();
     31                self.buildWidgetSelectors();
     32                self.highlightControls();
    2633
    27                         this.preview.bind( 'highlight-widget', self.highlightWidget );
    28                 },
     34                self.preview.bind( 'highlight-widget', self.highlightWidget );
     35
     36                api.preview.bind( 'active', function() {
     37                        self.highlightControls();
     38                } );
     39        };
     40
     41        if ( api.selectiveRefresh ) {
    2942
    3043                /**
    31                  * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
     44                 * Partial representing a widget instance.
     45                 *
     46                 * @class
     47                 * @augments wp.customize.selectiveRefresh.Partial
     48                 * @since 4.5.0
    3249                 */
    33                 buildWidgetSelectors: function () {
    34                         var self = this;
    35 
    36                         $.each( this.registeredSidebars, function ( i, sidebar ) {
    37                                 var widgetTpl = [
    38                                                 sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''),
    39                                                 sidebar.before_title,
    40                                                 sidebar.after_title,
    41                                                 sidebar.after_widget
    42                                         ].join(''),
    43                                         emptyWidget,
    44                                         widgetSelector,
    45                                         widgetClasses;
    46 
    47                                 emptyWidget = $(widgetTpl);
    48                                 widgetSelector = emptyWidget.prop('tagName');
    49                                 widgetClasses = emptyWidget.prop('className');
    50 
    51                                 // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
    52                                 if ( ! widgetClasses ) {
    53                                         return;
     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.' );
    5465                                }
    5566
    56                                 widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, '');
     67                                partial.widgetId = matches[1];
     68                                options = options || {};
     69                                options.params = _.extend(
     70                                        {
     71                                                /* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */
     72                                                selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]'
     73                                                settings: [ self.getWidgetSettingId( partial.widgetId ) ],
     74                                                containerInclusive: true
     75                                        },
     76                                        options.params || {}
     77                                );
     78
     79                                api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     80                        },
    5781
    58                                 if ( widgetClasses ) {
    59                                         widgetSelector += '.' + widgetClasses.split(/\s+/).join('.');
     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 );
    6093                                }
    61                                 self.widgetSelectors.push(widgetSelector);
    62                         });
    63                 },
     94                        }
     95                });
    6496
    6597                /**
    66                  * Highlight the widget on widget updates or widget control mouse overs.
     98                 * Partial representing a widget area.
    6799                 *
    68                  * @param  {string} widgetId ID of the widget.
     100                 * @class
     101                 * @augments wp.customize.selectiveRefresh.Partial
     102                 * @since 4.5.0
    69103                 */
    70                 highlightWidget: function( widgetId ) {
    71                         var $body = $( document.body ),
    72                                 $widget = $( '#' + widgetId );
     104                self.SidebarPartial = api.selectiveRefresh.Partial.extend({
    73105
    74                         $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
     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];
    75121
    76                         $widget.addClass( 'widget-customizer-highlighted-widget' );
    77                         setTimeout( function () {
    78                                 $widget.removeClass( 'widget-customizer-highlighted-widget' );
    79                         }, 500 );
    80                 },
     122                                options = options || {};
     123                                options.params = _.extend(
     124                                        {
     125                                                settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
     126                                        },
     127                                        options.params || {}
     128                                );
    81129
    82                 /**
    83                  * Show a title and highlight widgets on hover. On shift+clicking
    84                  * focus the widget control.
    85                  */
    86                 highlightControls: function() {
    87                         var self = this,
    88                                 selector = this.widgetSelectors.join(',');
     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 );
     161                                        }
     162                                } );
     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;
     170                                        }
     171                                        widgetId = parsedId.idBase;
     172                                        if ( parsedId.number ) {
     173                                                widgetId += '-' + String( parsedId.number );
     174                                        }
     175                                        if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
     176                                                sidebarPartial.ensureWidgetPlacementContainers( widgetId );
     177                                        }
     178                                } );
     179                        },
     180
     181                        /**
     182                         * Get the before/after boundary nodes for all instances of this sidebar (usually one).
     183                         *
     184                         * Note that TreeWalker is not implemented in IE8.
     185                         *
     186                         * @since 4.5.0
     187                         * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
     188                         */
     189                        findDynamicSidebarBoundaryNodes: function() {
     190                                var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
     191                                regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
     192                                recursiveCommentTraversal = function( childNodes ) {
     193                                        _.each( childNodes, function( node ) {
     194                                                var matches;
     195                                                if ( 8 === node.nodeType ) {
     196                                                        matches = node.nodeValue.match( regExp );
     197                                                        if ( ! matches || matches[2] !== partial.sidebarId ) {
     198                                                                return;
     199                                                        }
     200                                                        if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
     201                                                                boundaryNodes[ matches[3] ] = {
     202                                                                        before: null,
     203                                                                        after: null,
     204                                                                        instanceNumber: parseInt( matches[3], 10 )
     205                                                                };
     206                                                        }
     207                                                        if ( 'dynamic_sidebar_before' === matches[1] ) {
     208                                                                boundaryNodes[ matches[3] ].before = node;
     209                                                        } else {
     210                                                                boundaryNodes[ matches[3] ].after = node;
     211                                                        }
     212                                                } else if ( 1 === node.nodeType ) {
     213                                                        recursiveCommentTraversal( node.childNodes );
     214                                                }
     215                                        } );
     216                                };
     217
     218                                recursiveCommentTraversal( document.body.childNodes );
     219                                return _.values( boundaryNodes );
     220                        },
     221
     222                        /**
     223                         * Get the placements for this partial.
     224                         *
     225                         * @since 4.5.0
     226                         * @returns {Array}
     227                         */
     228                        placements: function() {
     229                                var partial = this;
     230                                return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
     231                                        return new api.selectiveRefresh.Placement( {
     232                                                partial: partial,
     233                                                container: null,
     234                                                startNode: boundaryNodes.before,
     235                                                endNode: boundaryNodes.after,
     236                                                context: {
     237                                                        instanceNumber: boundaryNodes.instanceNumber
     238                                                }
     239                                        } );
     240                                } );
     241                        },
     242
     243                        /**
     244                         * Get the list of widget IDs associated with this widget area.
     245                         *
     246                         * @since 4.5.0
     247                         *
     248                         * @returns {Array}
     249                         */
     250                        getWidgetIds: function() {
     251                                var sidebarPartial = this, settingId, widgetIds;
     252                                settingId = sidebarPartial.settings()[0];
     253                                if ( ! settingId ) {
     254                                        throw new Error( 'Missing associated setting.' );
     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;
     283                                        }
     284                                } );
     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                                }
    89347
    90                         $(selector).attr( 'title', this.l10n.widgetTooltip );
     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;
    91351
    92                         $(document).on( 'mouseenter', selector, function () {
    93                                 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
    94                         });
     352                                        foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
     353                                                return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
     354                                        } );
     355                                        if ( foundWidgetPlacement ) {
     356                                                return;
     357                                        }
    95358
    96                         // Open expand the widget control when shift+clicking the widget element
    97                         $(document).on( 'click', selector, function ( e ) {
    98                                 if ( ! e.shiftKey ) {
     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();
    99407                                        return;
    100408                                }
    101                                 e.preventDefault();
    102409
    103                                 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
    104                         });
     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        }
     490
     491        /**
     492         * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
     493         *
     494         * @since 3.9.0
     495         */
     496        self.buildWidgetSelectors = function() {
     497                var self = this;
     498
     499                $.each( self.registeredSidebars, function( i, sidebar ) {
     500                        var widgetTpl = [
     501                                        sidebar.before_widget.replace( '%1$s', '' ).replace( '%2$s', '' ),
     502                                        sidebar.before_title,
     503                                        sidebar.after_title,
     504                                        sidebar.after_widget
     505                                ].join( '' ),
     506                                emptyWidget,
     507                                widgetSelector,
     508                                widgetClasses;
     509
     510                        emptyWidget = $( widgetTpl );
     511                        widgetSelector = emptyWidget.prop( 'tagName' );
     512                        widgetClasses = emptyWidget.prop( 'className' );
     513
     514                        // Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
     515                        if ( ! widgetClasses ) {
     516                                return;
     517                        }
     518
     519                        widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );
     520
     521                        if ( widgetClasses ) {
     522                                widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
     523                        }
     524                        self.widgetSelectors.push( widgetSelector );
     525                });
     526        };
     527
     528        /**
     529         * Highlight the widget on widget updates or widget control mouse overs.
     530         *
     531         * @since 3.9.0
     532         * @param  {string} widgetId ID of the widget.
     533         */
     534        self.highlightWidget = function( widgetId ) {
     535                var $body = $( document.body ),
     536                        $widget = $( '#' + widgetId );
     537
     538                $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
     539
     540                $widget.addClass( 'widget-customizer-highlighted-widget' );
     541                setTimeout( function() {
     542                        $widget.removeClass( 'widget-customizer-highlighted-widget' );
     543                }, 500 );
     544        };
     545
     546        /**
     547         * Show a title and highlight widgets on hover. On shift+clicking
     548         * focus the widget control.
     549         *
     550         * @since 3.9.0
     551         */
     552        self.highlightControls = function() {
     553                var self = this,
     554                        selector = this.widgetSelectors.join( ',' );
     555
     556                $( selector ).attr( 'title', this.l10n.widgetTooltip );
     557
     558                $( document ).on( 'mouseenter', selector, function() {
     559                        self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
     560                });
     561
     562                // Open expand the widget control when shift+clicking the widget element
     563                $( document ).on( 'click', selector, function( e ) {
     564                        if ( ! e.shiftKey ) {
     565                                return;
     566                        }
     567                        e.preventDefault();
     568
     569                        self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
     570                });
     571        };
     572
     573        /**
     574         * Parse a widget ID.
     575         *
     576         * @since 4.5.0
     577         *
     578         * @param {string} widgetId Widget ID.
     579         * @returns {{idBase: string, number: number|null}}
     580         */
     581        self.parseWidgetId = function( widgetId ) {
     582                var matches, parsed = {
     583                        idBase: '',
     584                        number: null
     585                };
     586
     587                matches = widgetId.match( /^(.+)-(\d+)$/ );
     588                if ( matches ) {
     589                        parsed.idBase = matches[1];
     590                        parsed.number = parseInt( matches[2], 10 );
     591                } else {
     592                        parsed.idBase = widgetId; // Likely an old single widget.
     593                }
     594
     595                return parsed;
     596        };
     597
     598        /**
     599         * Parse a widget setting ID.
     600         *
     601         * @since 4.5.0
     602         *
     603         * @param {string} settingId Widget setting ID.
     604         * @returns {{idBase: string, number: number|null}|null}
     605         */
     606        self.parseWidgetSettingId = function( settingId ) {
     607                var matches, parsed = {
     608                        idBase: '',
     609                        number: null
     610                };
     611
     612                matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
     613                if ( ! matches ) {
     614                        return null;
    105615                }
     616                parsed.idBase = matches[1];
     617                if ( matches[2] ) {
     618                        parsed.number = parseInt( matches[2], 10 );
     619                }
     620                return parsed;
    106621        };
    107622
    108         $(function () {
    109                 var settings = window._wpWidgetCustomizerPreviewSettings;
    110                 if ( ! settings ) {
    111                         return;
     623        /**
     624         * Convert a widget ID into a Customizer setting ID.
     625         *
     626         * @since 4.5.0
     627         *
     628         * @param {string} widgetId Widget ID.
     629         * @returns {string} settingId Setting ID.
     630         */
     631        self.getWidgetSettingId = function( widgetId ) {
     632                var parsed = this.parseWidgetId( widgetId ), settingId;
     633
     634                settingId = 'widget_' + parsed.idBase;
     635                if ( parsed.number ) {
     636                        settingId += '[' + String( parsed.number ) + ']';
    112637                }
    113638
    114                 $.extend( api.WidgetCustomizerPreview, settings );
     639                return settingId;
     640        };
    115641
    116                 api.WidgetCustomizerPreview.init();
     642        api.bind( 'preview-ready', function() {
     643                $.extend( self, _wpWidgetCustomizerPreviewSettings );
     644                self.init();
    117645        });
    118646
    119 })( window.wp, jQuery );
     647        return self;
     648})( jQuery, _, wp, wp.customize );
  • new file src/wp-includes/js/customize-selective-refresh.js

    diff --git src/wp-includes/js/customize-selective-refresh.js src/wp-includes/js/customize-selective-refresh.js
    new file mode 100644
    index 0000000..3da6144
    - +  
     1/* global jQuery, JSON, _customizePartialRefreshExports, console */
     2
     3wp.customize.selectiveRefresh = ( function( $, api ) {
     4        'use strict';
     5        var self, Partial, Placement;
     6
     7        self = {
     8                ready: $.Deferred(),
     9                data: {
     10                        partials: {},
     11                        renderQueryVar: '',
     12                        l10n: {
     13                                shiftClickToEdit: ''
     14                        },
     15                        refreshBuffer: 250
     16                },
     17                currentRequest: null
     18        };
     19
     20        _.extend( self, api.Events );
     21
     22        /**
     23         * A Customizer Partial.
     24         *
     25         * A partial provides a rendering of one or more settings according to a template.
     26         *
     27         * @see PHP class WP_Customize_Partial.
     28         *
     29         * @class
     30         * @augments wp.customize.Class
     31         * @since 4.5.0
     32         *
     33         * @param {string} id                              Unique identifier for the control instance.
     34         * @param {object} options                         Options hash for the control instance.
     35         * @param {object} options.params
     36         * @param {string} options.params.type             Type of partial (e.g. nav_menu, widget, etc)
     37         * @param {string} options.params.selector         jQuery selector to find the container element in the page.
     38         * @param {array}  options.params.settings         The IDs for the settings the partial relates to.
     39         * @param {string} options.params.primarySetting   The ID for the primary setting the partial renders.
     40         * @param {bool}   options.params.fallbackRefresh  Whether to refresh the entire preview in case of a partial refresh failure.
     41         */
     42        Partial = self.Partial = api.Class.extend({
     43
     44                id: null,
     45
     46                 /**
     47                 * Constructor.
     48                 *
     49                 * @since 4.5.0
     50                 *
     51                 * @param {string} id - Partial ID.
     52                 * @param {Object} options
     53                 * @param {Object} options.params
     54                 */
     55                initialize: function( id, options ) {
     56                        var partial = this;
     57                        options = options || {};
     58                        partial.id = id;
     59
     60                        partial.params = _.extend(
     61                                {
     62                                        selector: null,
     63                                        settings: [],
     64                                        primarySetting: null,
     65                                        containerInclusive: false,
     66                                        fallbackRefresh: true // Note this needs to be false in a frontend editing context.
     67                                },
     68                                options.params || {}
     69                        );
     70
     71                        partial.deferred = {};
     72                        partial.deferred.ready = $.Deferred();
     73
     74                        partial.deferred.ready.done( function() {
     75                                partial.ready();
     76                        } );
     77                },
     78
     79                /**
     80                 * Set up the partial.
     81                 *
     82                 * @since 4.5.0
     83                 */
     84                ready: function() {
     85                        var partial = this;
     86                        _.each( _.pluck( partial.placements(), 'container' ), function( container ) {
     87                                $( container ).attr( 'title', self.data.l10n.shiftClickToEdit );
     88                        } );
     89                        $( document ).on( 'click', partial.params.selector, function( e ) {
     90                                if ( ! e.shiftKey ) {
     91                                        return;
     92                                }
     93                                e.preventDefault();
     94                                _.each( partial.placements(), function( placement ) {
     95                                        if ( $( placement.container ).is( e.currentTarget ) ) {
     96                                                partial.showControl();
     97                                        }
     98                                } );
     99                        } );
     100                },
     101
     102                /**
     103                 * Find all placements for this partial int he document.
     104                 *
     105                 * @since 4.5.0
     106                 *
     107                 * @return {Array.<Placement>}
     108                 */
     109                placements: function() {
     110                        var partial = this, selector;
     111
     112                        selector = partial.params.selector;
     113                        if ( selector ) {
     114                                selector += ', ';
     115                        }
     116                        selector += '[data-customize-partial-id="' + partial.id + '"]'; // @todo Consider injecting customize-partial-id-${id} classnames instead.
     117
     118                        return $( selector ).map( function() {
     119                                var container = $( this ), context;
     120
     121                                context = container.data( 'customize-partial-placement-context' );
     122                                if ( _.isString( context ) && '{' === context.substr( 0, 1 ) ) {
     123                                        throw new Error( 'context JSON parse error' );
     124                                }
     125
     126                                return new Placement( {
     127                                        partial: partial,
     128                                        container: container,
     129                                        context: context
     130                                } );
     131                        } ).get();
     132                },
     133
     134                /**
     135                 * Get list of setting IDs related to this partial.
     136                 *
     137                 * @since 4.5.0
     138                 *
     139                 * @return {String[]}
     140                 */
     141                settings: function() {
     142                        var partial = this;
     143                        if ( partial.params.settings && 0 !== partial.params.settings.length ) {
     144                                return partial.params.settings;
     145                        } else if ( partial.params.primarySetting ) {
     146                                return [ partial.params.primarySetting ];
     147                        } else {
     148                                return [ partial.id ];
     149                        }
     150                },
     151
     152                /**
     153                 * Return whether the setting is related to the partial.
     154                 *
     155                 * @since 4.5.0
     156                 *
     157                 * @param {wp.customize.Value|string} setting  ID or object for setting.
     158                 * @return {boolean} Whether the setting is related to the partial.
     159                 */
     160                isRelatedSetting: function( setting /*... newValue, oldValue */ ) {
     161                        var partial = this;
     162                        if ( _.isString( setting ) ) {
     163                                setting = api( setting );
     164                        }
     165                        if ( ! setting ) {
     166                                return false;
     167                        }
     168                        return -1 !== _.indexOf( partial.settings(), setting.id );
     169                },
     170
     171                /**
     172                 * Show the control to modify this partial's setting(s).
     173                 *
     174                 * This may be overridden for inline editing.
     175                 *
     176                 * @since 4.5.0
     177                 */
     178                showControl: function() {
     179                        var partial = this, settingId = partial.params.primarySetting;
     180                        if ( ! settingId ) {
     181                                settingId = _.first( partial.settings() );
     182                        }
     183                        api.preview.send( 'focus-control-for-setting', settingId );
     184                },
     185
     186                /**
     187                 * Prepare container for selective refresh.
     188                 *
     189                 * @since 4.5.0
     190                 *
     191                 * @param {Placement} placement
     192                 */
     193                preparePlacement: function( placement ) {
     194                        $( placement.container ).addClass( 'customize-partial-refreshing' );
     195                },
     196
     197                /**
     198                 * Reference to the pending promise returned from self.requestPartial().
     199                 *
     200                 * @since 4.5.0
     201                 * @private
     202                 */
     203                _pendingRefreshPromise: null,
     204
     205                /**
     206                 * Request the new partial and render it into the placements.
     207                 *
     208                 * @since 4.5.0
     209                 *
     210                 * @this {wp.customize.selectiveRefresh.Partial}
     211                 * @return {jQuery.Promise}
     212                 */
     213                refresh: function() {
     214                        var partial = this, refreshPromise;
     215
     216                        refreshPromise = self.requestPartial( partial );
     217
     218                        if ( ! partial._pendingRefreshPromise ) {
     219                                _.each( partial.placements(), function( placement ) {
     220                                        partial.preparePlacement( placement );
     221                                } );
     222
     223                                refreshPromise.done( function( placements ) {
     224                                        _.each( placements, function( placement ) {
     225                                                partial.renderContent( placement );
     226                                        } );
     227                                } );
     228
     229                                refreshPromise.fail( function( data, placements ) {
     230                                        partial.fallback( data, placements );
     231                                } );
     232
     233                                // Allow new request when this one finishes.
     234                                partial._pendingRefreshPromise = refreshPromise;
     235                                refreshPromise.always( function() {
     236                                        partial._pendingRefreshPromise = null;
     237                                } );
     238                        }
     239
     240                        return refreshPromise;
     241                },
     242
     243                /**
     244                 * Apply the addedContent in the placement to the document.
     245                 *
     246                 * Note the placement object will have its container and removedNodes
     247                 * properties updated.
     248                 *
     249                 * @since 4.5.0
     250                 *
     251                 * @param {Placement}             placement
     252                 * @param {Element|jQuery}        [placement.container]  - This param will be empty if there was no element matching the selector.
     253                 * @param {string|object|boolean} placement.addedContent - Rendered HTML content, a data object for JS templates to render, or false if no render.
     254                 * @param {object}                [placement.context]    - Optional context information about the container.
     255                 * @returns {boolean} Whether the rendering was successful and the fallback was not invoked.
     256                 */
     257                renderContent: function( placement ) {
     258                        var partial = this, content, newContainerElement;
     259                        if ( ! placement.container ) {
     260                                partial.fallback( new Error( 'no_container' ), [ placement ] );
     261                                return false;
     262                        }
     263                        placement.container = $( placement.container );
     264                        if ( false === placement.addedContent ) {
     265                                partial.fallback( new Error( 'missing_render' ), [ placement ] );
     266                                return false;
     267                        }
     268
     269                        // Currently a subclass needs to override renderContent to handle partials returning data object.
     270                        if ( ! _.isString( placement.addedContent ) ) {
     271                                partial.fallback( new Error( 'non_string_content' ), [ placement ] );
     272                                return false;
     273                        }
     274
     275                        content = placement.addedContent;
     276                        if ( wp.emoji && wp.emoji.parse && ! $.contains( document.head, placement.container[0] ) ) {
     277                                content = wp.emoji.parse( content );
     278                        }
     279
     280                        // @todo Should containerInclusive be context information as opposed to a param?
     281                        if ( partial.params.containerInclusive ) {
     282
     283                                // Note that content may be an empty string, and in this case jQuery will just remove the oldContainer
     284                                newContainerElement = $( content );
     285
     286                                // Merge the new context on top of the old context.
     287                                placement.context = _.extend(
     288                                        placement.context,
     289                                        newContainerElement.data( 'customize-partial-placement-context' ) || {}
     290                                );
     291                                newContainerElement.data( 'customize-partial-placement-context', placement.context );
     292
     293                                placement.removedNodes = placement.container;
     294                                placement.container = newContainerElement;
     295                                placement.removedNodes.replaceWith( placement.container );
     296                                placement.container.attr( 'title', self.data.l10n.shiftClickToEdit );
     297                        } else {
     298                                placement.removedNodes = document.createDocumentFragment();
     299                                while ( placement.container[0].firstChild ) {
     300                                        placement.removedNodes.appendChild( placement.container[0].firstChild );
     301                                }
     302
     303                                placement.container.html( content );
     304                        }
     305
     306                        placement.container.removeClass( 'customize-partial-refreshing' );
     307
     308                        // Prevent placement container from being being re-triggered as being rendered among nested partials.
     309                        placement.container.data( 'customize-partial-content-rendered', true );
     310
     311                        /**
     312                         * Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
     313                         */
     314                        self.trigger( 'partial-content-rendered', placement );
     315                        return true;
     316                },
     317
     318                /**
     319                 * Handle fail to render partial.
     320                 *
     321                 * The first argument is either the failing jqXHR or an Error object, and the second argument is the array of containers.
     322                 *
     323                 * @since 4.5.0
     324                 */
     325                fallback: function() {
     326                        var partial = this;
     327                        if ( partial.params.fallbackRefresh ) {
     328                                self.requestFullRefresh();
     329                        }
     330                }
     331        } );
     332
     333        /**
     334         * A Placement for a Partial.
     335         *
     336         * A partial placement is the actual physical representation of a partial for a given context.
     337         * It also may have information in relation to how a placement may have just changed.
     338         * The placement is conceptually similar to a DOM Range or MutationRecord.
     339         *
     340         * @class
     341         * @augments wp.customize.Class
     342         * @since 4.5.0
     343         */
     344        self.Placement = Placement = api.Class.extend({
     345
     346                /**
     347                 * The partial with which the container is associated.
     348                 *
     349                 * @param {wp.customize.selectiveRefresh.Partial}
     350                 */
     351                partial: null,
     352
     353                /**
     354                 * DOM element which contains the placement's contents.
     355                 *
     356                 * This will be null if the startNode and endNode do not point to the same
     357                 * DOM element, such as in the case of a sidebar partial.
     358                 * This container element itself will be replaced for partials that
     359                 * have containerInclusive param defined as true.
     360                 */
     361                container: null,
     362
     363                /**
     364                 * DOM node for the initial boundary of the placement.
     365                 *
     366                 * This will normally be the same as endNode since most placements appear as elements.
     367                 * This is primarily useful for widget sidebars which do not have intrinsic containers, but
     368                 * for which an HTML comment is output before to mark the starting position.
     369                 */
     370                startNode: null,
     371
     372                /**
     373                 * DOM node for the terminal boundary of the placement.
     374                 *
     375                 * This will normally be the same as startNode since most placements appear as elements.
     376                 * This is primarily useful for widget sidebars which do not have intrinsic containers, but
     377                 * for which an HTML comment is output before to mark the ending position.
     378                 */
     379                endNode: null,
     380
     381                /**
     382                 * Context data.
     383                 *
     384                 * This provides information about the placement which is included in the request
     385                 * in order to render the partial properly.
     386                 *
     387                 * @param {object}
     388                 */
     389                context: null,
     390
     391                /**
     392                 * The content for the partial when refreshed.
     393                 *
     394                 * @param {string}
     395                 */
     396                addedContent: null,
     397
     398                /**
     399                 * DOM node(s) removed when the partial is refreshed.
     400                 *
     401                 * If the partial is containerInclusive, then the removedNodes will be
     402                 * the single Element that was the partial's former placement. If the
     403                 * partial is not containerInclusive, then the removedNodes will be a
     404                 * documentFragment containing the nodes removed.
     405                 *
     406                 * @param {Element|DocumentFragment}
     407                 */
     408                removedNodes: null,
     409
     410                /**
     411                 * Constructor.
     412                 *
     413                 * @since 4.5.0
     414                 *
     415                 * @param {object}                   args
     416                 * @param {Partial}                  args.partial
     417                 * @param {jQuery|Element}           [args.container]
     418                 * @param {Node}                     [args.startNode]
     419                 * @param {Node}                     [args.endNode]
     420                 * @param {object}                   [args.context]
     421                 * @param {string}                   [args.addedContent]
     422                 * @param {jQuery|DocumentFragment}  [args.removedNodes]
     423                 */
     424                initialize: function( args ) {
     425                        var placement = this;
     426
     427                        args = _.extend( {}, args || {} );
     428                        if ( ! args.partial || ! args.partial.extended( Partial ) ) {
     429                                throw new Error( 'Missing partial' );
     430                        }
     431                        args.context = args.context || {};
     432                        if ( args.container ) {
     433                                args.container = $( args.container );
     434                        }
     435
     436                        _.extend( placement, args );
     437                }
     438
     439        });
     440
     441        /**
     442         * Mapping of type names to Partial constructor subclasses.
     443         *
     444         * @since 4.5.0
     445         *
     446         * @type {Object.<string, wp.customize.selectiveRefresh.Partial>}
     447         */
     448        self.partialConstructor = {};
     449
     450        self.partial = new api.Values({ defaultConstructor: Partial });
     451
     452        /**
     453         * Get the POST vars for a Customizer preview request.
     454         *
     455         * @since 4.5.0
     456         * @see wp.customize.previewer.query()
     457         *
     458         * @return {object}
     459         */
     460        self.getCustomizeQuery = function() {
     461                var dirtyCustomized = {};
     462                api.each( function( value, key ) {
     463                        if ( value._dirty ) {
     464                                dirtyCustomized[ key ] = value();
     465                        }
     466                } );
     467
     468                return {
     469                        wp_customize: 'on',
     470                        nonce: api.settings.nonce.preview,
     471                        theme: api.settings.theme.stylesheet,
     472                        customized: JSON.stringify( dirtyCustomized )
     473                };
     474        };
     475
     476        /**
     477         * Currently-requested partials and their associated deferreds.
     478         *
     479         * @since 4.5.0
     480         * @type {Object<string, { deferred: jQuery.Promise, partial: wp.customize.selectiveRefresh.Partial }>}
     481         */
     482        self._pendingPartialRequests = {};
     483
     484        /**
     485         * Timeout ID for the current requesr, or null if no request is current.
     486         *
     487         * @since 4.5.0
     488         * @type {number|null}
     489         * @private
     490         */
     491        self._debouncedTimeoutId = null;
     492
     493        /**
     494         * Current jqXHR for the request to the partials.
     495         *
     496         * @since 4.5.0
     497         * @type {jQuery.jqXHR|null}
     498         * @private
     499         */
     500        self._currentRequest = null;
     501
     502        /**
     503         * Request full page refresh.
     504         *
     505         * When selective refresh is embedded in the context of frontend editing, this request
     506         * must fail or else changes will be lost, unless transactions are implemented.
     507         *
     508         * @since 4.5.0
     509         */
     510        self.requestFullRefresh = function() {
     511                api.preview.send( 'refresh' );
     512        };
     513
     514        /**
     515         * Request a re-rendering of a partial.
     516         *
     517         * @since 4.5.0
     518         *
     519         * @param {wp.customize.selectiveRefresh.Partial} partial
     520         * @return {jQuery.Promise}
     521         */
     522        self.requestPartial = function( partial ) {
     523                var partialRequest;
     524
     525                if ( self._debouncedTimeoutId ) {
     526                        clearTimeout( self._debouncedTimeoutId );
     527                        self._debouncedTimeoutId = null;
     528                }
     529                if ( self._currentRequest ) {
     530                        self._currentRequest.abort();
     531                        self._currentRequest = null;
     532                }
     533
     534                partialRequest = self._pendingPartialRequests[ partial.id ];
     535                if ( ! partialRequest || 'pending' !== partialRequest.deferred.state() ) {
     536                        partialRequest = {
     537                                deferred: $.Deferred(),
     538                                partial: partial
     539                        };
     540                        self._pendingPartialRequests[ partial.id ] = partialRequest;
     541                }
     542
     543                // Prevent leaking partial into debounced timeout callback.
     544                partial = null;
     545
     546                self._debouncedTimeoutId = setTimeout(
     547                        function() {
     548                                var data, partialPlacementContexts, partialsPlacements, request;
     549
     550                                self._debouncedTimeoutId = null;
     551                                data = self.getCustomizeQuery();
     552
     553                                /*
     554                                 * It is key that the containers be fetched exactly at the point of the request being
     555                                 * made, because the containers need to be mapped to responses by array indices.
     556                                 */
     557                                partialsPlacements = {};
     558
     559                                partialPlacementContexts = {};
     560
     561                                _.each( self._pendingPartialRequests, function( pending, partialId ) {
     562                                        partialsPlacements[ partialId ] = pending.partial.placements();
     563                                        if ( ! self.partial.has( partialId ) ) {
     564                                                pending.deferred.rejectWith( pending.partial, [ new Error( 'partial_removed' ), partialsPlacements[ partialId ] ] );
     565                                        } else {
     566                                                /*
     567                                                 * Note that this may in fact be an empty array. In that case, it is the responsibility
     568                                                 * of the Partial subclass instance to know where to inject the response, or else to
     569                                                 * just issue a refresh (default behavior). The data being returned with each container
     570                                                 * is the context information that may be needed to render certain partials, such as
     571                                                 * the contained sidebar for rendering widgets or what the nav menu args are for a menu.
     572                                                 */
     573                                                partialPlacementContexts[ partialId ] = _.map( partialsPlacements[ partialId ], function( placement ) {
     574                                                        return placement.context || {};
     575                                                } );
     576                                        }
     577                                } );
     578
     579                                data.partials = JSON.stringify( partialPlacementContexts );
     580                                data[ self.data.renderQueryVar ] = '1';
     581
     582                                request = self._currentRequest = wp.ajax.send( null, {
     583                                        data: data,
     584                                        url: api.settings.url.self
     585                                } );
     586
     587                                request.done( function( data ) {
     588
     589                                        /**
     590                                         * Announce the data returned from a request to render partials.
     591                                         *
     592                                         * The data is filtered on the server via customize_render_partials_response
     593                                         * so plugins can inject data from the server to be utilized
     594                                         * on the client via this event. Plugins may use this filter
     595                                         * to communicate script and style dependencies that need to get
     596                                         * injected into the page to support the rendered partials.
     597                                         * This is similar to the 'saved' event.
     598                                         */
     599                                        self.trigger( 'render-partials-response', data );
     600
     601                                        // Relay errors (warnings) captured during rendering and relay to console.
     602                                        if ( data.errors && 'undefined' !== typeof console && console.warn ) {
     603                                                _.each( data.errors, function( error ) {
     604                                                        console.warn( error );
     605                                                } );
     606                                        }
     607
     608                                        /*
     609                                         * Note that data is an array of items that correspond to the array of
     610                                         * containers that were submitted in the request. So we zip up the
     611                                         * array of containers with the array of contents for those containers,
     612                                         * and send them into .
     613                                         */
     614                                        _.each( self._pendingPartialRequests, function( pending, partialId ) {
     615                                                var placementsContents;
     616                                                if ( ! _.isArray( data.contents[ partialId ] ) ) {
     617                                                        pending.deferred.rejectWith( pending.partial, [ new Error( 'unrecognized_partial' ), partialsPlacements[ partialId ] ] );
     618                                                } else {
     619                                                        placementsContents = _.map( data.contents[ partialId ], function( content, i ) {
     620                                                                var partialPlacement = partialsPlacements[ partialId ][ i ];
     621                                                                if ( partialPlacement ) {
     622                                                                        partialPlacement.addedContent = content;
     623                                                                } else {
     624                                                                        partialPlacement = new Placement( {
     625                                                                                partial: pending.partial,
     626                                                                                addedContent: content
     627                                                                        } );
     628                                                                }
     629                                                                return partialPlacement;
     630                                                        } );
     631                                                        pending.deferred.resolveWith( pending.partial, [ placementsContents ] );
     632                                                }
     633                                        } );
     634                                        self._pendingPartialRequests = {};
     635                                } );
     636
     637                                request.fail( function( data, statusText ) {
     638
     639                                        /*
     640                                         * Ignore failures caused by partial.currentRequest.abort()
     641                                         * The pending deferreds will remain in self._pendingPartialRequests
     642                                         * for re-use with the next request.
     643                                         */
     644                                        if ( 'abort' === statusText ) {
     645                                                return;
     646                                        }
     647
     648                                        _.each( self._pendingPartialRequests, function( pending, partialId ) {
     649                                                pending.deferred.rejectWith( pending.partial, [ data, partialsPlacements[ partialId ] ] );
     650                                        } );
     651                                        self._pendingPartialRequests = {};
     652                                } );
     653                        },
     654                        self.data.refreshBuffer
     655                );
     656
     657                return partialRequest.deferred.promise();
     658        };
     659
     660        /**
     661         * Add partials for any nav menu container elements in the document.
     662         *
     663         * This method may be called multiple times. Containers that already have been
     664         * seen will be skipped.
     665         *
     666         * @since 4.5.0
     667         *
     668         * @param {jQuery|HTMLElement} [rootElement]
     669         * @param {object}             [options]
     670         * @param {boolean=true}       [options.triggerRendered]
     671         */
     672        self.addPartials = function( rootElement, options ) {
     673                var containerElements;
     674                if ( ! rootElement ) {
     675                        rootElement = document.documentElement;
     676                }
     677                rootElement = $( rootElement );
     678                options = _.extend(
     679                        {
     680                                triggerRendered: true
     681                        },
     682                        options || {}
     683                );
     684
     685                containerElements = rootElement.find( '[data-customize-partial-id]' );
     686                if ( rootElement.is( '[data-customize-partial-id]' ) ) {
     687                        containerElements = containerElements.add( rootElement );
     688                }
     689                containerElements.each( function() {
     690                        var containerElement = $( this ), partial, id, Constructor, partialOptions, containerContext;
     691                        id = containerElement.data( 'customize-partial-id' );
     692                        if ( ! id ) {
     693                                return;
     694                        }
     695                        containerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
     696
     697                        partial = self.partial( id );
     698                        if ( ! partial ) {
     699                                partialOptions = containerElement.data( 'customize-partial-options' ) || {};
     700                                partialOptions.constructingContainerContext = containerElement.data( 'customize-partial-placement-context' ) || {};
     701                                Constructor = self.partialConstructor[ containerElement.data( 'customize-partial-type' ) ] || self.Partial;
     702                                partial = new Constructor( id, partialOptions );
     703                                self.partial.add( partial.id, partial );
     704                        }
     705
     706                        /*
     707                         * Only trigger renders on (nested) partials that have been not been
     708                         * handled yet. An example where this would apply is a nav menu
     709                         * embedded inside of a custom menu widget. When the widget's title
     710                         * is updated, the entire widget will re-render and then the event
     711                         * will be triggered for the nested nav menu to do any initialization.
     712                         */
     713                        if ( options.triggerRendered && ! containerElement.data( 'customize-partial-content-rendered' ) ) {
     714
     715                                /**
     716                                 * Announce when a partial's nested placement has been re-rendered.
     717                                 */
     718                                self.trigger( 'partial-content-rendered', new Placement( {
     719                                        partial: partial,
     720                                        context: containerContext,
     721                                        container: containerElement
     722                                } ) );
     723                        }
     724                        containerElement.data( 'customize-partial-content-rendered', true );
     725                } );
     726        };
     727
     728        api.bind( 'preview-ready', function() {
     729                var handleSettingChange, watchSettingChange, unwatchSettingChange;
     730
     731                // Polyfill for IE8 to support the document.head attribute.
     732                if ( ! document.head ) {
     733                        document.head = $( 'head:first' )[0];
     734                }
     735
     736                _.extend( self.data, _customizePartialRefreshExports );
     737
     738                // Create the partial JS models.
     739                _.each( self.data.partials, function( data, id ) {
     740                        var Constructor, partial = self.partial( id );
     741                        if ( ! partial ) {
     742                                Constructor = self.partialConstructor[ data.type ] || self.Partial;
     743                                partial = new Constructor( id, { params: data } );
     744                                self.partial.add( id, partial );
     745                        } else {
     746                                _.extend( partial.params, data );
     747                        }
     748                } );
     749
     750                /**
     751                 * Handle change to a setting.
     752                 *
     753                 * Note this is largely needed because adding a 'change' event handler to wp.customize
     754                 * will only include the changed setting object as an argument, not including the
     755                 * new value or the old value.
     756                 *
     757                 * @since 4.5.0
     758                 * @this {wp.customize.Setting}
     759                 *
     760                 * @param {*|null} newValue New value, or null if the setting was just removed.
     761                 * @param {*|null} oldValue Old value, or null if the setting was just added.
     762                 */
     763                handleSettingChange = function( newValue, oldValue ) {
     764                        var setting = this;
     765                        self.partial.each( function( partial ) {
     766                                if ( partial.isRelatedSetting( setting, newValue, oldValue ) ) {
     767                                        partial.refresh();
     768                                }
     769                        } );
     770                };
     771
     772                /**
     773                 * Trigger the initial change for the added setting, and watch for changes.
     774                 *
     775                 * @since 4.5.0
     776                 * @this {wp.customize.Values}
     777                 *
     778                 * @param {wp.customize.Setting} setting
     779                 */
     780                watchSettingChange = function( setting ) {
     781                        handleSettingChange.call( setting, setting(), null );
     782                        setting.bind( handleSettingChange );
     783                };
     784
     785                /**
     786                 * Trigger the final change for the removed setting, and unwatch for changes.
     787                 *
     788                 * @since 4.5.0
     789                 * @this {wp.customize.Values}
     790                 *
     791                 * @param {wp.customize.Setting} setting
     792                 */
     793                unwatchSettingChange = function( setting ) {
     794                        handleSettingChange.call( setting, null, setting() );
     795                        setting.unbind( handleSettingChange );
     796                };
     797
     798                api.bind( 'add', watchSettingChange );
     799                api.bind( 'remove', unwatchSettingChange );
     800                api.each( function( setting ) {
     801                        setting.bind( handleSettingChange );
     802                } );
     803
     804                // Add (dynamic) initial partials that are declared via data-* attributes.
     805                self.addPartials( document.documentElement, {
     806                        triggerRendered: false
     807                } );
     808
     809                // Add new dynamic partials when the document changes.
     810                if ( 'undefined' !== typeof MutationObserver ) {
     811                        self.mutationObserver = new MutationObserver( function( mutations ) {
     812                                _.each( mutations, function( mutation ) {
     813                                        self.addPartials( $( mutation.target ) );
     814                                } );
     815                        } );
     816                        self.mutationObserver.observe( document.documentElement, {
     817                                childList: true,
     818                                subtree: true
     819                        } );
     820                }
     821
     822                /**
     823                 * Handle rendering of partials.
     824                 *
     825                 * @param {api.selectiveRefresh.Placement} placement
     826                 */
     827                api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     828                        if ( placement.container ) {
     829                                self.addPartials( placement.container );
     830                        }
     831                } );
     832
     833                api.preview.bind( 'active', function() {
     834
     835                        // Make all partials ready.
     836                        self.partial.each( function( partial ) {
     837                                partial.deferred.ready.resolve();
     838                        } );
     839
     840                        // Make all partials added henceforth as ready upon add.
     841                        self.partial.bind( 'add', function( partial ) {
     842                                partial.deferred.ready.resolve();
     843                        } );
     844                } );
     845
     846        } );
     847
     848        return self;
     849}( jQuery, wp.customize ) );
  • src/wp-includes/script-loader.php

    diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
    index 1a9d259..01be0f5 100644
    function wp_default_scripts( &$scripts ) { 
    447447                // Used for overriding the file types allowed in plupload.
    448448                'allowedFiles'       => __( 'Allowed Files' ),
    449449        ) );
     450        $scripts->add( 'customize-selective-refresh', "/wp-includes/js/customize-selective-refresh$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
    450451
    451452        $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 );
    452453        $scripts->add( 'customize-preview-widgets', "/wp-includes/js/customize-preview-widgets$suffix.js", array( 'jquery', 'wp-util', 'customize-preview' ), false, 1 );
  • tests/phpunit/tests/customize/manager.php

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

    diff --git tests/phpunit/tests/customize/nav-menu-item-setting.php tests/phpunit/tests/customize/nav-menu-item-setting.php
    index 39ed42e..3431ef8 100644
    class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { 
    6969
    7070                $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, 'nav_menu_item[123]' );
    7171                $this->assertEquals( 'nav_menu_item', $setting->type );
    72                 $this->assertEquals( 'postMessage', $setting->transport );
    7372                $this->assertEquals( 123, $setting->post_id );
    7473                $this->assertNull( $setting->previous_post_id );
    7574                $this->assertNull( $setting->update_status );
  • tests/phpunit/tests/customize/nav-menus.php

    diff --git tests/phpunit/tests/customize/nav-menus.php tests/phpunit/tests/customize/nav-menus.php
    index 2969a2d..61982ca 100644
    class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 
    353353
    354354                $expected = array( 'type' => 'nav_menu_item' );
    355355                $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu_item[123]' );
    356                 $this->assertEquals( $expected, $results );
     356                $this->assertEquals( $expected['type'], $results['type'] );
    357357
    358358                $expected = array( 'type' => 'nav_menu' );
    359359                $results = $menus->filter_dynamic_setting_args( $this->wp_customize, 'nav_menu[123]' );
    360                 $this->assertEquals( $expected, $results );
     360                $this->assertEquals( $expected['type'], $results['type'] );
    361361        }
    362362
    363363        /**
    class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 
    523523        }
    524524
    525525        /**
     526         * Test WP_Customize_Nav_Menus::customize_dynamic_partial_args().
     527         *
     528         * @see WP_Customize_Nav_Menus::customize_dynamic_partial_args()
     529         */
     530        function test_customize_dynamic_partial_args() {
     531                do_action( 'customize_register', $this->wp_customize );
     532
     533                $args = apply_filters( 'customize_dynamic_partial_args', false, 'nav_menu_instance[68b329da9893e34099c7d8ad5cb9c940]' );
     534                $this->assertInternalType( 'array', $args );
     535                $this->assertEquals( 'nav_menu_instance', $args['type'] );
     536                $this->assertEquals( array( $this->wp_customize->nav_menus, 'render_nav_menu_partial' ), $args['render_callback'] );
     537                $this->assertTrue( $args['container_inclusive'] );
     538
     539                $args = apply_filters( 'customize_dynamic_partial_args', array( 'fallback_refresh' => false ), 'nav_menu_instance[4099c7d8ad5cb9c94068b329da9893e3]' );
     540                $this->assertInternalType( 'array', $args );
     541                $this->assertEquals( 'nav_menu_instance', $args['type'] );
     542                $this->assertEquals( array( $this->wp_customize->nav_menus, 'render_nav_menu_partial' ), $args['render_callback'] );
     543                $this->assertTrue( $args['container_inclusive'] );
     544                $this->assertFalse( $args['fallback_refresh'] );
     545        }
     546
     547        /**
    526548         * Test the customize_preview_init method.
    527549         *
    528550         * @see WP_Customize_Nav_Menus::customize_preview_init()
    class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 
    532554                $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
    533555
    534556                $menus->customize_preview_init();
    535                 $this->assertEquals( 10, has_action( 'template_redirect', array( $menus, 'render_menu' ) ) );
    536557                $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $menus, 'customize_preview_enqueue_deps' ) ) );
    537 
    538                 if ( ! isset( $_REQUEST[ WP_Customize_Nav_Menus::RENDER_QUERY_VAR ] ) ) {
    539                         $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) );
    540                         $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
    541                 }
     558                $this->assertEquals( 1000, has_filter( 'wp_nav_menu_args', array( $menus, 'filter_wp_nav_menu_args' ) ) );
     559                $this->assertEquals( 10, has_filter( 'wp_nav_menu', array( $menus, 'filter_wp_nav_menu' ) ) );
    542560        }
    543561
    544562        /**
    class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 
    548566         */
    549567        function test_filter_wp_nav_menu_args() {
    550568                do_action( 'customize_register', $this->wp_customize );
    551                 $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     569                $menus = $this->wp_customize->nav_menus;
    552570
    553571                $results = $menus->filter_wp_nav_menu_args( array(
    554572                        'echo'            => true,
    555573                        'fallback_cb'     => 'wp_page_menu',
    556574                        'walker'          => '',
    557575                        'menu'            => wp_create_nav_menu( 'Foo' ),
     576                        'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    558577                ) );
    559                 $this->assertEquals( 1, $results['can_partial_refresh'] );
     578                $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
    560579
    561                 $expected = array(
    562                         'echo',
    563                         'can_partial_refresh',
    564                         'fallback_cb',
    565                         'instance_number',
    566                         'walker',
    567                 );
    568580                $results = $menus->filter_wp_nav_menu_args( array(
    569581                        'echo'            => false,
    570582                        'fallback_cb'     => 'wp_page_menu',
    571583                        'walker'          => new Walker_Nav_Menu(),
     584                        'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    572585                ) );
    573                 $this->assertEqualSets( $expected, array_keys( $results ) );
     586                $this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results );
    574587                $this->assertEquals( 'wp_page_menu', $results['fallback_cb'] );
    575                 $this->assertEquals( 0, $results['can_partial_refresh'] );
    576588
    577                 $this->assertNotEmpty( $menus->preview_nav_menu_instance_args[ $results['instance_number'] ] );
    578                 $preview_nav_menu_instance_args = $menus->preview_nav_menu_instance_args[ $results['instance_number'] ];
    579                 $this->assertEquals( '', $preview_nav_menu_instance_args['fallback_cb'] );
    580                 $this->assertEquals( '', $preview_nav_menu_instance_args['walker'] );
    581                 $this->assertNotEmpty( $preview_nav_menu_instance_args['args_hash'] );
     589                $nav_menu_term = get_term( wp_create_nav_menu( 'Bar' ) );
     590                $results = $menus->filter_wp_nav_menu_args( array(
     591                        'echo'            => true,
     592                        'fallback_cb'     => 'wp_page_menu',
     593                        'walker'          => '',
     594                        'menu'            => $nav_menu_term,
     595                        'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
     596                ) );
     597                $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
     598                $this->assertEquals( $nav_menu_term->term_id, $results['customize_preview_nav_menus_args']['menu'] );
    582599        }
    583600
    584601        /**
    class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 
    595612                        'menu'        => wp_create_nav_menu( 'Foo' ),
    596613                        'fallback_cb' => 'wp_page_menu',
    597614                        'walker'      => '',
     615                        'items_wrap'  => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    598616                ) );
    599617
    600618                ob_start();
    601619                wp_nav_menu( $args );
    602620                $nav_menu_content = ob_get_clean();
    603621
    604                 $object_args = json_decode( json_encode( $args ), false );
    605                 $result = $menus->filter_wp_nav_menu( $nav_menu_content, $object_args );
    606                 $expected = sprintf(
    607                         '<div class="partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d menu">',
    608                         $args['instance_number']
    609                 );
    610                 $this->assertStringStartsWith( $expected, $result );
     622                $result = $menus->filter_wp_nav_menu( $nav_menu_content, (object) $args );
     623
     624                $this->assertContains( sprintf( ' data-customize-partial-id="nav_menu_instance[%s]"', $args['customize_preview_nav_menus_args']['args_hmac'] ), $result );
     625                $this->assertContains( ' data-customize-partial-type="nav_menu_instance"', $result );
     626                $this->assertContains( ' data-customize-partial-placement-context="', $result );
    611627        }
    612628
    613629        /**
    class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase { 
    622638                $menus->customize_preview_enqueue_deps();
    623639
    624640                $this->assertTrue( wp_script_is( 'customize-preview-nav-menus' ) );
    625                 $this->assertEquals( 10, has_action( 'wp_print_footer_scripts', array( $menus, 'export_preview_data' ) ) );
    626641        }
    627642
    628643        /**
    629          * Test the export_preview_data method.
     644         * Test WP_Customize_Nav_Menus::export_preview_data() method.
    630645         *
    631646         * @see WP_Customize_Nav_Menus::export_preview_data()
    632647         */
    633648        function test_export_preview_data() {
    634                 do_action( 'customize_register', $this->wp_customize );
    635                 $menus = new WP_Customize_Nav_Menus( $this->wp_customize );
     649                $this->setExpectedDeprecated( 'WP_Customize_Nav_Menus::export_preview_data' );
     650                $this->wp_customize->nav_menus->export_preview_data();
     651        }
    636652
    637                 $request_uri = $_SERVER['REQUEST_URI'];
     653        /**
     654         * Test WP_Customize_Nav_Menus::render_nav_menu_partial() method.
     655         *
     656         * @see WP_Customize_Nav_Menus::render_nav_menu_partial()
     657         */
     658        function test_render_nav_menu_partial() {
     659                $this->wp_customize->nav_menus->customize_preview_init();
     660
     661                $menu = wp_create_nav_menu( 'Foo' );
     662                wp_update_nav_menu_item( $menu, 0, array(
     663                        'menu-item-type' => 'custom',
     664                        'menu-item-title' => 'WordPress.org',
     665                        'menu-item-url' => 'https://wordpress.org',
     666                        'menu-item-status' => 'publish',
     667                ) );
    638668
    639                 ob_start();
    640                 $_SERVER['REQUEST_URI'] = '/wp-admin';
    641                 $menus->export_preview_data();
    642                 $data = ob_get_clean();
     669                $nav_menu_args = $this->wp_customize->nav_menus->filter_wp_nav_menu_args( array(
     670                        'echo'        => true,
     671                        'menu'        => $menu,
     672                        'fallback_cb' => 'wp_page_menu',
     673                        'walker'      => '',
     674                        'items_wrap'  => '<ul id="%1$s" class="%2$s">%3$s</ul>',
     675                ) );
    643676
    644                 $_SERVER['REQUEST_URI'] = $request_uri;
     677                $partial_id = sprintf( 'nav_menu_instance[%s]', $nav_menu_args['customize_preview_nav_menus_args']['args_hmac'] );
     678                $partials = $this->wp_customize->selective_refresh->add_dynamic_partials( array( $partial_id ) );
     679                $this->assertNotEmpty( $partials );
     680                $partial = array_shift( $partials );
     681                $this->assertEquals( $partial_id, $partial->id );
     682
     683                $missing_args_hmac_args = array_merge(
     684                        $nav_menu_args['customize_preview_nav_menus_args'],
     685                        array( 'args_hmac' => null )
     686                );
     687                $this->assertFalse( $partial->render( $missing_args_hmac_args ) );
     688
     689                $args_hmac_mismatch_args = array_merge(
     690                        $nav_menu_args['customize_preview_nav_menus_args'],
     691                        array( 'args_hmac' => strrev( $nav_menu_args['customize_preview_nav_menus_args']['args_hmac'] ) )
     692                );
     693                $this->assertFalse( $partial->render( $args_hmac_mismatch_args ) );
    645694
    646                 $this->assertContains( '_wpCustomizePreviewNavMenusExports', $data );
    647                 $this->assertContains( 'renderQueryVar', $data );
    648                 $this->assertContains( 'renderNonceValue', $data );
    649                 $this->assertContains( 'renderNoncePostKey', $data );
    650                 $this->assertContains( 'navMenuInstanceArgs', $data );
     695                $rendered = $partial->render( $nav_menu_args['customize_preview_nav_menus_args'] );
     696                $this->assertContains( 'data-customize-partial-type="nav_menu_instance"', $rendered );
     697                $this->assertContains( 'WordPress.org', $rendered );
    651698        }
    652699}
  • new file tests/phpunit/tests/customize/partial.php

    diff --git tests/phpunit/tests/customize/partial.php tests/phpunit/tests/customize/partial.php
    new file mode 100644
    index 0000000..6120355
    - +  
     1<?php
     2/**
     3 * Test_WP_Customize_Partial tests.
     4 *
     5 * @package WordPress
     6 */
     7
     8/**
     9 * Tests for the Test_WP_Customize_Partial class.
     10 *
     11 * @group customize
     12 */
     13class Test_WP_Customize_Partial extends WP_UnitTestCase {
     14
     15        /**
     16         * Manager.
     17         *
     18         * @var WP_Customize_Manager
     19         */
     20        public $wp_customize;
     21
     22        /**
     23         * Component.
     24         *
     25         * @var WP_Customize_Selective_Refresh
     26         */
     27        public $selective_refresh;
     28
     29        /**
     30         * Set up.
     31         */
     32        function setUp() {
     33                parent::setUp();
     34                require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
     35                // @codingStandardsIgnoreStart
     36                $GLOBALS['wp_customize'] = new WP_Customize_Manager();
     37                // @codingStandardsIgnoreEnd
     38                $this->wp_customize = $GLOBALS['wp_customize'];
     39                if ( isset( $this->wp_customize->selective_refresh ) ) {
     40                        $this->selective_refresh = $this->wp_customize->selective_refresh;
     41                }
     42        }
     43
     44        /**
     45         * Test WP_Customize_Partial::__construct().
     46         *
     47         * @see WP_Customize_Partial::__construct()
     48         */
     49        function test_construct_default_args() {
     50                $partial_id = 'blogname';
     51                $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id );
     52                $this->assertEquals( $partial_id, $partial->id );
     53                $this->assertEquals( $this->selective_refresh, $partial->component );
     54                $this->assertEquals( 'default', $partial->type );
     55                $this->assertEmpty( $partial->selector );
     56                $this->assertEquals( array( $partial_id ), $partial->settings );
     57                $this->assertEquals( $partial_id, $partial->primary_setting );
     58                $this->assertEquals( array( $partial, 'render_callback' ), $partial->render_callback );
     59                $this->assertEquals( false, $partial->container_inclusive );
     60                $this->assertEquals( true, $partial->fallback_refresh );
     61        }
     62
     63        /**
     64         * Render post content partial.
     65         *
     66         * @param WP_Customize_Partial $partial Partial.
     67         * @return string|false Content or false if error.
     68         */
     69        function render_post_content_partial( $partial ) {
     70                $id_data = $partial->id_data();
     71                $post_id = intval( $id_data['keys'][0] );
     72                if ( empty( $post_id ) ) {
     73                        return false;
     74                }
     75                $post = get_post( $post_id );
     76                if ( ! $post ) {
     77                        return false;
     78                }
     79                return apply_filters( 'the_content', $post->post_content );
     80        }
     81
     82        /**
     83         * Test WP_Customize_Partial::__construct().
     84         *
     85         * @see WP_Customize_Partial::__construct()
     86         */
     87        function test_construct_non_default_args() {
     88
     89                $post_id = self::factory()->post->create( array(
     90                        'post_title' => 'Hello World',
     91                        'post_content' => 'Lorem Ipsum',
     92                ) );
     93
     94                $partial_id = sprintf( 'post_content[%d]', $post_id );
     95                $args = array(
     96                        'type' => 'post',
     97                        'selector' => "article.post-$post_id .entry-content",
     98                        'settings' => array( 'user[1]', "post[$post_id]" ),
     99                        'primary_setting' => "post[$post_id]",
     100                        'render_callback' => array( $this, 'render_post_content_partial' ),
     101                        'container_inclusive' => false,
     102                        'fallback_refresh' => false,
     103                );
     104                $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id, $args );
     105                $this->assertEquals( $partial_id, $partial->id );
     106                $this->assertEquals( $this->selective_refresh, $partial->component );
     107                $this->assertEquals( $args['type'], $partial->type );
     108                $this->assertEquals( $args['selector'], $partial->selector );
     109                $this->assertEqualSets( $args['settings'], $partial->settings );
     110                $this->assertEquals( $args['primary_setting'], $partial->primary_setting );
     111                $this->assertEquals( $args['render_callback'], $partial->render_callback );
     112                $this->assertEquals( false, $partial->container_inclusive );
     113                $this->assertEquals( false, $partial->fallback_refresh );
     114                $this->assertContains( 'Lorem Ipsum', $partial->render() );
     115
     116                $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id, array(
     117                        'settings' => 'blogdescription',
     118                ) );
     119                $this->assertEquals( array( 'blogdescription' ), $partial->settings );
     120                $this->assertEquals( 'blogdescription', $partial->primary_setting );
     121        }
     122
     123        /**
     124         * Test WP_Customize_Partial::id_data().
     125         *
     126         * @see WP_Customize_Partial::id_data()
     127         */
     128        function test_id_data() {
     129                $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo' );
     130                $id_data = $partial->id_data();
     131                $this->assertEquals( 'foo', $id_data['base'] );
     132                $this->assertEquals( array(), $id_data['keys'] );
     133
     134                $partial = new WP_Customize_Partial( $this->selective_refresh, 'bar[baz][quux]' );
     135                $id_data = $partial->id_data();
     136                $this->assertEquals( 'bar', $id_data['base'] );
     137                $this->assertEquals( array( 'baz', 'quux' ), $id_data['keys'] );
     138        }
     139
     140        /**
     141         * Keep track of filter calls to customize_partial_render.
     142         *
     143         * @var int
     144         */
     145        protected $count_filter_customize_partial_render = 0;
     146
     147        /**
     148         * Keep track of filter calls to customize_partial_render_{$partial->id}.
     149         *
     150         * @var int
     151         */
     152        protected $count_filter_customize_partial_render_with_id = 0;
     153
     154        /**
     155         * Filter customize_partial_render.
     156         *
     157         * @param string|false         $rendered          Content.
     158         * @param WP_Customize_Partial $partial           Partial.
     159         * @param array                $container_context Data.
     160         * @return string|false Content.
     161         */
     162        function filter_customize_partial_render( $rendered, $partial, $container_context ) {
     163                $this->assertTrue( false === $rendered || is_string( $rendered ) );
     164                $this->assertInstanceOf( 'WP_Customize_Partial', $partial );
     165                $this->assertInternalType( 'array', $container_context );
     166                $this->count_filter_customize_partial_render += 1;
     167                return $rendered;
     168        }
     169
     170        /**
     171         * Filter customize_partial_render_{$partial->id}.
     172         *
     173         * @param string|false         $rendered          Content.
     174         * @param WP_Customize_Partial $partial           Partial.
     175         * @param array                $container_context Data.
     176         * @return string|false Content.
     177         */
     178        function filter_customize_partial_render_with_id( $rendered, $partial, $container_context ) {
     179                $this->assertEquals( sprintf( 'customize_partial_render_%s', $partial->id ), current_filter() );
     180                $this->assertTrue( false === $rendered || is_string( $rendered ) );
     181                $this->assertInstanceOf( 'WP_Customize_Partial', $partial );
     182                $this->assertInternalType( 'array', $container_context );
     183                $this->count_filter_customize_partial_render_with_id += 1;
     184                return $rendered;
     185        }
     186
     187        /**
     188         * Bad render_callback().
     189         *
     190         * @return string Content.
     191         */
     192        function render_echo_and_return() {
     193                echo 'foo';
     194                return 'bar';
     195        }
     196
     197        /**
     198         * Echo render_callback().
     199         */
     200        function render_echo() {
     201                echo 'foo';
     202        }
     203
     204        /**
     205         * Return render_callback().
     206         *
     207         * @return string Content.
     208         */
     209        function render_return() {
     210                return 'bar';
     211        }
     212
     213        /**
     214         * Test WP_Customize_Partial::render() with a bad return_callback.
     215         *
     216         * @see WP_Customize_Partial::render()
     217         */
     218        function test_render_bad_callback() {
     219                $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo', array(
     220                        'render_callback' => array( $this, 'render_echo_and_return' ),
     221                ) );
     222                $this->setExpectedIncorrectUsage( 'render' );
     223                $partial->render();
     224        }
     225
     226        /**
     227         * Test WP_Customize_Partial::render() with a return_callback that echos.
     228         *
     229         * @see WP_Customize_Partial::render()
     230         */
     231        function test_render_echo_callback() {
     232                $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo', array(
     233                        'render_callback' => array( $this, 'render_echo' ),
     234                ) );
     235                $count_filter_customize_partial_render = $this->count_filter_customize_partial_render;
     236                $count_filter_customize_partial_render_with_id = $this->count_filter_customize_partial_render_with_id;
     237                add_filter( 'customize_partial_render', array( $this, 'filter_customize_partial_render' ), 10, 3 );
     238                add_filter( "customize_partial_render_{$partial->id}", array( $this, 'filter_customize_partial_render_with_id' ), 10, 3 );
     239                $rendered = $partial->render();
     240                $this->assertEquals( 'foo', $rendered );
     241                $this->assertEquals( $count_filter_customize_partial_render + 1, $this->count_filter_customize_partial_render );
     242                $this->assertEquals( $count_filter_customize_partial_render_with_id + 1, $this->count_filter_customize_partial_render_with_id );
     243        }
     244
     245        /**
     246         * Test WP_Customize_Partial::render() with a return_callback that echos.
     247         *
     248         * @see WP_Customize_Partial::render()
     249         */
     250        function test_render_return_callback() {
     251                $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo', array(
     252                        'render_callback' => array( $this, 'render_return' ),
     253                ) );
     254                $count_filter_customize_partial_render = $this->count_filter_customize_partial_render;
     255                $count_filter_customize_partial_render_with_id = $this->count_filter_customize_partial_render_with_id;
     256                add_filter( 'customize_partial_render', array( $this, 'filter_customize_partial_render' ), 10, 3 );
     257                add_filter( "customize_partial_render_{$partial->id}", array( $this, 'filter_customize_partial_render_with_id' ), 10, 3 );
     258                $rendered = $partial->render();
     259                $this->assertEquals( 'bar', $rendered );
     260                $this->assertEquals( $count_filter_customize_partial_render + 1, $this->count_filter_customize_partial_render );
     261                $this->assertEquals( $count_filter_customize_partial_render_with_id + 1, $this->count_filter_customize_partial_render_with_id );
     262        }
     263
     264        /**
     265         * Test WP_Customize_Partial::render_callback() default.
     266         *
     267         * @see WP_Customize_Partial::render_callback()
     268         */
     269        function test_render_callback_default() {
     270                $partial = new WP_Customize_Partial( $this->selective_refresh, 'foo' );
     271                $this->assertFalse( $partial->render_callback() );
     272                $this->assertFalse( call_user_func( $partial->render_callback ) );
     273        }
     274
     275        /**
     276         * Test WP_Customize_Partial::json() default.
     277         *
     278         * @see WP_Customize_Partial::json()
     279         */
     280        function test_json() {
     281                $post_id = 123;
     282                $partial_id = sprintf( 'post_content[%d]', $post_id );
     283                $args = array(
     284                        'type' => 'post',
     285                        'selector' => "article.post-$post_id .entry-content",
     286                        'settings' => array( 'user[1]', "post[$post_id]" ),
     287                        'primary_setting' => "post[$post_id]",
     288                        'render_callback' => array( $this, 'render_post_content_partial' ),
     289                        'container_inclusive' => false,
     290                        'fallback_refresh' => false,
     291                );
     292                $partial = new WP_Customize_Partial( $this->selective_refresh, $partial_id, $args );
     293
     294                $exported = $partial->json();
     295                $this->assertArrayHasKey( 'settings', $exported );
     296                $this->assertArrayHasKey( 'primarySetting', $exported );
     297                $this->assertArrayHasKey( 'selector', $exported );
     298                $this->assertArrayHasKey( 'type', $exported );
     299                $this->assertArrayHasKey( 'fallbackRefresh', $exported );
     300                $this->assertArrayHasKey( 'containerInclusive', $exported );
     301        }
     302
     303        /**
     304         * Tear down.
     305         */
     306        function tearDown() {
     307                $this->wp_customize = null;
     308                unset( $GLOBALS['wp_customize'] );
     309                parent::tearDown();
     310        }
     311}
  • new file tests/phpunit/tests/customize/selective-refresh-ajax.php

    diff --git tests/phpunit/tests/customize/selective-refresh-ajax.php tests/phpunit/tests/customize/selective-refresh-ajax.php
    new file mode 100644
    index 0000000..09406de
    - +  
     1<?php
     2/**
     3 * WP_Customize_Selective_Refresh Ajax tests.
     4 *
     5 * @package    WordPress
     6 * @subpackage UnitTests
     7 * @since      4.5.0
     8 * @group      ajax
     9 * @group      customize
     10 */
     11
     12/**
     13 * Tests for the WP_Customize_Selective_Refresh class Ajax.
     14 *
     15 * Note that this is intentionally not extending WP_Ajax_UnitTestCase because it
     16 * is not admin ajax.
     17 */
     18class Test_WP_Customize_Selective_Refresh_Ajax extends WP_UnitTestCase {
     19
     20        /**
     21         * Manager.
     22         *
     23         * @var WP_Customize_Manager
     24         */
     25        public $wp_customize;
     26
     27        /**
     28         * Component.
     29         *
     30         * @var WP_Customize_Selective_Refresh
     31         */
     32        public $selective_refresh;
     33
     34        /**
     35         * Set up the test fixture.
     36         */
     37        function setUp() {
     38                parent::setUp();
     39
     40                // Define DOING_AJAX so that wp_die() will be used instead of die().
     41                if ( ! defined( 'DOING_AJAX' ) ) {
     42                        define( 'DOING_AJAX', true );
     43                }
     44                add_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler' ), 1, 1 );
     45
     46                require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' );
     47                // @codingStandardsIgnoreStart
     48                $GLOBALS['wp_customize'] = new WP_Customize_Manager();
     49                // @codingStandardsIgnoreEnd
     50                $this->wp_customize = $GLOBALS['wp_customize'];
     51                if ( isset( $this->wp_customize->selective_refresh ) ) {
     52                        $this->selective_refresh = $this->wp_customize->selective_refresh;
     53                }
     54
     55        }
     56
     57        /**
     58         * Do Customizer boot actions.
     59         */
     60        function do_customize_boot_actions() {
     61                // Remove actions that call add_theme_support( 'title-tag' ).
     62                remove_action( 'after_setup_theme', 'twentyfifteen_setup' );
     63                remove_action( 'after_setup_theme', 'twentysixteen_setup' );
     64
     65                $_SERVER['REQUEST_METHOD'] = 'POST';
     66                do_action( 'setup_theme' );
     67                do_action( 'after_setup_theme' );
     68                do_action( 'init' );
     69                do_action( 'customize_register', $this->wp_customize );
     70                $this->wp_customize->customize_preview_init();
     71                do_action( 'wp', $GLOBALS['wp'] );
     72        }
     73
     74        /**
     75         * Test WP_Customize_Selective_Refresh::handle_render_partials_request().
     76         *
     77         * @see WP_Customize_Selective_Refresh::handle_render_partials_request()
     78         */
     79        function test_handle_render_partials_request_for_unauthenticated_user() {
     80                $_POST[ WP_Customize_Selective_Refresh::RENDER_QUERY_VAR ] = '1';
     81
     82                // Check current_user_cannot_customize.
     83                ob_start();
     84                try {
     85                        $this->selective_refresh->handle_render_partials_request();
     86                } catch ( WPDieException $e ) {
     87                        unset( $e );
     88                }
     89                $output = json_decode( ob_get_clean(), true );
     90                $this->assertFalse( $output['success'] );
     91                $this->assertEquals( 'expected_customize_preview', $output['data'] );
     92
     93                // Check expected_customize_preview.
     94                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
     95                $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->wp_customize->theme()->get_stylesheet() );
     96                ob_start();
     97                try {
     98                        $this->selective_refresh->handle_render_partials_request();
     99                } catch ( WPDieException $e ) {
     100                        unset( $e );
     101                }
     102                $output = json_decode( ob_get_clean(), true );
     103                $this->assertFalse( $output['success'] );
     104                $this->assertEquals( 'expected_customize_preview', $output['data'] );
     105
     106                // Check missing_partials.
     107                $this->do_customize_boot_actions();
     108                ob_start();
     109                try {
     110                        $this->selective_refresh->handle_render_partials_request();
     111                } catch ( WPDieException $e ) {
     112                        unset( $e );
     113                }
     114                $output = json_decode( ob_get_clean(), true );
     115                $this->assertFalse( $output['success'] );
     116                $this->assertEquals( 'missing_partials', $output['data'] );
     117
     118                // Check missing_partials.
     119                $_POST['partials'] = 'bad';
     120                $this->do_customize_boot_actions();
     121                ob_start();
     122                try {
     123                        $this->selective_refresh->handle_render_partials_request();
     124                } catch ( WPDieException $e ) {
     125                        $this->assertEquals( '', $e->getMessage() );
     126                }
     127                $output = json_decode( ob_get_clean(), true );
     128                $this->assertFalse( $output['success'] );
     129                $this->assertEquals( 'malformed_partials', $output['data'] );
     130        }
     131
     132        /**
     133         * Set the current user to be an admin, add the preview nonce, and set the query var.
     134         */
     135        function setup_valid_render_partials_request_environment() {
     136                wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
     137                $_REQUEST['nonce'] = wp_create_nonce( 'preview-customize_' . $this->wp_customize->theme()->get_stylesheet() );
     138                $_POST[ WP_Customize_Selective_Refresh::RENDER_QUERY_VAR ] = '1';
     139                $this->do_customize_boot_actions();
     140        }
     141
     142        /**
     143         * Make sure that the Customizer "signature" is not included in partial render responses.
     144         *
     145         * @see WP_Customize_Selective_Refresh::handle_render_partials_request()
     146         */
     147        function test_handle_render_partials_request_removes_customize_signature() {
     148                $this->setup_valid_render_partials_request_environment();
     149                $this->assertTrue( is_customize_preview() );
     150                $this->assertEquals( 1000, has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );
     151                ob_start();
     152                try {
     153                        $this->selective_refresh->handle_render_partials_request();
     154                } catch ( WPDieException $e ) {
     155                        unset( $e );
     156                }
     157                ob_end_clean();
     158                $this->assertFalse( has_action( 'shutdown', array( $this->wp_customize, 'customize_preview_signature' ) ) );
     159        }
     160
     161        /**
     162         * Test WP_Customize_Selective_Refresh::handle_render_partials_request() for an unrecognized partial.
     163         *
     164         * @see WP_Customize_Selective_Refresh::handle_render_partials_request()
     165&nbs