You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
15 KiB
442 lines
15 KiB
/* global _wpCustomizePreviewNavMenusExports */ |
|
|
|
/** @namespace wp.customize.navMenusPreview */ |
|
wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) { |
|
'use strict'; |
|
|
|
var self = { |
|
data: { |
|
navMenuInstanceArgs: {} |
|
} |
|
}; |
|
if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) { |
|
_.extend( self.data, _wpCustomizePreviewNavMenusExports ); |
|
} |
|
|
|
/** |
|
* Initialize nav menus preview. |
|
*/ |
|
self.init = function() { |
|
var self = this, synced = false; |
|
|
|
/* |
|
* Keep track of whether we synced to determine whether or not bindSettingListener |
|
* should also initially fire the listener. This initial firing needs to wait until |
|
* after all of the settings have been synced from the pane in order to prevent |
|
* an infinite selective fallback-refresh. Note that this sync handler will be |
|
* added after the sync handler in customize-preview.js, so it will be triggered |
|
* after all of the settings are added. |
|
*/ |
|
api.preview.bind( 'sync', function() { |
|
synced = true; |
|
} ); |
|
|
|
if ( api.selectiveRefresh ) { |
|
// Listen for changes to settings related to nav menus. |
|
api.each( function( setting ) { |
|
self.bindSettingListener( setting ); |
|
} ); |
|
api.bind( 'add', function( setting ) { |
|
|
|
/* |
|
* Handle case where an invalid nav menu item (one for which its associated object has been deleted) |
|
* is synced from the controls into the preview. Since invalid nav menu items are filtered out from |
|
* being exported to the frontend by the _is_valid_nav_menu_item filter in wp_get_nav_menu_items(), |
|
* the customizer controls will have a nav_menu_item setting where the preview will have none, and |
|
* this can trigger an infinite fallback refresh when the nav menu item lacks any valid items. |
|
*/ |
|
if ( setting.get() && ! setting.get()._invalid ) { |
|
self.bindSettingListener( setting, { fire: synced } ); |
|
} |
|
} ); |
|
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() { |
|
self.highlightControls(); |
|
} ); |
|
}; |
|
|
|
if ( api.selectiveRefresh ) { |
|
|
|
/** |
|
* Partial representing an invocation of wp_nav_menu(). |
|
* |
|
* @memberOf wp.customize.navMenusPreview |
|
* @alias wp.customize.navMenusPreview.NavMenuInstancePartial |
|
* |
|
* @class |
|
* @augments wp.customize.selectiveRefresh.Partial |
|
* @since 4.5.0 |
|
*/ |
|
self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.navMenusPreview.NavMenuInstancePartial.prototype */{ |
|
|
|
/** |
|
* Constructor. |
|
* |
|
* @since 4.5.0 |
|
* @param {string} id - Partial ID. |
|
* @param {Object} options |
|
* @param {Object} options.params |
|
* @param {Object} options.params.navMenuArgs |
|
* @param {string} options.params.navMenuArgs.args_hmac |
|
* @param {string} [options.params.navMenuArgs.theme_location] |
|
* @param {number} [options.params.navMenuArgs.menu] |
|
* @param {object} [options.constructingContainerContext] |
|
*/ |
|
initialize: function( id, options ) { |
|
var partial = this, matches, argsHmac; |
|
matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ ); |
|
if ( ! matches ) { |
|
throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' ); |
|
} |
|
argsHmac = matches[1]; |
|
|
|
options = options || {}; |
|
options.params = _.extend( |
|
{ |
|
selector: '[data-customize-partial-id="' + id + '"]', |
|
navMenuArgs: options.constructingContainerContext || {}, |
|
containerInclusive: true |
|
}, |
|
options.params || {} |
|
); |
|
api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); |
|
|
|
if ( ! _.isObject( partial.params.navMenuArgs ) ) { |
|
throw new Error( 'Missing navMenuArgs' ); |
|
} |
|
if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) { |
|
throw new Error( 'args_hmac mismatch with id' ); |
|
} |
|
}, |
|
|
|
/** |
|
* Return whether the setting is related to this partial. |
|
* |
|
* @since 4.5.0 |
|
* @param {wp.customize.Value|string} setting - Object or ID. |
|
* @param {number|object|false|null} newValue - New value, or null if the setting was just removed. |
|
* @param {number|object|false|null} oldValue - Old value, or null if the setting was just added. |
|
* @returns {boolean} |
|
*/ |
|
isRelatedSetting: function( setting, newValue, oldValue ) { |
|
var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser; |
|
if ( _.isString( setting ) ) { |
|
setting = api( setting ); |
|
} |
|
|
|
/* |
|
* Prevent nav_menu_item changes only containing type_label differences triggering a refresh. |
|
* These settings in the preview do not include type_label property, and so if one of these |
|
* nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective |
|
* refresh immediately because the setting from the pane would have the type_label whereas |
|
* the setting in the preview would not, thus triggering a change event. The following |
|
* condition short-circuits this unnecessary selective refresh and also prevents an infinite |
|
* loop in the case where a nav_menu_instance partial had done a fallback refresh. |
|
* @todo Nav menu item settings should not include a type_label property to begin with. |
|
*/ |
|
isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id ); |
|
if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) { |
|
_newValue = _.clone( newValue ); |
|
_oldValue = _.clone( oldValue ); |
|
delete _newValue.type_label; |
|
delete _oldValue.type_label; |
|
|
|
// Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load. |
|
if ( 'https' === api.preview.scheme.get() ) { |
|
urlParser = document.createElement( 'a' ); |
|
urlParser.href = _newValue.url; |
|
urlParser.protocol = 'https:'; |
|
_newValue.url = urlParser.href; |
|
urlParser.href = _oldValue.url; |
|
urlParser.protocol = 'https:'; |
|
_oldValue.url = urlParser.href; |
|
} |
|
|
|
// Prevent original_title differences from causing refreshes if title is present. |
|
if ( newValue.title ) { |
|
delete _oldValue.original_title; |
|
delete _newValue.original_title; |
|
} |
|
|
|
if ( _.isEqual( _oldValue, _newValue ) ) { |
|
return false; |
|
} |
|
} |
|
|
|
if ( partial.params.navMenuArgs.theme_location ) { |
|
if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) { |
|
return true; |
|
} |
|
navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ); |
|
} |
|
|
|
navMenuId = partial.params.navMenuArgs.menu; |
|
if ( ! navMenuId && navMenuLocationSetting ) { |
|
navMenuId = navMenuLocationSetting(); |
|
} |
|
|
|
if ( ! navMenuId ) { |
|
return false; |
|
} |
|
return ( |
|
( 'nav_menu[' + navMenuId + ']' === setting.id ) || |
|
( isNavMenuItemSetting && ( |
|
( newValue && newValue.nav_menu_term_id === navMenuId ) || |
|
( oldValue && oldValue.nav_menu_term_id === navMenuId ) |
|
) ) |
|
); |
|
}, |
|
|
|
/** |
|
* 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. |
|
* |
|
* @inheritdoc |
|
* @param {wp.customize.selectiveRefresh.Placement} placement |
|
*/ |
|
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. |
|
$( document ).trigger( 'customize-preview-menu-refreshed', [ { |
|
instanceNumber: null, // @deprecated |
|
wpNavArgs: placement.context, // @deprecated |
|
wpNavMenuArgs: placement.context, |
|
oldContainer: previousContainer, |
|
newContainer: placement.container |
|
} ] ); |
|
} |
|
} |
|
}); |
|
|
|
api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial; |
|
|
|
/** |
|
* Request full refresh if there are nav menu instances that lack partials which also match the supplied args. |
|
* |
|
* @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.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; |
|
} |
|
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(); |
|
} |
|
}; |
|
} |
|
|
|
/** |
|
* Connect nav menu items with their corresponding controls in the pane. |
|
* |
|
* Setup shift-click on nav menu items which are more granular than the nav menu partial itself. |
|
* Also this applies even if a nav menu is not partial-refreshable. |
|
* |
|
* @since 4.5.0 |
|
*/ |
|
self.highlightControls = function() { |
|
var selector = '.menu-item'; |
|
|
|
// Skip adding highlights if not in the customizer preview iframe. |
|
if ( ! api.settings.channel ) { |
|
return; |
|
} |
|
|
|
// Focus on the menu item control when shift+clicking the menu item. |
|
$( document ).on( 'click', selector, function( e ) { |
|
var navMenuItemParts; |
|
if ( ! e.shiftKey ) { |
|
return; |
|
} |
|
|
|
navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(-?\d+)(?:\s|$)/ ); |
|
if ( navMenuItemParts ) { |
|
e.preventDefault(); |
|
e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items. |
|
api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) ); |
|
} |
|
}); |
|
}; |
|
|
|
api.bind( 'preview-ready', function() { |
|
self.init(); |
|
} ); |
|
|
|
return self; |
|
|
|
}( jQuery, _, wp, wp.customize ) );
|
|
|