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.

Built from https://develop.svn.wordpress.org/trunk@36889


git-svn-id: http://core.svn.wordpress.org/trunk@36856 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter 2016-03-09 00:09:26 +00:00
parent f64b97c597
commit e11fd98ec6
4 changed files with 236 additions and 31 deletions

View File

@ -821,6 +821,15 @@ final class WP_Customize_Nav_Menus {
// Start functionality specific to partial-refresh of menu changes in Customizer preview.
//
/**
* Nav menu args used for each instance, keyed by the args HMAC.
*
* @since 4.3.0
* @access public
* @var array
*/
public $preview_nav_menu_instance_args = array();
/**
* Filter arguments for dynamic nav_menu selective refresh partials.
*
@ -862,6 +871,8 @@ final class WP_Customize_Nav_Menus {
add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
}
/**
@ -881,7 +892,7 @@ final class WP_Customize_Nav_Menus {
* wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
* selective refreshed if...
*/
$can_selective_refresh = (
$can_partial_refresh = (
// ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
! empty( $args['echo'] )
&&
@ -904,13 +915,16 @@ final class WP_Customize_Nav_Menus {
( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
)
);
if ( ! $can_selective_refresh ) {
return $args;
}
$args['can_partial_refresh'] = $can_partial_refresh;
$exported_args = $args;
// Empty out args which may not be JSON-serializable.
if ( ! $can_partial_refresh ) {
$exported_args['fallback_cb'] = '';
$exported_args['walker'] = '';
}
/*
* Replace object menu arg with a term_id menu arg, as this exports better
* to JS and is easier to compare hashes.
@ -923,7 +937,7 @@ final class WP_Customize_Nav_Menus {
$exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
$args['customize_preview_nav_menus_args'] = $exported_args;
$this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
return $args;
}
@ -942,7 +956,7 @@ final class WP_Customize_Nav_Menus {
* @return null
*/
public function filter_wp_nav_menu( $nav_menu_content, $args ) {
if ( ! empty( $args->customize_preview_nav_menus_args ) ) {
if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
$attributes .= ' data-customize-partial-type="nav_menu_instance"';
$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
@ -987,11 +1001,29 @@ final class WP_Customize_Nav_Menus {
* Exports data from PHP to JS.
*
* @since 4.3.0
* @deprecated 4.5.0 Obsolete
* @access public
*/
public function export_preview_data() {
_deprecated_function( __METHOD__, '4.5.0' );
// Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
$exports = array(
'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
);
printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
}
/**
* Export any wp_nav_menu() calls during the rendering of any partials.
*
* @since 4.5.0
* @access public
*
* @param array $response Response.
* @return array Response.
*/
public function export_partial_rendered_nav_menu_instances( $response ) {
$response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
return $response;
}
/**

View File

@ -1,7 +1,15 @@
/* global _wpCustomizePreviewNavMenusExports */
wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
'use strict';
var self = {};
var self = {
data: {
navMenuInstanceArgs: {}
}
};
if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
_.extend( self.data, _wpCustomizePreviewNavMenusExports );
}
/**
* Initialize nav menus preview.
@ -10,7 +18,26 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
var self = this;
if ( api.selectiveRefresh ) {
self.watchNavMenuLocationChanges();
// Listen for changes to settings related to nav menus.
api.each( function( setting ) {
self.bindSettingListener( setting );
} );
api.bind( 'add', function( setting ) {
self.bindSettingListener( setting, { fire: true } );
} );
api.bind( 'remove', function( setting ) {
self.unbindSettingListener( setting );
} );
/*
* Ensure that wp_nav_menu() instances nested inside of other partials
* will be recognized as being present on the page.
*/
api.selectiveRefresh.bind( 'render-partials-response', function( response ) {
if ( response.nav_menu_instance_args ) {
_.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args );
}
} );
}
api.preview.bind( 'active', function() {
@ -127,6 +154,31 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
);
},
/**
* Make sure that partial fallback behavior is invoked if there is no associated menu.
*
* @since 4.5.0
*
* @returns {Promise}
*/
refresh: function() {
var partial = this, menuId, deferred = $.Deferred();
// Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
menuId = partial.params.navMenuArgs.menu;
} else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
}
if ( ! menuId ) {
partial.fallback();
deferred.reject();
return deferred.promise();
}
return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
},
/**
* Render content.
*
@ -135,6 +187,12 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
*/
renderContent: function( placement ) {
var partial = this, previousContainer = placement.container;
// Do fallback behavior to refresh preview if menu is now empty.
if ( '' === placement.addedContent ) {
placement.partial.fallback();
}
if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
// Trigger deprecated event.
@ -152,33 +210,148 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
/**
* Watch for changes to nav_menu_locations[] settings.
* Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
*
* Refresh partials associated with the given nav_menu_locations[] setting,
* or request an entire preview refresh if there are no containers in the
* document for a partial associated with the theme location.
* @param {object} navMenuInstanceArgs
*/
self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
var unplacedNavMenuInstances;
unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
} );
if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
api.selectiveRefresh.requestFullRefresh();
return true;
}
return false;
};
/**
* Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
*
* @since 4.5.0
*
* @param {wp.customize.Value} setting
* @param {object} [options]
* @param {boolean} options.fire Whether to invoke the callback after binding.
* This is used when a dynamic setting is added.
* @return {boolean} Whether the setting was bound.
*/
self.watchNavMenuLocationChanges = function() {
api.bind( 'change', function( setting ) {
var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ );
if ( ! matches ) {
self.bindSettingListener = function( setting, options ) {
var matches;
options = options || {};
matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
if ( matches ) {
setting._navMenuId = parseInt( matches[1], 10 );
setting.bind( this.onChangeNavMenuSetting );
if ( options.fire ) {
this.onChangeNavMenuSetting.call( setting, setting(), false );
}
return true;
}
matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
if ( matches ) {
setting._navMenuItemId = parseInt( matches[1], 10 );
setting.bind( this.onChangeNavMenuItemSetting );
if ( options.fire ) {
this.onChangeNavMenuItemSetting.call( setting, setting(), false );
}
return true;
}
matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
if ( matches ) {
setting._navMenuThemeLocation = matches[1];
setting.bind( this.onChangeNavMenuLocationsSetting );
if ( options.fire ) {
this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
}
return true;
}
return false;
};
/**
* Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
*
* @since 4.5.0
*
* @param {wp.customize.Value} setting
*/
self.unbindSettingListener = function( setting ) {
setting.unbind( this.onChangeNavMenuSetting );
setting.unbind( this.onChangeNavMenuItemSetting );
setting.unbind( this.onChangeNavMenuLocationsSetting );
};
/**
* Handle change for nav_menu[] setting for nav menu instances lacking partials.
*
* @since 4.5.0
*
* @this {wp.customize.Value}
*/
self.onChangeNavMenuSetting = function() {
var setting = this;
self.handleUnplacedNavMenuInstances( {
menu: setting._navMenuId
} );
// Ensure all nav menu instances with a theme_location assigned to this menu are handled.
api.each( function( otherSetting ) {
if ( ! otherSetting._navMenuThemeLocation ) {
return;
}
themeLocation = matches[1];
api.selectiveRefresh.partial.each( function( partial ) {
if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) {
partial.refresh();
themeLocationPartialFound = true;
}
} );
if ( ! themeLocationPartialFound ) {
api.selectiveRefresh.requestFullRefresh();
if ( setting._navMenuId === otherSetting() ) {
self.handleUnplacedNavMenuInstances( {
theme_location: otherSetting._navMenuThemeLocation
} );
}
} );
};
/**
* Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
*
* @since 4.5.0
*
* @param {object} newItem New value for nav_menu_item[] setting.
* @param {object} oldItem Old value for nav_menu_item[] setting.
* @this {wp.customize.Value}
*/
self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
var item = newItem || oldItem, navMenuSetting;
navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
if ( navMenuSetting ) {
self.onChangeNavMenuSetting.call( navMenuSetting );
}
};
/**
* Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
*
* @since 4.5.0
*
* @this {wp.customize.Value}
*/
self.onChangeNavMenuLocationsSetting = function() {
var setting = this, hasNavMenuInstance;
self.handleUnplacedNavMenuInstances( {
theme_location: setting._navMenuThemeLocation
} );
// If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
theme_location: setting._navMenuThemeLocation
} );
if ( ! hasNavMenuInstance ) {
api.selectiveRefresh.requestFullRefresh();
}
};
}
/**

View File

@ -1 +1 @@
wp.customize.navMenusPreview=wp.customize.MenusCustomizerPreview=function(a,b,c,d){"use strict";var e={};return e.init=function(){var a=this;d.selectiveRefresh&&a.watchNavMenuLocationChanges(),d.preview.bind("active",function(){a.highlightControls()})},d.selectiveRefresh&&(e.NavMenuInstancePartial=d.selectiveRefresh.Partial.extend({initialize:function(a,c){var e,f,g=this;if(e=a.match(/^nav_menu_instance\[([0-9a-f]{32})]$/),!e)throw new Error("Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.");if(f=e[1],c=c||{},c.params=b.extend({selector:'[data-customize-partial-id="'+a+'"]',navMenuArgs:c.constructingContainerContext||{},containerInclusive:!0},c.params||{}),d.selectiveRefresh.Partial.prototype.initialize.call(g,a,c),!b.isObject(g.params.navMenuArgs))throw new Error("Missing navMenuArgs");if(g.params.navMenuArgs.args_hmac!==f)throw new Error("args_hmac mismatch with id")},isRelatedSetting:function(a,c,e){var f,g,h,i=this;if(b.isString(a)&&(a=d(a)),h=/^nav_menu_item\[/.test(a.id),h&&b.isObject(c)&&b.isObject(e)&&(delete c.type_label,delete e.type_label,b.isEqual(e,c)))return!1;if(i.params.navMenuArgs.theme_location){if("nav_menu_locations["+i.params.navMenuArgs.theme_location+"]"===a.id)return!0;f=d("nav_menu_locations["+i.params.navMenuArgs.theme_location+"]")}return g=i.params.navMenuArgs.menu,!g&&f&&(g=f()),g?"nav_menu["+g+"]"===a.id||h&&(c&&c.nav_menu_term_id===g||e&&e.nav_menu_term_id===g):!1},renderContent:function(b){var c=this,e=b.container;d.selectiveRefresh.Partial.prototype.renderContent.call(c,b)&&a(document).trigger("customize-preview-menu-refreshed",[{instanceNumber:null,wpNavArgs:b.context,wpNavMenuArgs:b.context,oldContainer:e,newContainer:b.container}])}}),d.selectiveRefresh.partialConstructor.nav_menu_instance=e.NavMenuInstancePartial,e.watchNavMenuLocationChanges=function(){d.bind("change",function(a){var b,c=!1,f=a.id.match(/^nav_menu_locations\[(.+)]$/);f&&(b=f[1],d.selectiveRefresh.partial.each(function(a){a.extended(e.NavMenuInstancePartial)&&a.params.navMenuArgs.theme_location===b&&(a.refresh(),c=!0)}),c||d.selectiveRefresh.requestFullRefresh())})}),e.highlightControls=function(){var b=".menu-item";a(document).on("click",b,function(b){var c;b.shiftKey&&(c=a(this).attr("class").match(/(?:^|\s)menu-item-(\d+)(?:\s|$)/),c&&(b.preventDefault(),b.stopPropagation(),d.preview.send("focus-nav-menu-item-control",parseInt(c[1],10))))})},d.bind("preview-ready",function(){e.init()}),e}(jQuery,_,wp,wp.customize);
wp.customize.navMenusPreview=wp.customize.MenusCustomizerPreview=function(a,b,c,d){"use strict";var e={data:{navMenuInstanceArgs:{}}};return"undefined"!=typeof _wpCustomizePreviewNavMenusExports&&b.extend(e.data,_wpCustomizePreviewNavMenusExports),e.init=function(){var a=this;d.selectiveRefresh&&(d.each(function(b){a.bindSettingListener(b)}),d.bind("add",function(b){a.bindSettingListener(b,{fire:!0})}),d.bind("remove",function(b){a.unbindSettingListener(b)}),d.selectiveRefresh.bind("render-partials-response",function(c){c.nav_menu_instance_args&&b.extend(a.data.navMenuInstanceArgs,c.nav_menu_instance_args)})),d.preview.bind("active",function(){a.highlightControls()})},d.selectiveRefresh&&(e.NavMenuInstancePartial=d.selectiveRefresh.Partial.extend({initialize:function(a,c){var e,f,g=this;if(e=a.match(/^nav_menu_instance\[([0-9a-f]{32})]$/),!e)throw new Error("Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.");if(f=e[1],c=c||{},c.params=b.extend({selector:'[data-customize-partial-id="'+a+'"]',navMenuArgs:c.constructingContainerContext||{},containerInclusive:!0},c.params||{}),d.selectiveRefresh.Partial.prototype.initialize.call(g,a,c),!b.isObject(g.params.navMenuArgs))throw new Error("Missing navMenuArgs");if(g.params.navMenuArgs.args_hmac!==f)throw new Error("args_hmac mismatch with id")},isRelatedSetting:function(a,c,e){var f,g,h,i=this;if(b.isString(a)&&(a=d(a)),h=/^nav_menu_item\[/.test(a.id),h&&b.isObject(c)&&b.isObject(e)&&(delete c.type_label,delete e.type_label,b.isEqual(e,c)))return!1;if(i.params.navMenuArgs.theme_location){if("nav_menu_locations["+i.params.navMenuArgs.theme_location+"]"===a.id)return!0;f=d("nav_menu_locations["+i.params.navMenuArgs.theme_location+"]")}return g=i.params.navMenuArgs.menu,!g&&f&&(g=f()),g?"nav_menu["+g+"]"===a.id||h&&(c&&c.nav_menu_term_id===g||e&&e.nav_menu_term_id===g):!1},refresh:function(){var c,e=this,f=a.Deferred();return b.isNumber(e.params.navMenuArgs.menu)?c=e.params.navMenuArgs.menu:e.params.navMenuArgs.theme_location&&d.has("nav_menu_locations["+e.params.navMenuArgs.theme_location+"]")&&(c=d("nav_menu_locations["+e.params.navMenuArgs.theme_location+"]").get()),c?d.selectiveRefresh.Partial.prototype.refresh.call(e):(e.fallback(),f.reject(),f.promise())},renderContent:function(b){var c=this,e=b.container;""===b.addedContent&&b.partial.fallback(),d.selectiveRefresh.Partial.prototype.renderContent.call(c,b)&&a(document).trigger("customize-preview-menu-refreshed",[{instanceNumber:null,wpNavArgs:b.context,wpNavMenuArgs:b.context,oldContainer:e,newContainer:b.container}])}}),d.selectiveRefresh.partialConstructor.nav_menu_instance=e.NavMenuInstancePartial,e.handleUnplacedNavMenuInstances=function(a){var c;return c=b.filter(b.values(e.data.navMenuInstanceArgs),function(a){return!d.selectiveRefresh.partial.has("nav_menu_instance["+a.args_hmac+"]")}),b.findWhere(c,a)?(d.selectiveRefresh.requestFullRefresh(),!0):!1},e.bindSettingListener=function(a,b){var c;return b=b||{},(c=a.id.match(/^nav_menu\[(-?\d+)]$/))?(a._navMenuId=parseInt(c[1],10),a.bind(this.onChangeNavMenuSetting),b.fire&&this.onChangeNavMenuSetting.call(a,a(),!1),!0):(c=a.id.match(/^nav_menu_item\[(-?\d+)]$/))?(a._navMenuItemId=parseInt(c[1],10),a.bind(this.onChangeNavMenuItemSetting),b.fire&&this.onChangeNavMenuItemSetting.call(a,a(),!1),!0):(c=a.id.match(/^nav_menu_locations\[(.+?)]/),c?(a._navMenuThemeLocation=c[1],a.bind(this.onChangeNavMenuLocationsSetting),b.fire&&this.onChangeNavMenuLocationsSetting.call(a,a(),!1),!0):!1)},e.unbindSettingListener=function(a){a.unbind(this.onChangeNavMenuSetting),a.unbind(this.onChangeNavMenuItemSetting),a.unbind(this.onChangeNavMenuLocationsSetting)},e.onChangeNavMenuSetting=function(){var a=this;e.handleUnplacedNavMenuInstances({menu:a._navMenuId}),d.each(function(b){b._navMenuThemeLocation&&a._navMenuId===b()&&e.handleUnplacedNavMenuInstances({theme_location:b._navMenuThemeLocation})})},e.onChangeNavMenuItemSetting=function(a,b){var c,f=a||b;c=d("nav_menu["+String(f.nav_menu_term_id)+"]"),c&&e.onChangeNavMenuSetting.call(c)},e.onChangeNavMenuLocationsSetting=function(){var a,c=this;e.handleUnplacedNavMenuInstances({theme_location:c._navMenuThemeLocation}),a=!!b.findWhere(b.values(e.data.navMenuInstanceArgs),{theme_location:c._navMenuThemeLocation}),a||d.selectiveRefresh.requestFullRefresh()}),e.highlightControls=function(){var b=".menu-item";a(document).on("click",b,function(b){var c;b.shiftKey&&(c=a(this).attr("class").match(/(?:^|\s)menu-item-(\d+)(?:\s|$)/),c&&(b.preventDefault(),b.stopPropagation(),d.preview.send("focus-nav-menu-item-control",parseInt(c[1],10))))})},d.bind("preview-ready",function(){e.init()}),e}(jQuery,_,wp,wp.customize);

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.5-beta2-36888';
$wp_version = '4.5-beta2-36889';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.