[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Thu Oct 24 08:20:01 2024 Cross-referenced by PHPXref