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() {
20 if ( api.selectiveRefresh ) {
21 // Listen for changes to settings related to nav menus.
22 api.each( function( setting ) {
23 self.bindSettingListener( setting );
25 api.bind( 'add', function( setting ) {
26 self.bindSettingListener( setting, { fire: true } );
28 api.bind( 'remove', function( setting ) {
29 self.unbindSettingListener( setting );
33 * Ensure that wp_nav_menu() instances nested inside of other partials
34 * will be recognized as being present on the page.
36 api.selectiveRefresh.bind( 'render-partials-response', function( response ) {
37 if ( response.nav_menu_instance_args ) {
38 _.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args );
43 api.preview.bind( 'active', function() {
44 self.highlightControls();
48 if ( api.selectiveRefresh ) {
51 * Partial representing an invocation of wp_nav_menu().
54 * @augments wp.customize.selectiveRefresh.Partial
57 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
63 * @param {string} id - Partial ID.
64 * @param {Object} options
65 * @param {Object} options.params
66 * @param {Object} options.params.navMenuArgs
67 * @param {string} options.params.navMenuArgs.args_hmac
68 * @param {string} [options.params.navMenuArgs.theme_location]
69 * @param {number} [options.params.navMenuArgs.menu]
70 * @param {object} [options.constructingContainerContext]
72 initialize: function( id, options ) {
73 var partial = this, matches, argsHmac;
74 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
76 throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
78 argsHmac = matches[1];
80 options = options || {};
81 options.params = _.extend(
83 selector: '[data-customize-partial-id="' + id + '"]',
84 navMenuArgs: options.constructingContainerContext || {},
85 containerInclusive: true
89 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
91 if ( ! _.isObject( partial.params.navMenuArgs ) ) {
92 throw new Error( 'Missing navMenuArgs' );
94 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
95 throw new Error( 'args_hmac mismatch with id' );
100 * Return whether the setting is related to this partial.
103 * @param {wp.customize.Value|string} setting - Object or ID.
104 * @param {number|object|false|null} newValue - New value, or null if the setting was just removed.
105 * @param {number|object|false|null} oldValue - Old value, or null if the setting was just added.
108 isRelatedSetting: function( setting, newValue, oldValue ) {
109 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting;
110 if ( _.isString( setting ) ) {
111 setting = api( setting );
115 * Prevent nav_menu_item changes only containing type_label differences triggering a refresh.
116 * These settings in the preview do not include type_label property, and so if one of these
117 * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective
118 * refresh immediately because the setting from the pane would have the type_label whereas
119 * the setting in the preview would not, thus triggering a change event. The following
120 * condition short-circuits this unnecessary selective refresh and also prevents an infinite
121 * loop in the case where a nav_menu_instance partial had done a fallback refresh.
122 * @todo Nav menu item settings should not include a type_label property to begin with.
124 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id );
125 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) {
126 delete newValue.type_label;
127 delete oldValue.type_label;
128 if ( _.isEqual( oldValue, newValue ) ) {
133 if ( partial.params.navMenuArgs.theme_location ) {
134 if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
137 navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
140 navMenuId = partial.params.navMenuArgs.menu;
141 if ( ! navMenuId && navMenuLocationSetting ) {
142 navMenuId = navMenuLocationSetting();
149 ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
150 ( isNavMenuItemSetting && (
151 ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
152 ( oldValue && oldValue.nav_menu_term_id === navMenuId )
158 * Make sure that partial fallback behavior is invoked if there is no associated menu.
164 refresh: function() {
165 var partial = this, menuId, deferred = $.Deferred();
167 // Make sure the fallback behavior is invoked when the partial is no longer associated with a menu.
168 if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
169 menuId = partial.params.navMenuArgs.menu;
170 } else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
171 menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
176 return deferred.promise();
179 return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
186 * @param {wp.customize.selectiveRefresh.Placement} placement
188 renderContent: function( placement ) {
189 var partial = this, previousContainer = placement.container;
191 // Do fallback behavior to refresh preview if menu is now empty.
192 if ( '' === placement.addedContent ) {
193 placement.partial.fallback();
196 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
198 // Trigger deprecated event.
199 $( document ).trigger( 'customize-preview-menu-refreshed', [ {
200 instanceNumber: null, // @deprecated
201 wpNavArgs: placement.context, // @deprecated
202 wpNavMenuArgs: placement.context,
203 oldContainer: previousContainer,
204 newContainer: placement.container
210 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
213 * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
215 * @param {object} navMenuInstanceArgs
217 self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
218 var unplacedNavMenuInstances;
219 unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
220 return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
222 if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
223 api.selectiveRefresh.requestFullRefresh();
230 * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
234 * @param {wp.customize.Value} setting
235 * @param {object} [options]
236 * @param {boolean} options.fire Whether to invoke the callback after binding.
237 * This is used when a dynamic setting is added.
238 * @return {boolean} Whether the setting was bound.
240 self.bindSettingListener = function( setting, options ) {
242 options = options || {};
244 matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
246 setting._navMenuId = parseInt( matches[1], 10 );
247 setting.bind( this.onChangeNavMenuSetting );
248 if ( options.fire ) {
249 this.onChangeNavMenuSetting.call( setting, setting(), false );
254 matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
256 setting._navMenuItemId = parseInt( matches[1], 10 );
257 setting.bind( this.onChangeNavMenuItemSetting );
258 if ( options.fire ) {
259 this.onChangeNavMenuItemSetting.call( setting, setting(), false );
264 matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
266 setting._navMenuThemeLocation = matches[1];
267 setting.bind( this.onChangeNavMenuLocationsSetting );
268 if ( options.fire ) {
269 this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
278 * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
282 * @param {wp.customize.Value} setting
284 self.unbindSettingListener = function( setting ) {
285 setting.unbind( this.onChangeNavMenuSetting );
286 setting.unbind( this.onChangeNavMenuItemSetting );
287 setting.unbind( this.onChangeNavMenuLocationsSetting );
291 * Handle change for nav_menu[] setting for nav menu instances lacking partials.
295 * @this {wp.customize.Value}
297 self.onChangeNavMenuSetting = function() {
300 self.handleUnplacedNavMenuInstances( {
301 menu: setting._navMenuId
304 // Ensure all nav menu instances with a theme_location assigned to this menu are handled.
305 api.each( function( otherSetting ) {
306 if ( ! otherSetting._navMenuThemeLocation ) {
309 if ( setting._navMenuId === otherSetting() ) {
310 self.handleUnplacedNavMenuInstances( {
311 theme_location: otherSetting._navMenuThemeLocation
318 * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
322 * @param {object} newItem New value for nav_menu_item[] setting.
323 * @param {object} oldItem Old value for nav_menu_item[] setting.
324 * @this {wp.customize.Value}
326 self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
327 var item = newItem || oldItem, navMenuSetting;
328 navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
329 if ( navMenuSetting ) {
330 self.onChangeNavMenuSetting.call( navMenuSetting );
335 * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
339 * @this {wp.customize.Value}
341 self.onChangeNavMenuLocationsSetting = function() {
342 var setting = this, hasNavMenuInstance;
343 self.handleUnplacedNavMenuInstances( {
344 theme_location: setting._navMenuThemeLocation
347 // If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
348 hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
349 theme_location: setting._navMenuThemeLocation
351 if ( ! hasNavMenuInstance ) {
352 api.selectiveRefresh.requestFullRefresh();
358 * Connect nav menu items with their corresponding controls in the pane.
360 * Setup shift-click on nav menu items which are more granular than the nav menu partial itself.
361 * Also this applies even if a nav menu is not partial-refreshable.
365 self.highlightControls = function() {
366 var selector = '.menu-item';
368 // Focus on the menu item control when shift+clicking the menu item.
369 $( document ).on( 'click', selector, function( e ) {
370 var navMenuItemParts;
371 if ( ! e.shiftKey ) {
375 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
376 if ( navMenuItemParts ) {
378 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items.
379 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) );
384 api.bind( 'preview-ready', function() {
390 }( jQuery, _, wp, wp.customize ) );