[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> customize-nav-menus.js (source)

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


Generated : Sat Sep 5 08:20:28 2026 Cross-referenced by PHPXref