1 /* global _wpCustomizePreviewNavMenusExports */
2 wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
7 navMenuInstanceArgs: {}
10 if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
11 _.extend( self.data, _wpCustomizePreviewNavMenusExports );
15 * Initialize nav menus preview.
17 self.init = function() {
18 var self = this, synced = false;
21 * Keep track of whether we synced to determine whether or not bindSettingListener
22 * should also initially fire the listener. This initial firing needs to wait until
23 * after all of the settings have been synced from the pane in order to prevent
24 * an infinite selective fallback-refresh. Note that this sync handler will be
25 * added after the sync handler in customize-preview.js, so it will be triggered
26 * after all of the settings are added.
28 api.preview.bind( 'sync', function() {
32 if ( api.selectiveRefresh ) {
33 // Listen for changes to settings related to nav menus.
34 api.each( function( setting ) {
35 self.bindSettingListener( setting );
37 api.bind( 'add', function( setting ) {
40 * Handle case where an invalid nav menu item (one for which its associated object has been deleted)
41 * is synced from the controls into the preview. Since invalid nav menu items are filtered out from
42 * being exported to the frontend by the _is_valid_nav_menu_item filter in wp_get_nav_menu_items(),
43 * the customizer controls will have a nav_menu_item setting where the preview will have none, and
44 * this can trigger an infinite fallback refresh when the nav menu item lacks any valid items.
46 if ( setting.get() && ! setting.get()._invalid ) {
47 self.bindSettingListener( setting, { fire: synced } );
50 api.bind( 'remove', function( setting ) {
51 self.unbindSettingListener( setting );
55 * Ensure that wp_nav_menu() instances nested inside of other partials
56 * will be recognized as being present on the page.
58 api.selectiveRefresh.bind( 'render-partials-response', function( response ) {
59 if ( response.nav_menu_instance_args ) {
60 _.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args );
65 api.preview.bind( 'active', function() {
66 self.highlightControls();
70 if ( api.selectiveRefresh ) {
73 * Partial representing an invocation of wp_nav_menu().
76 * @augments wp.customize.selectiveRefresh.Partial
79 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
85 * @param {string} id - Partial ID.
86 * @param {Object} options
87 * @param {Object} options.params
88 * @param {Object} options.params.navMenuArgs
89 * @param {string} options.params.navMenuArgs.args_hmac
90 * @param {string} [options.params.navMenuArgs.theme_location]
91 * @param {number} [options.params.navMenuArgs.menu]
92 * @param {object} [options.constructingContainerContext]
94 initialize: function( id, options ) {
95 var partial = this, matches, argsHmac;
96 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
98 throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
100 argsHmac = matches[1];
102 options = options || {};
103 options.params = _.extend(
105 selector: '[data-customize-partial-id="' + id + '"]',
106 navMenuArgs: options.constructingContainerContext || {},
107 containerInclusive: true
111 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
113 if ( ! _.isObject( partial.params.navMenuArgs ) ) {
114 throw new Error( 'Missing navMenuArgs' );
116 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
117 throw new Error( 'args_hmac mismatch with id' );
122 * Return whether the setting is related to this partial.
125 * @param {wp.customize.Value|string} setting - Object or ID.
126 * @param {number|object|false|null} newValue - New value, or null if the setting was just removed.
127 * @param {number|object|false|null} oldValue - Old value, or null if the setting was just added.
130 isRelatedSetting: function( setting, newValue, oldValue ) {
131 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser;
132 if ( _.isString( setting ) ) {
133 setting = api( setting );
137 * Prevent nav_menu_item changes only containing type_label differences triggering a refresh.
138 * These settings in the preview do not include type_label property, and so if one of these
139 * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective
140 * refresh immediately because the setting from the pane would have the type_label whereas
141 * the setting in the preview would not, thus triggering a change event. The following
142 * condition short-circuits this unnecessary selective refresh and also prevents an infinite
143 * loop in the case where a nav_menu_instance partial had done a fallback refresh.
144 * @todo Nav menu item settings should not include a type_label property to begin with.
146 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id );
147 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) {
148 _newValue = _.clone( newValue );
149 _oldValue = _.clone( oldValue );
150 delete _newValue.type_label;
151 delete _oldValue.type_label;
153 // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load.
154 if ( 'https' === api.preview.scheme.get() ) {
155 urlParser = document.createElement( 'a' );
156 urlParser.href = _newValue.url;
157 urlParser.protocol = 'https:';
158 _newValue.url = urlParser.href;
159 urlParser.href = _oldValue.url;
160 urlParser.protocol = 'https:';
161 _oldValue.url = urlParser.href;
164 // Prevent original_title differences from causing refreshes if title is present.
165 if ( newValue.title ) {
166 delete _oldValue.original_title;
167 delete _newValue.original_title;
170 if ( _.isEqual( _oldValue, _newValue ) ) {
175 if ( partial.params.navMenuArgs.theme_location ) {
176 if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
179 navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
182 navMenuId = partial.params.navMenuArgs.menu;
183 if ( ! navMenuId && navMenuLocationSetting ) {
184 navMenuId = navMenuLocationSetting();
191 ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
192 ( isNavMenuItemSetting && (
193 ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
194 ( oldValue && oldValue.nav_menu_term_id === navMenuId )
200 * Make sure that partial fallback behavior is invoked if there is no associated menu.
206 refresh: function() {
207 var partial = this, menuId, deferred = $.Deferred();
209 // Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
210 if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
211 menuId = partial.params.navMenuArgs.menu;
212 } else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
213 menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
218 return deferred.promise();
221 return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
228 * @param {wp.customize.selectiveRefresh.Placement} placement
230 renderContent: function( placement ) {
231 var partial = this, previousContainer = placement.container;
233 // Do fallback behavior to refresh preview if menu is now empty.
234 if ( '' === placement.addedContent ) {
235 placement.partial.fallback();
238 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
240 // Trigger deprecated event.
241 $( document ).trigger( 'customize-preview-menu-refreshed', [ {
242 instanceNumber: null, // @deprecated
243 wpNavArgs: placement.context, // @deprecated
244 wpNavMenuArgs: placement.context,
245 oldContainer: previousContainer,
246 newContainer: placement.container
252 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
255 * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
257 * @param {object} navMenuInstanceArgs
259 self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
260 var unplacedNavMenuInstances;
261 unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
262 return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
264 if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
265 api.selectiveRefresh.requestFullRefresh();
272 * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
276 * @param {wp.customize.Value} setting
277 * @param {object} [options]
278 * @param {boolean} options.fire Whether to invoke the callback after binding.
279 * This is used when a dynamic setting is added.
280 * @return {boolean} Whether the setting was bound.
282 self.bindSettingListener = function( setting, options ) {
284 options = options || {};
286 matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
288 setting._navMenuId = parseInt( matches[1], 10 );
289 setting.bind( this.onChangeNavMenuSetting );
290 if ( options.fire ) {
291 this.onChangeNavMenuSetting.call( setting, setting(), false );
296 matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
298 setting._navMenuItemId = parseInt( matches[1], 10 );
299 setting.bind( this.onChangeNavMenuItemSetting );
300 if ( options.fire ) {
301 this.onChangeNavMenuItemSetting.call( setting, setting(), false );
306 matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
308 setting._navMenuThemeLocation = matches[1];
309 setting.bind( this.onChangeNavMenuLocationsSetting );
310 if ( options.fire ) {
311 this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
320 * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
324 * @param {wp.customize.Value} setting
326 self.unbindSettingListener = function( setting ) {
327 setting.unbind( this.onChangeNavMenuSetting );
328 setting.unbind( this.onChangeNavMenuItemSetting );
329 setting.unbind( this.onChangeNavMenuLocationsSetting );
333 * Handle change for nav_menu[] setting for nav menu instances lacking partials.
337 * @this {wp.customize.Value}
339 self.onChangeNavMenuSetting = function() {
342 self.handleUnplacedNavMenuInstances( {
343 menu: setting._navMenuId
346 // Ensure all nav menu instances with a theme_location assigned to this menu are handled.
347 api.each( function( otherSetting ) {
348 if ( ! otherSetting._navMenuThemeLocation ) {
351 if ( setting._navMenuId === otherSetting() ) {
352 self.handleUnplacedNavMenuInstances( {
353 theme_location: otherSetting._navMenuThemeLocation
360 * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
364 * @param {object} newItem New value for nav_menu_item[] setting.
365 * @param {object} oldItem Old value for nav_menu_item[] setting.
366 * @this {wp.customize.Value}
368 self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
369 var item = newItem || oldItem, navMenuSetting;
370 navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
371 if ( navMenuSetting ) {
372 self.onChangeNavMenuSetting.call( navMenuSetting );
377 * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
381 * @this {wp.customize.Value}
383 self.onChangeNavMenuLocationsSetting = function() {
384 var setting = this, hasNavMenuInstance;
385 self.handleUnplacedNavMenuInstances( {
386 theme_location: setting._navMenuThemeLocation
389 // If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
390 hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
391 theme_location: setting._navMenuThemeLocation
393 if ( ! hasNavMenuInstance ) {
394 api.selectiveRefresh.requestFullRefresh();
400 * Connect nav menu items with their corresponding controls in the pane.
402 * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
403 * Also this applies even if a nav menu is not partial-refreshable.
407 self.highlightControls = function() {
408 var selector = '.menu-item';
410 // Skip adding highlights if not in the customizer preview iframe.
411 if ( ! api.settings.channel ) {
415 // Focus on the menu item control when shift+clicking the menu item.
416 $( document ).on( 'click', selector, function( e ) {
417 var navMenuItemParts;
418 if ( ! e.shiftKey ) {
422 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
423 if ( navMenuItemParts ) {
425 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
426 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
431 api.bind( 'preview-ready', function() {
437 }( jQuery, _, wp, wp.customize ) );