Spaces:
Sleeping
Sleeping
/** | |
* @output wp-includes/js/customize-preview-nav-menus.js | |
*/ | |
/* 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. | |
* @return {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 | |
* | |
* @return {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 ) ); | |