]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/customize-preview-nav-menus.js
WordPress 4.5.1-scripts
[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;
19
20                 if ( api.selectiveRefresh ) {
21                         // Listen for changes to settings related to nav menus.
22                         api.each( function( setting ) {
23                                 self.bindSettingListener( setting );
24                         } );
25                         api.bind( 'add', function( setting ) {
26                                 self.bindSettingListener( setting, { fire: true } );
27                         } );
28                         api.bind( 'remove', function( setting ) {
29                                 self.unbindSettingListener( setting );
30                         } );
31
32                         /*
33                          * Ensure that wp_nav_menu() instances nested inside of other partials
34                          * will be recognized as being present on the page.
35                          */
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 );
39                                 }
40                         } );
41                 }
42
43                 api.preview.bind( 'active', function() {
44                         self.highlightControls();
45                 } );
46         };
47
48         if ( api.selectiveRefresh ) {
49
50                 /**
51                  * Partial representing an invocation of wp_nav_menu().
52                  *
53                  * @class
54                  * @augments wp.customize.selectiveRefresh.Partial
55                  * @since 4.5.0
56                  */
57                 self.NavMenuInstancePartial = api.selectiveRefresh.Partial.extend({
58
59                         /**
60                          * Constructor.
61                          *
62                          * @since 4.5.0
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]
71                          */
72                         initialize: function( id, options ) {
73                                 var partial = this, matches, argsHmac;
74                                 matches = id.match( /^nav_menu_instance\[([0-9a-f]{32})]$/ );
75                                 if ( ! matches ) {
76                                         throw new Error( 'Illegal id for nav_menu_instance partial. The key corresponds with the args HMAC.' );
77                                 }
78                                 argsHmac = matches[1];
79
80                                 options = options || {};
81                                 options.params = _.extend(
82                                         {
83                                                 selector: '[data-customize-partial-id="' + id + '"]',
84                                                 navMenuArgs: options.constructingContainerContext || {},
85                                                 containerInclusive: true
86                                         },
87                                         options.params || {}
88                                 );
89                                 api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
90
91                                 if ( ! _.isObject( partial.params.navMenuArgs ) ) {
92                                         throw new Error( 'Missing navMenuArgs' );
93                                 }
94                                 if ( partial.params.navMenuArgs.args_hmac !== argsHmac ) {
95                                         throw new Error( 'args_hmac mismatch with id' );
96                                 }
97                         },
98
99                         /**
100                          * Return whether the setting is related to this partial.
101                          *
102                          * @since 4.5.0
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.
106                          * @returns {boolean}
107                          */
108                         isRelatedSetting: function( setting, newValue, oldValue ) {
109                                 var partial = this, navMenuLocationSetting, navMenuId, isNavMenuItemSetting;
110                                 if ( _.isString( setting ) ) {
111                                         setting = api( setting );
112                                 }
113
114                                 /*
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.
123                                  */
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 ) ) {
129                                                 return false;
130                                         }
131                                 }
132
133                                 if ( partial.params.navMenuArgs.theme_location ) {
134                                         if ( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' === setting.id ) {
135                                                 return true;
136                                         }
137                                         navMenuLocationSetting = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' );
138                                 }
139
140                                 navMenuId = partial.params.navMenuArgs.menu;
141                                 if ( ! navMenuId && navMenuLocationSetting ) {
142                                         navMenuId = navMenuLocationSetting();
143                                 }
144
145                                 if ( ! navMenuId ) {
146                                         return false;
147                                 }
148                                 return (
149                                         ( 'nav_menu[' + navMenuId + ']' === setting.id ) ||
150                                         ( isNavMenuItemSetting && (
151                                                 ( newValue && newValue.nav_menu_term_id === navMenuId ) ||
152                                                 ( oldValue && oldValue.nav_menu_term_id === navMenuId )
153                                         ) )
154                                 );
155                         },
156
157                         /**
158                          * Make sure that partial fallback behavior is invoked if there is no associated menu.
159                          *
160                          * @since 4.5.0
161                          *
162                          * @returns {Promise}
163                          */
164                         refresh: function() {
165                                 var partial = this, menuId, deferred = $.Deferred();
166
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();
172                                 }
173                                 if ( ! menuId ) {
174                                         partial.fallback();
175                                         deferred.reject();
176                                         return deferred.promise();
177                                 }
178
179                                 return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
180                         },
181
182                         /**
183                          * Render content.
184                          *
185                          * @inheritdoc
186                          * @param {wp.customize.selectiveRefresh.Placement} placement
187                          */
188                         renderContent: function( placement ) {
189                                 var partial = this, previousContainer = placement.container;
190
191                                 // Do fallback behavior to refresh preview if menu is now empty.
192                                 if ( '' === placement.addedContent ) {
193                                         placement.partial.fallback();
194                                 }
195
196                                 if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
197
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
205                                         } ] );
206                                 }
207                         }
208                 });
209
210                 api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
211
212                 /**
213                  * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
214                  *
215                  * @param {object} navMenuInstanceArgs
216                  */
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 + ']' );
221                         } );
222                         if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
223                                 api.selectiveRefresh.requestFullRefresh();
224                                 return true;
225                         }
226                         return false;
227                 };
228
229                 /**
230                  * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
231                  *
232                  * @since 4.5.0
233                  *
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.
239                  */
240                 self.bindSettingListener = function( setting, options ) {
241                         var matches;
242                         options = options || {};
243
244                         matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
245                         if ( matches ) {
246                                 setting._navMenuId = parseInt( matches[1], 10 );
247                                 setting.bind( this.onChangeNavMenuSetting );
248                                 if ( options.fire ) {
249                                         this.onChangeNavMenuSetting.call( setting, setting(), false );
250                                 }
251                                 return true;
252                         }
253
254                         matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
255                         if ( matches ) {
256                                 setting._navMenuItemId = parseInt( matches[1], 10 );
257                                 setting.bind( this.onChangeNavMenuItemSetting );
258                                 if ( options.fire ) {
259                                         this.onChangeNavMenuItemSetting.call( setting, setting(), false );
260                                 }
261                                 return true;
262                         }
263
264                         matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
265                         if ( matches ) {
266                                 setting._navMenuThemeLocation = matches[1];
267                                 setting.bind( this.onChangeNavMenuLocationsSetting );
268                                 if ( options.fire ) {
269                                         this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
270                                 }
271                                 return true;
272                         }
273
274                         return false;
275                 };
276
277                 /**
278                  * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
279                  *
280                  * @since 4.5.0
281                  *
282                  * @param {wp.customize.Value} setting
283                  */
284                 self.unbindSettingListener = function( setting ) {
285                         setting.unbind( this.onChangeNavMenuSetting );
286                         setting.unbind( this.onChangeNavMenuItemSetting );
287                         setting.unbind( this.onChangeNavMenuLocationsSetting );
288                 };
289
290                 /**
291                  * Handle change for nav_menu[] setting for nav menu instances lacking partials.
292                  *
293                  * @since 4.5.0
294                  *
295                  * @this {wp.customize.Value}
296                  */
297                 self.onChangeNavMenuSetting = function() {
298                         var setting = this;
299
300                         self.handleUnplacedNavMenuInstances( {
301                                 menu: setting._navMenuId
302                         } );
303
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 ) {
307                                         return;
308                                 }
309                                 if ( setting._navMenuId === otherSetting() ) {
310                                         self.handleUnplacedNavMenuInstances( {
311                                                 theme_location: otherSetting._navMenuThemeLocation
312                                         } );
313                                 }
314                         } );
315                 };
316
317                 /**
318                  * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
319                  *
320                  * @since 4.5.0
321                  *
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}
325                  */
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 );
331                         }
332                 };
333
334                 /**
335                  * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
336                  *
337                  * @since 4.5.0
338                  *
339                  * @this {wp.customize.Value}
340                  */
341                 self.onChangeNavMenuLocationsSetting = function() {
342                         var setting = this, hasNavMenuInstance;
343                         self.handleUnplacedNavMenuInstances( {
344                                 theme_location: setting._navMenuThemeLocation
345                         } );
346
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
350                         } );
351                         if ( ! hasNavMenuInstance ) {
352                                 api.selectiveRefresh.requestFullRefresh();
353                         }
354                 };
355         }
356
357         /**
358          * Connect nav menu items with their corresponding controls in the pane.
359          *
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.
362          *
363          * @since 4.5.0
364          */
365         self.highlightControls = function() {
366                 var selector = '.menu-item';
367
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 ) {
372                                 return;
373                         }
374
375                         navMenuItemParts = $( this ).attr( 'class' ).match( /(?:^|\s)menu-item-(\d+)(?:\s|$)/ );
376                         if ( navMenuItemParts ) {
377                                 e.preventDefault();
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 ) );
380                         }
381                 });
382         };
383
384         api.bind( 'preview-ready', function() {
385                 self.init();
386         } );
387
388         return self;
389
390 }( jQuery, _, wp, wp.customize ) );