Make WordPress Core

Changeset 36586


Ignore:
Timestamp:
02/19/2016 06:40:06 PM (9 years ago)
Author:
westonruter
Message:

Customize: Add selective refresh framework with implementation for widgets and re-implementation for nav menus.

See https://make.wordpress.org/core/2016/02/16/selective-refresh-in-the-customizer/.

Props westonruter, valendesigns, DrewAPicture, ocean90.
Fixes #27355.

Location:
trunk
Files:
6 added
17 edited

Legend:

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

    r36532 r36586  
    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
  • trunk/src/wp-admin/js/customize-nav-menus.js

    r36573 r36586  
    2020        itemTypes: [],
    2121        l10n: {},
    22         menuItemTransport: 'postMessage',
     22        settingTransport: 'refresh',
    2323        phpIntMax: 0,
    2424        defaultSettingValues: {
     
    23112311            settingArgs = {
    23122312                type: 'nav_menu_item',
    2313                 transport: 'postMessage',
     2313                transport: api.Menus.data.settingTransport,
    23142314                previewer: api.previewer
    23152315            };
     
    24002400            api.create( customizeId, customizeId, {}, {
    24012401                type: 'nav_menu',
    2402                 transport: 'postMessage',
     2402                transport: api.Menus.data.settingTransport,
    24032403                previewer: api.previewer
    24042404            } );
     
    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 );
     
    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                } );
     
    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                } );
  • trunk/src/wp-admin/js/customize-widgets.js

    r36546 r36586  
    3535        name: null,
    3636        id_base: null,
    37         transport: 'refresh',
     37        transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
    3838        params: [],
    3939        width: null,
     
    19831983            if ( ! isExistingWidget ) {
    19841984                settingArgs = {
    1985                     transport: 'refresh',
     1985                    transport: api.Widgets.data.selectiveRefresh ? 'postMessage' : 'refresh',
    19861986                    previewer: this.setting.previewer
    19871987                };
  • trunk/src/wp-content/themes/twentythirteen/js/theme-customizer.js

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

    r36532 r36586  
    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     *
     
    101110     * @var array
    102111     */
    103     protected $components = array( 'widgets', 'nav_menus' );
     112    protected $components = array( 'widgets', 'nav_menus', 'selective_refresh' );
    104113
    105114    /**
     
    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 );
     269        }
     270
     271        if ( in_array( 'selective_refresh', $components, true ) ) {
     272            require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
     273            $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
    259274        }
    260275
     
    17121727            'documentTitleTmpl' => $this->get_document_title_template(),
    17131728            'previewableDevices' => $this->get_previewable_devices(),
     1729            'selectiveRefreshEnabled' => isset( $this->selective_refresh ),
    17141730        );
    17151731
  • trunk/src/wp-includes/class-wp-customize-nav-menus.php

    r36573 r36586  
    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
     
    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(
     
    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        }
     
    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' ) );
     
    524529                    'theme_supports'    => 'menus',
    525530                    'type'              => 'theme_mod',
    526                     'transport'         => 'postMessage',
     531                    'transport'         => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    527532                    'default'           => 0,
    528533                ) );
     
    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.
     
    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
     
    587595            'type'      => 'new_menu',
    588596            'default'   => '',
    589             'transport' => 'postMessage',
     597            'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    590598        ) );
    591599
     
    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';
    809 
    810     /**
    811      * The number of wp_nav_menu() calls which have happened in the preview.
    812      *
    813      * @since 4.3.0
    814      * @access public
    815      * @var int
    816      */
    817     public $preview_nav_menu_instance_number = 0;
    818 
    819     /**
    820      * Nav menu args used for each instance.
    821      *
    822      * @since 4.3.0
    823      * @access public
    824      * @var array
    825      */
    826     public $preview_nav_menu_instance_args = array();
     815    //
     816
     817    /**
     818     * Filters arguments for dynamic nav_menu selective refresh partials.
     819     *
     820     * @since 4.5.0
     821     * @access public
     822     *
     823     * @param array|false $partial_args Partial args.
     824     * @param string      $partial_id   Partial ID.
     825     * @return array Partial args
     826     */
     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    /**
     
    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
     
    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.
     
    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;
    875 
    876         if ( ! $can_partial_refresh ) {
    877             $hashed_args['fallback_cb'] = '';
    878             $hashed_args['walker'] = '';
    879         }
    880 
    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;
    884         }
    885 
    886         ksort( $hashed_args );
    887         $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
    888 
    889         $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
     898
     899        if ( ! $can_selective_refresh ) {
     900            return $args;
     901        }
     902
     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;
     911        }
     912
     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;
     917
    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
     
    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;
     
    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
     
    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
     
    936966     */
    937967    public function customize_preview_enqueue_deps() {
    938         wp_enqueue_script( 'customize-preview-nav-menus' );
     968        if ( isset( $this->manager->selective_refresh ) ) {
     969            $script = wp_scripts()->registered['customize-preview-nav-menus'];
     970            $script->deps[] = 'customize-selective-refresh';
     971        }
     972
     973        wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
    939974        wp_enqueue_style( 'customize-preview' );
    940 
    941         add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
    942     }
    943 
    944     /**
    945      * Export data from PHP to JS.
    946      *
    947      * @since 4.3.0
     975    }
     976
     977    /**
     978     * Exports data from PHP to JS.
     979     *
     980     * @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
     
    971993     *
    972994     * @see wp_nav_menu()
    973      */
    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         }
    988 
    989         if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
    990             wp_send_json_error( 'nonce_check_fail' );
    991         }
    992 
    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         }
    1000 
    1001         if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
    1002             wp_send_json_error( 'missing_param' );
    1003         }
    1004 
    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         }
    1014 
    1015         $wp_nav_menu_args['echo'] = false;
    1016         wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
     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
     999     */
     1000    public function render_nav_menu_partial( $partial, $nav_menu_args ) {
     1001        unset( $partial );
     1002
     1003        if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
     1004            // Error: missing_args_hmac.
     1005            return false;
     1006        }
     1007
     1008        $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
     1009        unset( $nav_menu_args['args_hmac'] );
     1010
     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;
     1015        }
     1016
     1017        ob_start();
     1018        wp_nav_menu( $nav_menu_args );
     1019        $content = ob_get_clean();
     1020
     1021        return $content;
    10171022    }
    10181023}
  • trunk/src/wp-includes/class-wp-customize-widgets.php

    r36414 r36586  
    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
     
    683687                'moveWidgetArea'   => $move_widget_area_tpl,
    684688            ),
     689            'selectiveRefresh'     => isset( $this->manager->selective_refresh ),
    685690        );
    686691
     
    763768            'type'       => 'option',
    764769            'capability' => 'edit_theme_options',
    765             'transport'  => 'refresh',
     770            'transport'  => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh',
    766771            'default'    => array(),
    767772        );
     
    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'],
     
    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 ) {
     
    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    /**
     
    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 ) {
  • trunk/src/wp-includes/css/customize-preview.css

    r33859 r36586  
    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}
     13
     14.customize-render-content-error {
     15    outline: solid 1px red;
     16}
     17.customize-render-content-error-message {
     18    display: block;
     19    padding: 1em;
     20    background-color: #FFCCCC;
     21}
  • trunk/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php

    r35724 r36586  
    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    /**
  • trunk/src/wp-includes/js/customize-preview-nav-menus.js

    r36523 r36586  
    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: {}
     4    var self = {};
     5
     6    /**
     7     * Initialize nav menus preview.
     8     */
     9    self.init = function() {
     10        var self = this;
     11
     12        if ( api.selectiveRefresh ) {
     13            self.watchNavMenuLocationChanges();
     14        }
     15
     16        api.preview.bind( 'active', function() {
     17            self.highlightControls();
     18        } );
     19    };
     20
     21    if ( api.selectiveRefresh ) {
     22
     23        /**
     24         * Partial representing an invocation of wp_nav_menu().
     25         *
     26         * @class
     27         * @augments wp.customize.selectiveRefresh.Partial
     28         * @since 4.5.0
     29         */
     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.' );
     50                }
     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' );
     66                }
     67                if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
     68                    throw new Error( 'args_hmac mismatch with id' );
     69                }
     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, isNavMenuItemSetting;
     83                if ( _.isString( setting ) ) {
     84                    setting = api( setting );
     85                }
     86
     87                /*
     88                 * Prevent nav_menu_item changes only containing type_label differences triggering a refresh.
     89                 * These settings in the preview do not include type_label property, and so if one of these
     90                 * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective
     91                 * refresh immediately because the setting from the pane would have the type_label whereas
     92                 * the setting in the preview would not, thus triggering a change event. The following
     93                 * condition short-circuits this unnecessary selective refresh and also prevents an infinite
     94                 * loop in the case where a nav_menu_instance partial had done a fallback refresh.
     95                 * @todo Nav menu item settings should not include a type_label property to begin with.
     96                 */
     97                isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id );
     98                if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) {
     99                    delete newValue.type_label;
     100                    delete oldValue.type_label;
     101                    if ( _.isEqual( oldValue, newValue ) ) {
     102                        return false;
     103                    }
     104                }
     105
     106                if ( partial.params.navMenuArgs.theme_location ) {
     107                    if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
     108                        return true;
     109                    }
     110                    navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
     111                }
     112
     113                navMenuId = partial.params.navMenuArgs.menu;
     114                if ( ! navMenuId && navMenuLocationSetting ) {
     115                    navMenuId = navMenuLocationSetting();
     116                }
     117
     118                if ( ! navMenuId ) {
     119                    return false;
     120                }
     121                return (
     122                    ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
     123                    ( isNavMenuItemSetting && (
     124                        ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
     125                        ( oldValue && oldValue.nav_menu_term_id === navMenuId )
     126                    ) )
     127                );
     128            },
     129
     130            /**
     131             * Render content.
     132             *
     133             * @inheritdoc
     134             * @param {wp.customize.selectiveRefresh.Placement} placement
     135             */
     136            renderContent: function( placement ) {
     137                var partial = this, previousContainer = placement.container;
     138                if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
     139
     140                    // Trigger deprecated event.
     141                    $( document ).trigger( 'customize-preview-menu-refreshed', [ {
     142                        instanceNumber: null, // @deprecated
     143                        wpNavArgs: placement.context, // @deprecated
     144                        wpNavMenuArgs: placement.context,
     145                        oldContainer: previousContainer,
     146                        newContainer: placement.container
     147                    } ] );
     148                }
     149            }
     150        });
     151
     152        api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
     153
     154        /**
     155         * Watch for changes to nav_menu_locations[] settings.
     156         *
     157         * Refresh partials associated with the given nav_menu_locations[] setting,
     158         * or request an entire preview refresh if there are no containers in the
     159         * document for a partial associated with the theme location.
     160         *
     161         * @since 4.5.0
     162         */
     163        self.watchNavMenuLocationChanges = function() {
     164            api.bind( 'change', function( setting ) {
     165                var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ );
     166                if ( ! matches ) {
     167                    return;
     168                }
     169                themeLocation = matches[1];
     170                api.selectiveRefresh.partial.each( function( partial ) {
     171                    if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) {
     172                        partial.refresh();
     173                        themeLocationPartialFound = true;
     174                    }
     175                } );
     176
     177                if ( ! themeLocationPartialFound ) {
     178                    api.selectiveRefresh.requestFullRefresh();
     179                }
     180            } );
    19181        };
    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();
    44 
    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                 }
    54 
    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             } );
    62 
    63             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         },
    112 
    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         },
    128 
    129         /**
    130          * Update a given menu rendered in the preview.
    131          *
    132          * @param {int} menuId
    133          */
    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] );
    141                 }
    142             });
    143 
    144             _.each( settings.navMenuInstanceArgs, function( navMenuArgs, instanceNumber ) {
    145                 if ( menuId === navMenuArgs.menu || -1 !== _.indexOf( assignedLocations, navMenuArgs.theme_location ) ) {
    146                     this.refreshMenuInstanceDebounced( instanceNumber );
    147                 }
    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;
    162                 }
    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' );
     182    }
     183
     184    /**
     185     * Connect nav menu items with their corresponding controls in the pane.
     186     *
     187     * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
     188     * Also this applies even if a nav menu is not partial-refreshable.
     189     *
     190     * @since 4.5.0
     191     */
     192    self.highlightControls = function() {
     193        var selector = '.menu-item';
     194
     195        // Focus on the menu item control when shift+clicking the menu item.
     196        $( document ).on( 'click', selector, function( e ) {
     197            var navMenuItemParts;
     198            if ( ! e.shiftKey ) {
    193199                return;
    194200            }
    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;
     201
     202            navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
     203            if ( navMenuItemParts ) {
     204                e.preventDefault();
     205                e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
     206                api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
    203207            }
    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;
    223                 }
    224             } );
    225             data.customized = JSON.stringify( customized );
    226             data[ settings.renderNoncePostKey ] = settings.renderNonceValue;
    227 
    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;
    244                 }
    245 
    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         },
    265 
    266         refreshMenuInstanceDebounced : function( instanceNumber ) {
    267             if ( currentRefreshDebounced[ instanceNumber ] ) {
    268                 clearTimeout( currentRefreshDebounced[ instanceNumber ] );
    269             }
    270             currentRefreshDebounced[ instanceNumber ] = setTimeout(
    271                 _.bind( function() {
    272                     this.refreshMenuInstance( instanceNumber );
    273                 }, this ),
    274                 refreshDebounceDelay
    275             );
    276         },
    277 
    278         /**
    279          * Connect nav menu items with their corresponding controls in the pane.
    280          */
    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 ) {
    289                     return;
    290                 }
    291 
    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 ) );
    297                 }
    298             });
    299 
    300             addTooltips = function( e, params ) {
    301                 params.newContainer.find( selector ).attr( 'title', settings.l10n.editNavMenuItemTooltip );
    302             };
    303 
    304             addTooltips( null, { newContainer: $( document.body ) } );
    305             $( document ).on( 'customize-preview-menu-refreshed', addTooltips );
    306         }
     208        });
    307209    };
    308210
    309211    api.bind( 'preview-ready', function() {
    310         api.preview.bind( 'active', function() {
    311             api.MenusCustomizerPreview.init();
    312         } );
     212        self.init();
    313213    } );
    314214
    315 }( jQuery, _, wp ) );
     215    return self;
     216
     217}( jQuery, _, wp, wp.customize ) );
  • trunk/src/wp-includes/js/customize-preview-widgets.js

    r35783 r36586  
    1 (function( wp, $ ){
    2 
    3     if ( ! wp || ! wp.customize ) { return; }
    4 
    5     var api = wp.customize;
    6 
    7     /**
    8      * wp.customize.WidgetCustomizerPreview
    9      *
    10      */
    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
     1/* global _wpWidgetCustomizerPreviewSettings */
     2wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {
     3
     4    var self;
     5
     6    self = {
     7        renderedSidebars: {},
     8        renderedWidgets: {},
     9        registeredSidebars: [],
     10        registeredWidgets: {},
    1611        widgetSelectors: [],
    1712        preview: null,
    18         l10n: {},
    19 
    20         init: function () {
    21             var self = this;
    22 
    23             this.preview = api.preview;
    24             this.buildWidgetSelectors();
    25             this.highlightControls();
    26 
    27             this.preview.bind( 'highlight-widget', self.highlightWidget );
    28         },
     13        l10n: {
     14            widgetTooltip: ''
     15        }
     16    };
     17
     18    /**
     19     * Init widgets preview.
     20     *
     21     * @since 4.5.0
     22     */
     23    self.init = function() {
     24        var self = this;
     25
     26        self.preview = api.preview;
     27        if ( api.selectiveRefresh ) {
     28            self.addPartials();
     29        }
     30
     31        self.buildWidgetSelectors();
     32        self.highlightControls();
     33
     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 ) {
     50        self.WidgetPartial = api.selectiveRefresh.Partial.extend({
     51
     52            /**
     53             * Constructor.
     54             *
     55             * @since 4.5.0
     56             * @param {string} id - Partial ID.
     57             * @param {Object} options
     58             * @param {Object} options.params
     59             */
     60            initialize: function( id, options ) {
     61                var partial = this, matches;
     62                matches = id.match( /^widget\[(.+)]$/ );
     63                if ( ! matches ) {
     64                    throw new Error( 'Illegal id for widget partial.' );
     65                }
     66
     67                partial.widgetId = matches[1];
     68                options = options || {};
     69                options.params = _.extend(
     70                    {
     71                        /* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */
     72                        selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]'
     73                        settings: [ self.getWidgetSettingId( partial.widgetId ) ],
     74                        containerInclusive: true
     75                    },
     76                    options.params || {}
     77                );
     78
     79                api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     80            },
     81
     82            /**
     83             * Send widget-updated message to parent so spinner will get removed from widget control.
     84             *
     85             * @inheritdoc
     86             * @param {wp.customize.selectiveRefresh.Placement} placement
     87             */
     88            renderContent: function( placement ) {
     89                var partial = this;
     90                if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
     91                    api.preview.send( 'widget-updated', partial.widgetId );
     92                    api.selectiveRefresh.trigger( 'widget-updated', partial );
     93                }
     94            }
     95        });
     96
     97        /**
     98         * Partial representing a widget area.
     99         *
     100         * @class
     101         * @augments wp.customize.selectiveRefresh.Partial
     102         * @since 4.5.0
     103         */
     104        self.SidebarPartial = api.selectiveRefresh.Partial.extend({
     105
     106            /**
     107             * Constructor.
     108             *
     109             * @since 4.5.0
     110             * @param {string} id - Partial ID.
     111             * @param {Object} options
     112             * @param {Object} options.params
     113             */
     114            initialize: function( id, options ) {
     115                var partial = this, matches;
     116                matches = id.match( /^sidebar\[(.+)]$/ );
     117                if ( ! matches ) {
     118                    throw new Error( 'Illegal id for sidebar partial.' );
     119                }
     120                partial.sidebarId = matches[1];
     121
     122                options = options || {};
     123                options.params = _.extend(
     124                    {
     125                        settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
     126                    },
     127                    options.params || {}
     128                );
     129
     130                api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
     131
     132                if ( ! partial.params.sidebarArgs ) {
     133                    throw new Error( 'The sidebarArgs param was not provided.' );
     134                }
     135                if ( partial.params.settings.length > 1 ) {
     136                    throw new Error( 'Expected SidebarPartial to only have one associated setting' );
     137                }
     138            },
     139
     140            /**
     141             * Set up the partial.
     142             *
     143             * @since 4.5.0
     144             */
     145            ready: function() {
     146                var sidebarPartial = this;
     147
     148                // Watch for changes to the sidebar_widgets setting.
     149                _.each( sidebarPartial.settings(), function( settingId ) {
     150                    api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
     151                } );
     152
     153                // Trigger an event for this sidebar being updated whenever a widget inside is rendered.
     154                api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     155                    var isAssignedWidgetPartial = (
     156                        placement.partial.extended( self.WidgetPartial ) &&
     157                        ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
     158                    );
     159                    if ( isAssignedWidgetPartial ) {
     160                        api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
     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                }
     347
     348                // Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
     349                _.each( sidebarPartial.placements(), function( sidebarPlacement ) {
     350                    var foundWidgetPlacement, widgetContainerElement;
     351
     352                    foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
     353                        return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
     354                    } );
     355                    if ( foundWidgetPlacement ) {
     356                        return;
     357                    }
     358
     359                    widgetContainerElement = $(
     360                        sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
     361                        sidebarPartial.params.sidebarArgs.after_widget
     362                    );
     363
     364                    widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
     365                    widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
     366                    widgetContainerElement.attr( 'data-customize-widget-id', widgetId );
     367
     368                    /*
     369                     * Make sure the widget container element has the customize-container context data.
     370                     * The sidebar_instance_number is used to disambiguate multiple instances of the
     371                     * same sidebar are rendered onto the template, and so the same widget is embedded
     372                     * multiple times.
     373                     */
     374                    widgetContainerElement.data( 'customize-partial-placement-context', {
     375                        'sidebar_id': sidebarPartial.sidebarId,
     376                        'sidebar_instance_number': sidebarPlacement.context.instanceNumber
     377                    } );
     378
     379                    sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
     380                    wasInserted = true;
     381                } );
     382
     383                if ( wasInserted ) {
     384                    sidebarPartial.reflowWidgets();
     385                }
     386
     387                return widgetPartial;
     388            },
     389
     390            /**
     391             * Handle change to the sidebars_widgets[] setting.
     392             *
     393             * @since 4.5.0
     394             *
     395             * @param {Array} newWidgetIds New widget ids.
     396             * @param {Array} oldWidgetIds Old widget ids.
     397             */
     398            handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
     399                var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];
     400
     401                needsRefresh = (
     402                    ( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
     403                    ( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
     404                );
     405                if ( needsRefresh ) {
     406                    sidebarPartial.fallback();
    53407                    return;
    54408                }
    55409
    56                 widgetClasses = widgetClasses.replace(/^\s+|\s+$/g, '');
    57 
    58                 if ( widgetClasses ) {
    59                     widgetSelector += '.' + widgetClasses.split(/\s+/).join('.');
    60                 }
    61                 self.widgetSelectors.push(widgetSelector);
    62             });
    63         },
     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;
    64468
    65469        /**
    66          * Highlight the widget on widget updates or widget control mouse overs.
     470         * Add partials for the registered widget areas (sidebars).
    67471         *
    68          * @param  {string} widgetId ID of the widget.
     472         * @since 4.5.0
    69473         */
    70         highlightWidget: function( widgetId ) {
    71             var $body = $( document.body ),
    72                 $widget = $( '#' + widgetId );
    73 
    74             $body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
    75 
    76             $widget.addClass( 'widget-customizer-highlighted-widget' );
    77             setTimeout( function () {
    78                 $widget.removeClass( 'widget-customizer-highlighted-widget' );
    79             }, 500 );
    80         },
    81 
    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(',');
    89 
    90             $(selector).attr( 'title', this.l10n.widgetTooltip );
    91 
    92             $(document).on( 'mouseenter', selector, function () {
    93                 self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
    94             });
    95 
    96             // Open expand the widget control when shift+clicking the widget element
    97             $(document).on( 'click', selector, function ( e ) {
    98                 if ( ! e.shiftKey ) {
    99                     return;
    100                 }
    101                 e.preventDefault();
    102 
    103                 self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
    104             });
     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.
    105593        }
    106     };
    107 
    108     $(function () {
    109         var settings = window._wpWidgetCustomizerPreviewSettings;
    110         if ( ! settings ) {
    111             return;
     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;
    112615        }
    113 
    114         $.extend( api.WidgetCustomizerPreview, settings );
    115 
    116         api.WidgetCustomizerPreview.init();
     616        parsed.idBase = matches[1];
     617        if ( matches[2] ) {
     618            parsed.number = parseInt( matches[2], 10 );
     619        }
     620        return parsed;
     621    };
     622
     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 ) + ']';
     637        }
     638
     639        return settingId;
     640    };
     641
     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 );
  • trunk/src/wp-includes/script-loader.php

    r36551 r36586  
    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 );
  • trunk/tests/phpunit/tests/customize/manager.php

    r36532 r36586  
    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'] );
  • trunk/tests/phpunit/tests/customize/nav-menu-item-setting.php

    r35583 r36586  
    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 );
  • trunk/tests/phpunit/tests/customize/nav-menus.php

    r36414 r36586  
    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
     
    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     *
     
    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
     
    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(
     
    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'] );
    560 
    561         $expected = array(
    562             'echo',
    563             'can_partial_refresh',
    564             'fallback_cb',
    565             'instance_number',
    566             'walker',
    567         );
     578        $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
     579
    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'] );
    576 
    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'] );
     588
     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
     
    596613            'fallback_cb' => 'wp_page_menu',
    597614            'walker'      => '',
     615            'items_wrap'  => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    598616        ) );
    599617
     
    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
     
    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' ) ) );
    626     }
    627 
    628     /**
    629      * Test the export_preview_data method.
     641    }
     642
     643    /**
     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 );
    636 
    637         $request_uri = $_SERVER['REQUEST_URI'];
    638 
    639         ob_start();
    640         $_SERVER['REQUEST_URI'] = '/wp-admin';
    641         $menus->export_preview_data();
    642         $data = ob_get_clean();
    643 
    644         $_SERVER['REQUEST_URI'] = $request_uri;
    645 
    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 );
     649        $this->setExpectedDeprecated( 'WP_Customize_Nav_Menus::export_preview_data' );
     650        $this->wp_customize->nav_menus->export_preview_data();
     651    }
     652
     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        ) );
     668
     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        ) );
     676
     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 ) );
     694
     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}
  • trunk/tests/phpunit/tests/customize/widgets.php

    r35754 r36586  
    4040        remove_action( 'after_setup_theme', 'twentyfifteen_setup' );
    4141        remove_action( 'after_setup_theme', 'twentysixteen_setup' );
     42        remove_action( 'customize_register', 'twentysixteen_customize_register', 11 );
    4243
    4344        $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
     
    122123            'type' => 'option',
    123124            'capability' => 'edit_theme_options',
    124             'transport' => 'refresh',
     125            'transport' => 'postMessage',
    125126            'default' => array(),
    126127            'sanitize_callback' => array( $this->manager->widgets, 'sanitize_widget_instance' ),
     
    151152            'type' => 'option',
    152153            'capability' => 'edit_theme_options',
    153             'transport' => 'refresh',
     154            'transport' => 'postMessage',
    154155            'default' => array(),
    155156            'sanitize_callback' => array( $this->manager->widgets, 'sanitize_sidebar_widgets' ),
     
    347348        $this->assertEquals( $post_value, $this->manager->widgets->sanitize_widget_js_instance( $instance ) );
    348349    }
     350
     351    /**
     352     * Test WP_Customize_Widgets::customize_dynamic_partial_args().
     353     *
     354     * @see WP_Customize_Widgets::customize_dynamic_partial_args()
     355     */
     356    function test_customize_dynamic_partial_args() {
     357        do_action( 'customize_register', $this->manager );
     358
     359        $args = apply_filters( 'customize_dynamic_partial_args', false, 'widget[search-2]' );
     360        $this->assertInternalType( 'array', $args );
     361        $this->assertEquals( 'widget', $args['type'] );
     362        $this->assertEquals( array( $this->manager->widgets, 'render_widget_partial' ), $args['render_callback'] );
     363        $this->assertTrue( $args['container_inclusive'] );
     364
     365        $args = apply_filters( 'customize_dynamic_partial_args', array( 'fallback_refresh' => false ), 'widget[search-2]' );
     366        $this->assertInternalType( 'array', $args );
     367        $this->assertEquals( 'widget', $args['type'] );
     368        $this->assertEquals( array( $this->manager->widgets, 'render_widget_partial' ), $args['render_callback'] );
     369        $this->assertTrue( $args['container_inclusive'] );
     370        $this->assertFalse( $args['fallback_refresh'] );
     371    }
     372
     373    /**
     374     * Test WP_Customize_Widgets::selective_refresh_init().
     375     *
     376     * @see WP_Customize_Widgets::selective_refresh_init()
     377     */
     378    function test_selective_refresh_init() {
     379        $this->manager->widgets->selective_refresh_init();
     380        $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $this->manager->widgets, 'customize_preview_enqueue_deps' ) ) );
     381        $this->assertEquals( 10, has_action( 'dynamic_sidebar_before', array( $this->manager->widgets, 'start_dynamic_sidebar' ) ) );
     382        $this->assertEquals( 10, has_action( 'dynamic_sidebar_after', array( $this->manager->widgets, 'end_dynamic_sidebar' ) ) );
     383        $this->assertEquals( 10, has_filter( 'dynamic_sidebar_params', array( $this->manager->widgets, 'filter_dynamic_sidebar_params' ) ) );
     384        $this->assertEquals( 10, has_filter( 'wp_kses_allowed_html', array( $this->manager->widgets, 'filter_wp_kses_allowed_data_attributes' ) ) );
     385    }
     386
     387    /**
     388     * Test WP_Customize_Widgets::customize_preview_enqueue_deps().
     389     *
     390     * @see WP_Customize_Widgets::customize_preview_enqueue_deps()
     391     */
     392    function test_customize_preview_enqueue_deps() {
     393        $this->manager->widgets->customize_preview_enqueue_deps();
     394        $this->assertTrue( wp_script_is( 'customize-preview-widgets', 'enqueued' ) );
     395        $this->assertTrue( wp_style_is( 'customize-preview', 'enqueued' ) );
     396        $script = wp_scripts()->registered['customize-preview-widgets'];
     397        $this->assertContains( 'customize-selective-refresh', $script->deps );
     398    }
     399
     400    /**
     401     * Test extensions to dynamic_sidebar().
     402     *
     403     * @see WP_Customize_Widgets::filter_dynamic_sidebar_params()
     404     * @see WP_Customize_Widgets::start_dynamic_sidebar()
     405     * @see WP_Customize_Widgets::end_dynamic_sidebar()
     406     */
     407    function test_filter_dynamic_sidebar_params() {
     408        global $wp_registered_sidebars;
     409        register_sidebar( array(
     410            'id' => 'foo',
     411        ) );
     412
     413        $this->manager->widgets->selective_refresh_init();
     414
     415        $params = array(
     416            array_merge(
     417                $wp_registered_sidebars['foo'],
     418                array(
     419                    'widget_id' => 'search-2',
     420                )
     421            ),
     422            array(),
     423        );
     424        $this->assertEquals( $params, $this->manager->widgets->filter_dynamic_sidebar_params( $params ), 'Expected short-circuit if not called after dynamic_sidebar_before.' );
     425
     426        ob_start();
     427        do_action( 'dynamic_sidebar_before', 'foo' );
     428        $output = ob_get_clean();
     429        $this->assertEquals( '<!--dynamic_sidebar_before:foo:1-->', trim( $output ) );
     430
     431        $bad_params = $params;
     432        unset( $bad_params[0]['id'] );
     433        $this->assertEquals( $bad_params, $this->manager->widgets->filter_dynamic_sidebar_params( $bad_params ) );
     434
     435        $bad_params = $params;
     436        $bad_params[0]['id'] = 'non-existing';
     437        $this->assertEquals( $bad_params, $this->manager->widgets->filter_dynamic_sidebar_params( $bad_params ) );
     438
     439        $bad_params = $params;
     440        $bad_params[0]['before_widget'] = '   <oops>';
     441        $this->assertEquals( $bad_params, $this->manager->widgets->filter_dynamic_sidebar_params( $bad_params ) );
     442
     443        $filtered_params = $this->manager->widgets->filter_dynamic_sidebar_params( $params );
     444        $this->assertNotEquals( $params, $filtered_params );
     445        ob_start();
     446        do_action( 'dynamic_sidebar_after', 'foo' );
     447        $output = ob_get_clean();
     448        $this->assertEquals( '<!--dynamic_sidebar_after:foo:1-->', trim( $output ) );
     449
     450        $output = wp_kses_post( $filtered_params[0]['before_widget'] );
     451        $this->assertContains( 'data-customize-partial-id="widget[search-2]"', $output );
     452        $this->assertContains( 'data-customize-partial-type="widget"', $output );
     453    }
     454
     455    /**
     456     * Test WP_Customize_Widgets::render_widget_partial() method.
     457     *
     458     * @see WP_Customize_Widgets::render_widget_partial()
     459     */
     460    function test_render_widget_partial() {
     461        $this->manager->widgets->selective_refresh_init();
     462
     463        $partial_id = 'widget[search-2]';
     464        $partials = $this->manager->selective_refresh->add_dynamic_partials( array( $partial_id ) );
     465        $this->assertNotEmpty( $partials );
     466        $partial = array_shift( $partials );
     467        $this->assertEquals( $partial_id, $partial->id );
     468
     469        $this->assertFalse( $this->manager->widgets->render_widget_partial( $partial, array() ) );
     470        $this->assertFalse( $this->manager->widgets->render_widget_partial( $partial, array( 'sidebar_id' => 'non-existing' ) ) );
     471
     472        $output = $this->manager->widgets->render_widget_partial( $partial, array( 'sidebar_id' => 'sidebar-1' ) );
     473
     474        $this->assertEquals( 1, substr_count( $output, 'data-customize-partial-id' ) );
     475        $this->assertEquals( 1, substr_count( $output, 'data-customize-partial-type="widget"' ) );
     476        $this->assertContains( ' id="search-2"', $output );
     477    }
     478
     479    /**
     480     * Test deprecated methods.
     481     */
     482    public function test_deprecated_methods() {
     483        $this->setExpectedDeprecated( 'WP_Customize_Widgets::setup_widget_addition_previews' );
     484        $this->setExpectedDeprecated( 'WP_Customize_Widgets::prepreview_added_sidebars_widgets' );
     485        $this->setExpectedDeprecated( 'WP_Customize_Widgets::prepreview_added_widget_instance' );
     486        $this->setExpectedDeprecated( 'WP_Customize_Widgets::remove_prepreview_filters' );
     487        $this->manager->widgets->setup_widget_addition_previews();
     488        $this->manager->widgets->prepreview_added_sidebars_widgets();
     489        $this->manager->widgets->prepreview_added_widget_instance();
     490        $this->manager->widgets->remove_prepreview_filters();
     491    }
    349492}
  • trunk/tests/qunit/fixtures/customize-menus.js

    r36574 r36586  
    33    'nonce': 'yo',
    44    'phpIntMax': '2147483647',
    5     'menuItemTransport': 'postMessage',
     5    'settingTransport': 'postMessage',
    66    'allMenus': [{
    77        'term_id': '2',
Note: See TracChangeset for help on using the changeset viewer.