WordPress.org

Make WordPress Core

Changeset 36586


Ignore:
Timestamp:
02/19/16 18:40:06 (18 months 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.