WordPress.org

Make WordPress Core

Changeset 36889


Ignore:
Timestamp:
03/09/2016 12:08:51 AM (6 years ago)
Author:
westonruter
Message:

Customize: Fix regressions and harden implementation of selective refresh for nav menus.

  • Request full refresh if there are nav menu instances that lack partials for a changed setting.
  • Restore WP_Customize_Nav_Menus::$preview_nav_menu_instance_args and WP_Customize_Nav_Menus::export_preview_data() from 4.3, and keeping a tally of all wp_nav_menu() calls regardless of whether they can use selective refresh.
  • Ensure that all instances of wp_nav_menu() are tallied, regardless of whether they are made during the initial preview call or during subsequent partial renderings. Export nav_menu_instance_args with each partial rendering response just as they are returned when rendering the preview as a whole.
  • Fix issues with Custom Menu widget where nav menu items would fail to render when switching between menus when a menu lacked items to begin with.
  • Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
  • Do fallback behavior to refresh preview when all menu items are removed from a menu.

Follows [36586].
See #27355.
Fixes #35362.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-nav-menus.php

    r36836 r36889  
    823823
    824824    /**
     825     * Nav menu args used for each instance, keyed by the args HMAC.
     826     *
     827     * @since 4.3.0
     828     * @access public
     829     * @var array
     830     */
     831    public $preview_nav_menu_instance_args = array();
     832
     833    /**
    825834     * Filter arguments for dynamic nav_menu selective refresh partials.
    826835     *
     
    863872        add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
    864873        add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
     874        add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
     875        add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
    865876    }
    866877
     
    882893         * selective refreshed if...
    883894         */
    884         $can_selective_refresh = (
     895        $can_partial_refresh = (
    885896            // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
    886897            ! empty( $args['echo'] )
     
    905916            )
    906917        );
    907 
    908         if ( ! $can_selective_refresh ) {
    909             return $args;
    910         }
     918        $args['can_partial_refresh'] = $can_partial_refresh;
    911919
    912920        $exported_args = $args;
     921
     922        // Empty out args which may not be JSON-serializable.
     923        if ( ! $can_partial_refresh ) {
     924            $exported_args['fallback_cb'] = '';
     925            $exported_args['walker'] = '';
     926        }
    913927
    914928        /*
     
    924938
    925939        $args['customize_preview_nav_menus_args'] = $exported_args;
    926 
     940        $this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
    927941        return $args;
    928942    }
     
    943957     */
    944958    public function filter_wp_nav_menu( $nav_menu_content, $args ) {
    945         if ( ! empty( $args->customize_preview_nav_menus_args ) ) {
     959        if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
    946960            $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
    947961            $attributes .= ' data-customize-partial-type="nav_menu_instance"';
     
    9881002     *
    9891003     * @since 4.3.0
    990      * @deprecated 4.5.0 Obsolete
    9911004     * @access public
    9921005     */
    9931006    public function export_preview_data() {
    994         _deprecated_function( __METHOD__, '4.5.0' );
     1007
     1008        // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
     1009        $exports = array(
     1010            'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
     1011        );
     1012        printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
     1013    }
     1014
     1015    /**
     1016     * Export any wp_nav_menu() calls during the rendering of any partials.
     1017     *
     1018     * @since 4.5.0
     1019     * @access public
     1020     *
     1021     * @param array $response Response.
     1022     * @return array Response.
     1023     */
     1024    public function export_partial_rendered_nav_menu_instances( $response ) {
     1025        $response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
     1026        return $response;
    9951027    }
    9961028
  • trunk/src/wp-includes/js/customize-preview-nav-menus.js

    r36586 r36889  
     1/* global _wpCustomizePreviewNavMenusExports */
    12wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
    23    'use strict';
    34
    4     var self = {};
     5    var self = {
     6        data: {
     7            navMenuInstanceArgs: {}
     8        }
     9    };
     10    if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
     11        _.extend( self.data, _wpCustomizePreviewNavMenusExports );
     12    }
    513
    614    /**
     
    1119
    1220        if ( api.selectiveRefresh ) {
    13             self.watchNavMenuLocationChanges();
     21            // Listen for changes to settings related to nav menus.
     22            api.each( function( setting ) {
     23                self.bindSettingListener( setting );
     24            } );
     25            api.bind( 'add', function( setting ) {
     26                self.bindSettingListener( setting, { fire: true } );
     27            } );
     28            api.bind( 'remove', function( setting ) {
     29                self.unbindSettingListener( setting );
     30            } );
     31
     32            /*
     33             * Ensure that wp_nav_menu() instances nested inside of other partials
     34             * will be recognized as being present on the page.
     35             */
     36            api.selectiveRefresh.bind( 'render-partials-response', function( response ) {
     37                if ( response.nav_menu_instance_args ) {
     38                    _.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args );
     39                }
     40            } );
    1441        }
    1542
     
    129156
    130157            /**
     158             * Make sure that partial fallback behavior is invoked if there is no associated menu.
     159             *
     160             * @since 4.5.0
     161             *
     162             * @returns {Promise}
     163             */
     164            refresh: function() {
     165                var partial = this, menuId, deferred = $.Deferred();
     166
     167                // Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
     168                if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
     169                    menuId = partial.params.navMenuArgs.menu;
     170                } else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
     171                    menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
     172                }
     173                if ( ! menuId ) {
     174                    partial.fallback();
     175                    deferred.reject();
     176                    return deferred.promise();
     177                }
     178
     179                return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
     180            },
     181
     182            /**
    131183             * Render content.
    132184             *
     
    136188            renderContent: function( placement ) {
    137189                var partial = this, previousContainer = placement.container;
     190
     191                // Do fallback behavior to refresh preview if menu is now empty.
     192                if ( '' === placement.addedContent ) {
     193                    placement.partial.fallback();
     194                }
     195
    138196                if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
    139197
     
    153211
    154212        /**
    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 ) {
     213         * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
     214         *
     215         * @param {object} navMenuInstanceArgs
     216         */
     217        self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
     218            var unplacedNavMenuInstances;
     219            unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
     220                return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
     221            } );
     222            if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
     223                api.selectiveRefresh.requestFullRefresh();
     224                return true;
     225            }
     226            return false;
     227        };
     228
     229        /**
     230         * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
     231         *
     232         * @since 4.5.0
     233         *
     234         * @param {wp.customize.Value} setting
     235         * @param {object}             [options]
     236         * @param {boolean}            options.fire Whether to invoke the callback after binding.
     237         *                                          This is used when a dynamic setting is added.
     238         * @return {boolean} Whether the setting was bound.
     239         */
     240        self.bindSettingListener = function( setting, options ) {
     241            var matches;
     242            options = options || {};
     243
     244            matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
     245            if ( matches ) {
     246                setting._navMenuId = parseInt( matches[1], 10 );
     247                setting.bind( this.onChangeNavMenuSetting );
     248                if ( options.fire ) {
     249                    this.onChangeNavMenuSetting.call( setting, setting(), false );
     250                }
     251                return true;
     252            }
     253
     254            matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
     255            if ( matches ) {
     256                setting._navMenuItemId = parseInt( matches[1], 10 );
     257                setting.bind( this.onChangeNavMenuItemSetting );
     258                if ( options.fire ) {
     259                    this.onChangeNavMenuItemSetting.call( setting, setting(), false );
     260                }
     261                return true;
     262            }
     263
     264            matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
     265            if ( matches ) {
     266                setting._navMenuThemeLocation = matches[1];
     267                setting.bind( this.onChangeNavMenuLocationsSetting );
     268                if ( options.fire ) {
     269                    this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
     270                }
     271                return true;
     272            }
     273
     274            return false;
     275        };
     276
     277        /**
     278         * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
     279         *
     280         * @since 4.5.0
     281         *
     282         * @param {wp.customize.Value} setting
     283         */
     284        self.unbindSettingListener = function( setting ) {
     285            setting.unbind( this.onChangeNavMenuSetting );
     286            setting.unbind( this.onChangeNavMenuItemSetting );
     287            setting.unbind( this.onChangeNavMenuLocationsSetting );
     288        };
     289
     290        /**
     291         * Handle change for nav_menu[] setting for nav menu instances lacking partials.
     292         *
     293         * @since 4.5.0
     294         *
     295         * @this {wp.customize.Value}
     296         */
     297        self.onChangeNavMenuSetting = function() {
     298            var setting = this;
     299
     300            self.handleUnplacedNavMenuInstances( {
     301                menu: setting._navMenuId
     302            } );
     303
     304            // Ensure all nav menu instances with a theme_location assigned to this menu are handled.
     305            api.each( function( otherSetting ) {
     306                if ( ! otherSetting._navMenuThemeLocation ) {
    167307                    return;
    168308                }
    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             } );
     309                if ( setting._navMenuId === otherSetting() ) {
     310                    self.handleUnplacedNavMenuInstances( {
     311                        theme_location: otherSetting._navMenuThemeLocation
     312                    } );
     313                }
     314            } );
     315        };
     316
     317        /**
     318         * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
     319         *
     320         * @since 4.5.0
     321         *
     322         * @param {object} newItem New value for nav_menu_item[] setting.
     323         * @param {object} oldItem Old value for nav_menu_item[] setting.
     324         * @this {wp.customize.Value}
     325         */
     326        self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
     327            var item = newItem || oldItem, navMenuSetting;
     328            navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
     329            if ( navMenuSetting ) {
     330                self.onChangeNavMenuSetting.call( navMenuSetting );
     331            }
     332        };
     333
     334        /**
     335         * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
     336         *
     337         * @since 4.5.0
     338         *
     339         * @this {wp.customize.Value}
     340         */
     341        self.onChangeNavMenuLocationsSetting = function() {
     342            var setting = this, hasNavMenuInstance;
     343            self.handleUnplacedNavMenuInstances( {
     344                theme_location: setting._navMenuThemeLocation
     345            } );
     346
     347            // If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
     348            hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
     349                theme_location: setting._navMenuThemeLocation
     350            } );
     351            if ( ! hasNavMenuInstance ) {
     352                api.selectiveRefresh.requestFullRefresh();
     353            }
    181354        };
    182355    }
  • trunk/tests/phpunit/tests/customize/nav-menus.php

    r36782 r36889  
    618618        do_action( 'customize_register', $this->wp_customize );
    619619        $menus = $this->wp_customize->nav_menus;
     620        $menu_id = wp_create_nav_menu( 'Foo' );
    620621
    621622        $results = $menus->filter_wp_nav_menu_args( array(
     
    623624            'fallback_cb'     => 'wp_page_menu',
    624625            'walker'          => '',
    625             'menu'            => wp_create_nav_menu( 'Foo' ),
     626            'menu'            => $menu_id,
    626627            'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    627628        ) );
    628629        $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
     630        $this->assertTrue( $results['can_partial_refresh'] );
    629631
    630632        $results = $menus->filter_wp_nav_menu_args( array(
     
    634636            'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    635637        ) );
    636         $this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results );
     638        $this->assertFalse( $results['can_partial_refresh'] );
     639        $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
    637640        $this->assertEquals( 'wp_page_menu', $results['fallback_cb'] );
    638641
     
    645648            'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    646649        ) );
     650        $this->assertTrue( $results['can_partial_refresh'] );
    647651        $this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
    648652        $this->assertEquals( $nav_menu_term->term_id, $results['customize_preview_nav_menus_args']['menu'] );
     653
     654        $results = $menus->filter_wp_nav_menu_args( array(
     655            'echo'            => true,
     656            'fallback_cb'     => 'wp_page_menu',
     657            'walker'          => '',
     658            'menu'            => $menu_id,
     659            'container'       => 'div',
     660            'items_wrap'      => '%3$s',
     661        ) );
     662        $this->assertTrue( $results['can_partial_refresh'] );
     663
     664        $results = $menus->filter_wp_nav_menu_args( array(
     665            'echo'            => true,
     666            'fallback_cb'     => 'wp_page_menu',
     667            'walker'          => '',
     668            'menu'            => $menu_id,
     669            'container'       => false,
     670            'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
     671        ) );
     672        $this->assertTrue( $results['can_partial_refresh'] );
     673
     674        $results = $menus->filter_wp_nav_menu_args( array(
     675            'echo'            => true,
     676            'fallback_cb'     => 'wp_page_menu',
     677            'walker'          => '',
     678            'menu'            => $menu_id,
     679            'container'       => false,
     680            'items_wrap'      => '%3$s',
     681        ) );
     682        $this->assertFalse( $results['can_partial_refresh'] );
    649683    }
    650684
     
    692726
    693727    /**
     728     * Test WP_Customize_Nav_Menus::export_preview_data() method.
     729     *
     730     * @see WP_Customize_Nav_Menus::export_preview_data()
     731     */
     732    function test_export_preview_data() {
     733        ob_start();
     734        $this->wp_customize->nav_menus->export_preview_data();
     735        $html = ob_get_clean();
     736        $this->assertTrue( (bool) preg_match( '/_wpCustomizePreviewNavMenusExports = ({.+})/s', $html, $matches ) );
     737        $exported_data = json_decode( $matches[1], true );
     738        $this->assertArrayHasKey( 'navMenuInstanceArgs', $exported_data );
     739    }
     740
     741    /**
    694742     * Test WP_Customize_Nav_Menus::render_nav_menu_partial() method.
    695743     *
Note: See TracChangeset for help on using the changeset viewer.