27c525107bcf31044f4cf411bba5d6992082b179
[autoinstalls/wordpress.git] / wp-includes / js / customize-preview-nav-menus.js
1 /* global _wpCustomizePreviewNavMenusExports */
2 wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
3         'use strict';
4
5         var self = {
6                 data: {
7                         navMenuInstanceArgs: {}
8                 }
9         };
10         if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
11                 _.extend( self.data, _wpCustomizePreviewNavMenusExports );
12         }
13
14         /**
15          * Initialize nav menus preview.
16          */
17         self.init = function() {
18                 var self = this, synced = false;
19
20                 /*
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.
27                  */
28                 api.preview.bind( 'sync', function() {
29                         synced = true;
30                 } );
31
32                 if ( api.selectiveRefresh ) {
33                         // Listen for changes to settings related to nav menus.
34                         api.each( function( setting ) {
35                                 self.bindSettingListener( setting );
36                         } );
37                         api.bind( 'add', function( setting ) {
38
39                                 /*
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.
45                                  */
46                                 if ( setting.get() && ! setting.get()._invalid ) {
47                                         self.bindSettingListener( setting, { fire: synced } );
48                                 }
49                         } );
50                         api.bind( 'remove', function( setting ) {
51                                 self.unbindSettingListener( setting );
52                         } );
53
54                         /*
55                          * Ensure that wp_nav_menu() instances nested inside of other partials
56                          * will be recognized as being present on the page.
57                          */
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 );
61                                 }
62                         } );
63                 }
64
65                 api.preview.bind( 'active', function() {
66                         self.highlightControls();
67                 } );
68         };
69
70         if ( api.selectiveRefresh ) {
71
72                 /**
73                  * Partial representing an invocation of wp_nav_menu().
74                  *
75                  * @class
76                  * @augments wp.customize.selectiveRefresh.Partial
77                  * @since 4.5.0
78                  */
79                 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
80
81                         /**
82                          * Constructor.
83                          *
84                          * @since 4.5.0
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]
93                          */
94                         initialize: function( id, options ) {
95                                 var partial = this, matches, argsHmac;
96                                 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
97                                 if ( ! matches ) {
98                                         throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
99                                 }
100                                 argsHmac = matches[1];
101
102                                 options = options || {};
103                                 options.params = _.extend(
104                                         {
105                                                 selector: '[data-customize-partial-id="' + id + '"]',
106                                                 navMenuArgs: options.constructingContainerContext || {},
107                                                 containerInclusive: true
108                                         },
109                                         options.params || {}
110                                 );
111                                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
112
113                                 if ( ! _.isObject( partial.params.navMenuArgs ) ) {
114                                         throw new Error( 'Missing navMenuArgs' );
115                                 }
116                                 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
117                                         throw new Error( 'args_hmac mismatch with id' );
118                                 }
119                         },
120
121                         /**
122                          * Return whether the setting is related to this partial.
123                          *
124                          * @since 4.5.0
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.
128                          * @returns {boolean}
129                          */
130                         isRelatedSetting: function( setting, newValue, oldValue ) {
131                                 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting, _newValue, _oldValue, urlParser;
132                                 if ( _.isString( setting ) ) {
133                                         setting = api( setting );
134                                 }
135
136                                 /*
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.
145                                  */
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;
152
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;
162                                         }
163
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;
168                                         }
169
170                                         if ( _.isEqual( _oldValue, _newValue ) ) {
171                                                 return false;
172                                         }
173                                 }
174
175                                 if ( partial.params.navMenuArgs.theme_location ) {
176                                         if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
177                                                 return true;
178                                         }
179                                         navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
180                                 }
181
182                                 navMenuId = partial.params.navMenuArgs.menu;
183                                 if ( ! navMenuId && navMenuLocationSetting ) {
184                                         navMenuId = navMenuLocationSetting();
185                                 }
186
187                                 if ( ! navMenuId ) {
188                                         return false;
189                                 }
190                                 return (
191                                         ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
192                                         ( isNavMenuItemSetting && (
193                                                 ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
194                                                 ( oldValue && oldValue.nav_menu_term_id === navMenuId )
195                                         ) )
196                                 );
197                         },
198
199                         /**
200                          * Make sure that partial fallback behavior is invoked if there is no associated menu.
201                          *
202                          * @since 4.5.0
203                          *
204                          * @returns {Promise}
205                          */
206                         refresh: function() {
207                                 var partial = this, menuId, deferred = $.Deferred();
208
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();
214                                 }
215                                 if ( ! menuId ) {
216                                         partial.fallback();
217                                         deferred.reject();
218                                         return deferred.promise();
219                                 }
220
221                                 return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
222                         },
223
224                         /**
225                          * Render content.
226                          *
227                          * @inheritdoc
228                          * @param {wp.customize.selectiveRefresh.Placement} placement
229                          */
230                         renderContent: function( placement ) {
231                                 var partial = this, previousContainer = placement.container;
232
233                                 // Do fallback behavior to refresh preview if menu is now empty.
234                                 if ( '' === placement.addedContent ) {
235                                         placement.partial.fallback();
236                                 }
237
238                                 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
239
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
247                                         } ] );
248                                 }
249                         }
250                 });
251
252                 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
253
254                 /**
255                  * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
256                  *
257                  * @param {object} navMenuInstanceArgs
258                  */
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 + ']' );
263                         } );
264                         if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
265                                 api.selectiveRefresh.requestFullRefresh();
266                                 return true;
267                         }
268                         return false;
269                 };
270
271                 /**
272                  * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
273                  *
274                  * @since 4.5.0
275                  *
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.
281                  */
282                 self.bindSettingListener = function( setting, options ) {
283                         var matches;
284                         options = options || {};
285
286                         matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
287                         if ( matches ) {
288                                 setting._navMenuId = parseInt( matches[1], 10 );
289                                 setting.bind( this.onChangeNavMenuSetting );
290                                 if ( options.fire ) {
291                                         this.onChangeNavMenuSetting.call( setting, setting(), false );
292                                 }
293                                 return true;
294                         }
295
296                         matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
297                         if ( matches ) {
298                                 setting._navMenuItemId = parseInt( matches[1], 10 );
299                                 setting.bind( this.onChangeNavMenuItemSetting );
300                                 if ( options.fire ) {
301                                         this.onChangeNavMenuItemSetting.call( setting, setting(), false );
302                                 }
303                                 return true;
304                         }
305
306                         matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
307                         if ( matches ) {
308                                 setting._navMenuThemeLocation = matches[1];
309                                 setting.bind( this.onChangeNavMenuLocationsSetting );
310                                 if ( options.fire ) {
311                                         this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
312                                 }
313                                 return true;
314                         }
315
316                         return false;
317                 };
318
319                 /**
320                  * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
321                  *
322                  * @since 4.5.0
323                  *
324                  * @param {wp.customize.Value} setting
325                  */
326                 self.unbindSettingListener = function( setting ) {
327                         setting.unbind( this.onChangeNavMenuSetting );
328                         setting.unbind( this.onChangeNavMenuItemSetting );
329                         setting.unbind( this.onChangeNavMenuLocationsSetting );
330                 };
331
332                 /**
333                  * Handle change for nav_menu[] setting for nav menu instances lacking partials.
334                  *
335                  * @since 4.5.0
336                  *
337                  * @this {wp.customize.Value}
338                  */
339                 self.onChangeNavMenuSetting = function() {
340                         var setting = this;
341
342                         self.handleUnplacedNavMenuInstances( {
343                                 menu: setting._navMenuId
344                         } );
345
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 ) {
349                                         return;
350                                 }
351                                 if ( setting._navMenuId === otherSetting() ) {
352                                         self.handleUnplacedNavMenuInstances( {
353                                                 theme_location: otherSetting._navMenuThemeLocation
354                                         } );
355                                 }
356                         } );
357                 };
358
359                 /**
360                  * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
361                  *
362                  * @since 4.5.0
363                  *
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}
367                  */
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 );
373                         }
374                 };
375
376                 /**
377                  * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
378                  *
379                  * @since 4.5.0
380                  *
381                  * @this {wp.customize.Value}
382                  */
383                 self.onChangeNavMenuLocationsSetting = function() {
384                         var setting = this, hasNavMenuInstance;
385                         self.handleUnplacedNavMenuInstances( {
386                                 theme_location: setting._navMenuThemeLocation
387                         } );
388
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
392                         } );
393                         if ( ! hasNavMenuInstance ) {
394                                 api.selectiveRefresh.requestFullRefresh();
395                         }
396                 };
397         }
398
399         /**
400          * Connect nav menu items with their corresponding controls in the pane.
401          *
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.
404          *
405          * @since 4.5.0
406          */
407         self.highlightControls = function() {
408                 var selector = '.menu-item';
409
410                 // Skip adding highlights if not in the customizer preview iframe.
411                 if ( ! api.settings.channel ) {
412                         return;
413                 }
414
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 ) {
419                                 return;
420                         }
421
422                         navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
423                         if ( navMenuItemParts ) {
424                                 e.preventDefault();
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 ) );
427                         }
428                 });
429         };
430
431         api.bind( 'preview-ready', function() {
432                 self.init();
433         } );
434
435         return self;
436
437 }( jQuery, _, wp, wp.customize ) );