angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-nav-menus.js (108450B)


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