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