[ Index ]

PHP Cross Reference of WordPress

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


Generated: Tue Sep 17 01:00:03 2019 Cross-referenced by PHPXref 0.7.1