[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Tue Sep 9 08:20:04 2025 Cross-referenced by PHPXref