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