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