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