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