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