home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-admin / js / customize-nav-menus.js < prev    next >
Encoding:
JavaScript  |  2017-11-10  |  104.9 KB  |  3,450 lines

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