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


Generated : Fri Oct 10 08:20:03 2025 Cross-referenced by PHPXref