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. git-svn-id: https://develop.svn.wordpress.org/trunk@36889 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
parent
f07f2252fe
commit
dcbbeffc57
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -617,15 +617,17 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
|
||||
function test_filter_wp_nav_menu_args() {
|
||||
do_action( 'customize_register', $this->wp_customize );
|
||||
$menus = $this->wp_customize->nav_menus;
|
||||
$menu_id = wp_create_nav_menu( 'Foo' );
|
||||
|
||||
$results = $menus->filter_wp_nav_menu_args( array(
|
||||
'echo' => true,
|
||||
'fallback_cb' => 'wp_page_menu',
|
||||
'walker' => '',
|
||||
'menu' => wp_create_nav_menu( 'Foo' ),
|
||||
'menu' => $menu_id,
|
||||
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
|
||||
) );
|
||||
$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
|
||||
$this->assertTrue( $results['can_partial_refresh'] );
|
||||
|
||||
$results = $menus->filter_wp_nav_menu_args( array(
|
||||
'echo' => false,
|
||||
@ -633,7 +635,8 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
|
||||
'walker' => new Walker_Nav_Menu(),
|
||||
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
|
||||
) );
|
||||
$this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results );
|
||||
$this->assertFalse( $results['can_partial_refresh'] );
|
||||
$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
|
||||
$this->assertEquals( 'wp_page_menu', $results['fallback_cb'] );
|
||||
|
||||
$nav_menu_term = get_term( wp_create_nav_menu( 'Bar' ) );
|
||||
@ -644,8 +647,39 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
|
||||
'menu' => $nav_menu_term,
|
||||
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
|
||||
) );
|
||||
$this->assertTrue( $results['can_partial_refresh'] );
|
||||
$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
|
||||
$this->assertEquals( $nav_menu_term->term_id, $results['customize_preview_nav_menus_args']['menu'] );
|
||||
|
||||
$results = $menus->filter_wp_nav_menu_args( array(
|
||||
'echo' => true,
|
||||
'fallback_cb' => 'wp_page_menu',
|
||||
'walker' => '',
|
||||
'menu' => $menu_id,
|
||||
'container' => 'div',
|
||||
'items_wrap' => '%3$s',
|
||||
) );
|
||||
$this->assertTrue( $results['can_partial_refresh'] );
|
||||
|
||||
$results = $menus->filter_wp_nav_menu_args( array(
|
||||
'echo' => true,
|
||||
'fallback_cb' => 'wp_page_menu',
|
||||
'walker' => '',
|
||||
'menu' => $menu_id,
|
||||
'container' => false,
|
||||
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
|
||||
) );
|
||||
$this->assertTrue( $results['can_partial_refresh'] );
|
||||
|
||||
$results = $menus->filter_wp_nav_menu_args( array(
|
||||
'echo' => true,
|
||||
'fallback_cb' => 'wp_page_menu',
|
||||
'walker' => '',
|
||||
'menu' => $menu_id,
|
||||
'container' => false,
|
||||
'items_wrap' => '%3$s',
|
||||
) );
|
||||
$this->assertFalse( $results['can_partial_refresh'] );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -690,6 +724,20 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
|
||||
$this->assertTrue( wp_script_is( 'customize-preview-nav-menus' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test WP_Customize_Nav_Menus::export_preview_data() method.
|
||||
*
|
||||
* @see WP_Customize_Nav_Menus::export_preview_data()
|
||||
*/
|
||||
function test_export_preview_data() {
|
||||
ob_start();
|
||||
$this->wp_customize->nav_menus->export_preview_data();
|
||||
$html = ob_get_clean();
|
||||
$this->assertTrue( (bool) preg_match( '/_wpCustomizePreviewNavMenusExports = ({.+})/s', $html, $matches ) );
|
||||
$exported_data = json_decode( $matches[1], true );
|
||||
$this->assertArrayHasKey( 'navMenuInstanceArgs', $exported_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test WP_Customize_Nav_Menus::render_nav_menu_partial() method.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user