[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Fri Sep 25 08:20:31 2026 Cross-referenced by PHPXref