|
1 /* global _wpCustomizePreviewNavMenusExports */ |
|
2 |
|
3 /** @namespace wp.customize.navMenusPreview */ |
|
4 wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) { |
|
5 'use strict'; |
|
6 |
|
7 var self = { |
|
8 data: { |
|
9 navMenuInstanceArgs: {} |
|
10 } |
|
11 }; |
|
12 if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) { |
|
13 _.extend( self.data, _wpCustomizePreviewNavMenusExports ); |
|
14 } |
|
15 |
|
16 /** |
|
17 * Initialize nav menus preview. |
|
18 */ |
|
19 self.init = function() { |
|
20 var self = this, synced = false; |
|
21 |
|
22 /* |
|
23 * Keep track of whether we synced to determine whether or not bindSettingListener |
|
24 * should also initially fire the listener. This initial firing needs to wait until |
|
25 * after all of the settings have been synced from the pane in order to prevent |
|
26 * an infinite selective fallback-refresh. Note that this sync handler will be |
|
27 * added after the sync handler in customize-preview.js, so it will be triggered |
|
28 * after all of the settings are added. |
|
29 */ |
|
30 api.preview.bind( 'sync', function() { |
|
31 synced = true; |
|
32 } ); |
|
33 |
|
34 if ( api.selectiveRefresh ) { |
|
35 // Listen for changes to settings related to nav menus. |
|
36 api.each( function( setting ) { |
|
37 self.bindSettingListener( setting ); |
|
38 } ); |
|
39 api.bind( 'add', function( setting ) { |
|
40 |
|
41 /* |
|
42 * Handle case where an invalid nav menu item (one for which its associated object has been deleted) |
|
43 * is synced from the controls into the preview. Since invalid nav menu items are filtered out from |
|
44 * being exported to the frontend by the _is_valid_nav_menu_item filter in wp_get_nav_menu_items(), |
|
45 * the customizer controls will have a nav_menu_item setting where the preview will have none, and |
|
46 * this can trigger an infinite fallback refresh when the nav menu item lacks any valid items. |
|
47 */ |
|
48 if ( setting.get() && ! setting.get()._invalid ) { |
|
49 self.bindSettingListener( setting, { fire: synced } ); |
|
50 } |
|
51 } ); |
|
52 api.bind( 'remove', function( setting ) { |
|
53 self.unbindSettingListener( setting ); |
|
54 } ); |
|
55 |
|
56 /* |
|
57 * Ensure that wp_nav_menu() instances nested inside of other partials |
|
58 * will be recognized as being present on the page. |
|
59 */ |
|
60 api.selectiveRefresh.bind( 'render-partials-response', function( response ) { |
|
61 if ( response.nav_menu_instance_args ) { |
|
62 _.extend( self.data.navMenuInstanceArgs, response.nav_menu_instance_args ); |
|
63 } |
|
64 } ); |
|
65 } |
|
66 |
|
67 api.preview.bind( 'active', function() { |
|
68 self.highlightControls(); |
|
69 } ); |
|
70 }; |
|
71 |
|
72 if ( api.selectiveRefresh ) { |
|
73 |
|
74 /** |
|
75 * Partial representing an invocation of wp_nav_menu(). |
|
76 * |
|
77 * @memberOf wp.customize.navMenusPreview |
|
78 * @alias wp.customize.navMenusPreview.NavMenuInstancePartial |
|
79 * |
|
80 * @class |
|
81 * @augments wp.customize.selectiveRefresh.Partial |
|
82 * @since 4.5.0 |
|
83 */ |
|
84 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend(/** @lends wp.customize.navMenusPreview.NavMenuInstancePartial.prototype */{ |
|
85 |
|
86 /** |
|
87 * Constructor. |
|
88 * |
|
89 * @since 4.5.0 |
|
90 * @param {string} id - Partial ID. |
|
91 * @param {Object} options |
|
92 * @param {Object} options.params |
|
93 * @param {Object} options.params.navMenuArgs |
|
94 * @param {string} options.params.navMenuArgs.args_hmac |
|
95 * @param {string} [options.params.navMenuArgs.theme_location] |
|
96 * @param {number} [options.params.navMenuArgs.menu] |
|
97 * @param {object} [options.constructingContainerContext] |
|
98 */ |
|
99 initialize: function( id, options ) { |
|
100 var partial = this, matches, argsHmac; |
|
101 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ ); |
|
102 if ( ! matches ) { |
|
103 throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' ); |
|
104 } |
|
105 argsHmac = matches[1]; |
|
106 |
|
107 options = options || {}; |
|
108 options.params = _.extend( |
|
109 { |
|
110 selector: '[data-customize-partial-id="' + id + '"]', |
|
111 navMenuArgs: options.constructingContainerContext || {}, |
|
112 containerInclusive: true |
|
113 }, |
|
114 options.params || {} |
|
115 ); |
|
116 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options ); |
|
117 |
|
118 if ( ! _.isObject( partial.params.navMenuArgs ) ) { |
|
119 throw new Error( 'Missing navMenuArgs' ); |
|
120 } |
|
121 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) { |
|
122 throw new Error( 'args_hmac mismatch with id' ); |
|
123 } |
|
124 }, |
|
125 |
|
126 /** |
|
127 * Return whether the setting is related to this partial. |
|
128 * |
|
129 * @since 4.5.0 |
|
130 * @param {wp.customize.Value|string} setting - Object or ID. |
|
131 * @param {number|object|false|null} newValue - New value, or null if the setting was just removed. |
|
132 * @param {number|object|false|null} oldValue - Old value, or null if the setting was just added. |
|
133 * @returns {boolean} |
|
134 */ |
|
135 isRelatedSetting: function( setting, newValue, oldValue ) { |
|
136 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser; |
|
137 if ( _.isString( setting ) ) { |
|
138 setting = api( setting ); |
|
139 } |
|
140 |
|
141 /* |
|
142 * Prevent nav_menu_item changes only containing type_label differences triggering a refresh. |
|
143 * These settings in the preview do not include type_label property, and so if one of these |
|
144 * nav_menu_item settings is dirty, after a refresh the nav menu instance would do a selective |
|
145 * refresh immediately because the setting from the pane would have the type_label whereas |
|
146 * the setting in the preview would not, thus triggering a change event. The following |
|
147 * condition short-circuits this unnecessary selective refresh and also prevents an infinite |
|
148 * loop in the case where a nav_menu_instance partial had done a fallback refresh. |
|
149 * @todo Nav menu item settings should not include a type_label property to begin with. |
|
150 */ |
|
151 isNavMenuItemSetting = /^nav_menu_item\[/.test( setting.id ); |
|
152 if ( isNavMenuItemSetting && _.isObject( newValue ) && _.isObject( oldValue ) ) { |
|
153 _newValue = _.clone( newValue ); |
|
154 _oldValue = _.clone( oldValue ); |
|
155 delete _newValue.type_label; |
|
156 delete _oldValue.type_label; |
|
157 |
|
158 // Normalize URL scheme when parent frame is HTTPS to prevent selective refresh upon initial page load. |
|
159 if ( 'https' === api.preview.scheme.get() ) { |
|
160 urlParser = document.createElement( 'a' ); |
|
161 urlParser.href = _newValue.url; |
|
162 urlParser.protocol = 'https:'; |
|
163 _newValue.url = urlParser.href; |
|
164 urlParser.href = _oldValue.url; |
|
165 urlParser.protocol = 'https:'; |
|
166 _oldValue.url = urlParser.href; |
|
167 } |
|
168 |
|
169 // Prevent original_title differences from causing refreshes if title is present. |
|
170 if ( newValue.title ) { |
|
171 delete _oldValue.original_title; |
|
172 delete _newValue.original_title; |
|
173 } |
|
174 |
|
175 if ( _.isEqual( _oldValue, _newValue ) ) { |
|
176 return false; |
|
177 } |
|
178 } |
|
179 |
|
180 if ( partial.params.navMenuArgs.theme_location ) { |
|
181 if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) { |
|
182 return true; |
|
183 } |
|
184 navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ); |
|
185 } |
|
186 |
|
187 navMenuId = partial.params.navMenuArgs.menu; |
|
188 if ( ! navMenuId && navMenuLocationSetting ) { |
|
189 navMenuId = navMenuLocationSetting(); |
|
190 } |
|
191 |
|
192 if ( ! navMenuId ) { |
|
193 return false; |
|
194 } |
|
195 return ( |
|
196 ( 'nav_menu[' + navMenuId + ']' === setting.id ) || |
|
197 ( isNavMenuItemSetting && ( |
|
198 ( newValue && newValue.nav_menu_term_id === navMenuId ) || |
|
199 ( oldValue && oldValue.nav_menu_term_id === navMenuId ) |
|
200 ) ) |
|
201 ); |
|
202 }, |
|
203 |
|
204 /** |
|
205 * Make sure that partial fallback behavior is invoked if there is no associated menu. |
|
206 * |
|
207 * @since 4.5.0 |
|
208 * |
|
209 * @returns {Promise} |
|
210 */ |
|
211 refresh: function() { |
|
212 var partial = this, menuId, deferred = $.Deferred(); |
|
213 |
|
214 // Make sure the fallback behavior is invoked when the partial is no longer associated with a menu. |
|
215 if ( _.isNumber( partial.params.navMenuArgs.menu ) ) { |
|
216 menuId = partial.params.navMenuArgs.menu; |
|
217 } else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) { |
|
218 menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get(); |
|
219 } |
|
220 if ( ! menuId ) { |
|
221 partial.fallback(); |
|
222 deferred.reject(); |
|
223 return deferred.promise(); |
|
224 } |
|
225 |
|
226 return api.selectiveRefresh.Partial.prototype.refresh.call( partial ); |
|
227 }, |
|
228 |
|
229 /** |
|
230 * Render content. |
|
231 * |
|
232 * @inheritdoc |
|
233 * @param {wp.customize.selectiveRefresh.Placement} placement |
|
234 */ |
|
235 renderContent: function( placement ) { |
|
236 var partial = this, previousContainer = placement.container; |
|
237 |
|
238 // Do fallback behavior to refresh preview if menu is now empty. |
|
239 if ( '' === placement.addedContent ) { |
|
240 placement.partial.fallback(); |
|
241 } |
|
242 |
|
243 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) { |
|
244 |
|
245 // Trigger deprecated event. |
|
246 $( document ).trigger( 'customize-preview-menu-refreshed', [ { |
|
247 instanceNumber: null, // @deprecated |
|
248 wpNavArgs: placement.context, // @deprecated |
|
249 wpNavMenuArgs: placement.context, |
|
250 oldContainer: previousContainer, |
|
251 newContainer: placement.container |
|
252 } ] ); |
|
253 } |
|
254 } |
|
255 }); |
|
256 |
|
257 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial; |
|
258 |
|
259 /** |
|
260 * Request full refresh if there are nav menu instances that lack partials which also match the supplied args. |
|
261 * |
|
262 * @param {object} navMenuInstanceArgs |
|
263 */ |
|
264 self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) { |
|
265 var unplacedNavMenuInstances; |
|
266 unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) { |
|
267 return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' ); |
|
268 } ); |
|
269 if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) { |
|
270 api.selectiveRefresh.requestFullRefresh(); |
|
271 return true; |
|
272 } |
|
273 return false; |
|
274 }; |
|
275 |
|
276 /** |
|
277 * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting. |
|
278 * |
|
279 * @since 4.5.0 |
|
280 * |
|
281 * @param {wp.customize.Value} setting |
|
282 * @param {object} [options] |
|
283 * @param {boolean} options.fire Whether to invoke the callback after binding. |
|
284 * This is used when a dynamic setting is added. |
|
285 * @return {boolean} Whether the setting was bound. |
|
286 */ |
|
287 self.bindSettingListener = function( setting, options ) { |
|
288 var matches; |
|
289 options = options || {}; |
|
290 |
|
291 matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ ); |
|
292 if ( matches ) { |
|
293 setting._navMenuId = parseInt( matches[1], 10 ); |
|
294 setting.bind( this.onChangeNavMenuSetting ); |
|
295 if ( options.fire ) { |
|
296 this.onChangeNavMenuSetting.call( setting, setting(), false ); |
|
297 } |
|
298 return true; |
|
299 } |
|
300 |
|
301 matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ ); |
|
302 if ( matches ) { |
|
303 setting._navMenuItemId = parseInt( matches[1], 10 ); |
|
304 setting.bind( this.onChangeNavMenuItemSetting ); |
|
305 if ( options.fire ) { |
|
306 this.onChangeNavMenuItemSetting.call( setting, setting(), false ); |
|
307 } |
|
308 return true; |
|
309 } |
|
310 |
|
311 matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ ); |
|
312 if ( matches ) { |
|
313 setting._navMenuThemeLocation = matches[1]; |
|
314 setting.bind( this.onChangeNavMenuLocationsSetting ); |
|
315 if ( options.fire ) { |
|
316 this.onChangeNavMenuLocationsSetting.call( setting, setting(), false ); |
|
317 } |
|
318 return true; |
|
319 } |
|
320 |
|
321 return false; |
|
322 }; |
|
323 |
|
324 /** |
|
325 * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting. |
|
326 * |
|
327 * @since 4.5.0 |
|
328 * |
|
329 * @param {wp.customize.Value} setting |
|
330 */ |
|
331 self.unbindSettingListener = function( setting ) { |
|
332 setting.unbind( this.onChangeNavMenuSetting ); |
|
333 setting.unbind( this.onChangeNavMenuItemSetting ); |
|
334 setting.unbind( this.onChangeNavMenuLocationsSetting ); |
|
335 }; |
|
336 |
|
337 /** |
|
338 * Handle change for nav_menu[] setting for nav menu instances lacking partials. |
|
339 * |
|
340 * @since 4.5.0 |
|
341 * |
|
342 * @this {wp.customize.Value} |
|
343 */ |
|
344 self.onChangeNavMenuSetting = function() { |
|
345 var setting = this; |
|
346 |
|
347 self.handleUnplacedNavMenuInstances( { |
|
348 menu: setting._navMenuId |
|
349 } ); |
|
350 |
|
351 // Ensure all nav menu instances with a theme_location assigned to this menu are handled. |
|
352 api.each( function( otherSetting ) { |
|
353 if ( ! otherSetting._navMenuThemeLocation ) { |
|
354 return; |
|
355 } |
|
356 if ( setting._navMenuId === otherSetting() ) { |
|
357 self.handleUnplacedNavMenuInstances( { |
|
358 theme_location: otherSetting._navMenuThemeLocation |
|
359 } ); |
|
360 } |
|
361 } ); |
|
362 }; |
|
363 |
|
364 /** |
|
365 * Handle change for nav_menu_item[] setting for nav menu instances lacking partials. |
|
366 * |
|
367 * @since 4.5.0 |
|
368 * |
|
369 * @param {object} newItem New value for nav_menu_item[] setting. |
|
370 * @param {object} oldItem Old value for nav_menu_item[] setting. |
|
371 * @this {wp.customize.Value} |
|
372 */ |
|
373 self.onChangeNavMenuItemSetting = function( newItem, oldItem ) { |
|
374 var item = newItem || oldItem, navMenuSetting; |
|
375 navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' ); |
|
376 if ( navMenuSetting ) { |
|
377 self.onChangeNavMenuSetting.call( navMenuSetting ); |
|
378 } |
|
379 }; |
|
380 |
|
381 /** |
|
382 * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials. |
|
383 * |
|
384 * @since 4.5.0 |
|
385 * |
|
386 * @this {wp.customize.Value} |
|
387 */ |
|
388 self.onChangeNavMenuLocationsSetting = function() { |
|
389 var setting = this, hasNavMenuInstance; |
|
390 self.handleUnplacedNavMenuInstances( { |
|
391 theme_location: setting._navMenuThemeLocation |
|
392 } ); |
|
393 |
|
394 // If there are no wp_nav_menu() instances that refer to the theme location, do full refresh. |
|
395 hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), { |
|
396 theme_location: setting._navMenuThemeLocation |
|
397 } ); |
|
398 if ( ! hasNavMenuInstance ) { |
|
399 api.selectiveRefresh.requestFullRefresh(); |
|
400 } |
|
401 }; |
|
402 } |
|
403 |
|
404 /** |
|
405 * Connect nav menu items with their corresponding controls in the pane. |
|
406 * |
|
407 * Setup shift-click on nav menu items which are more granular than the nav menu partial itself. |
|
408 * Also this applies even if a nav menu is not partial-refreshable. |
|
409 * |
|
410 * @since 4.5.0 |
|
411 */ |
|
412 self.highlightControls = function() { |
|
413 var selector = '.menu-item'; |
|
414 |
|
415 // Skip adding highlights if not in the customizer preview iframe. |
|
416 if ( ! api.settings.channel ) { |
|
417 return; |
|
418 } |
|
419 |
|
420 // Focus on the menu item control when shift+clicking the menu item. |
|
421 $( document ).on( 'click', selector, function( e ) { |
|
422 var navMenuItemParts; |
|
423 if ( ! e.shiftKey ) { |
|
424 return; |
|
425 } |
|
426 |
|
427 navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(-?\d+)(?:\s|$)/ ); |
|
428 if ( navMenuItemParts ) { |
|
429 e.preventDefault(); |
|
430 e.stopPropagation(); // Make sure a sub-nav menu item will get focused instead of parent items. |
|
431 api.preview.send( 'focus-nav-menu-item-control', parseInt( navMenuItemParts[1], 10 ) ); |
|
432 } |
|
433 }); |
|
434 }; |
|
435 |
|
436 api.bind( 'preview-ready', function() { |
|
437 self.init(); |
|
438 } ); |
|
439 |
|
440 return self; |
|
441 |
|
442 }( jQuery, _, wp, wp.customize ) ); |