aboutsummaryrefslogtreecommitdiff
path: root/srcs/wordpress/wp-admin/js/customize-nav-menus.js
diff options
context:
space:
mode:
Diffstat (limited to 'srcs/wordpress/wp-admin/js/customize-nav-menus.js')
-rw-r--r--srcs/wordpress/wp-admin/js/customize-nav-menus.js3463
1 files changed, 3463 insertions, 0 deletions
diff --git a/srcs/wordpress/wp-admin/js/customize-nav-menus.js b/srcs/wordpress/wp-admin/js/customize-nav-menus.js
new file mode 100644
index 0000000..e92104a
--- /dev/null
+++ b/srcs/wordpress/wp-admin/js/customize-nav-menus.js
@@ -0,0 +1,3463 @@
+/**
+ * @output wp-admin/js/customize-nav-menus.js
+ */
+
+/* global _wpCustomizeNavMenusSettings, wpNavMenu, console */
+( function( api, wp, $ ) {
+ 'use strict';
+
+ /**
+ * Set up wpNavMenu for drag and drop.
+ */
+ wpNavMenu.originalInit = wpNavMenu.init;
+ wpNavMenu.options.menuItemDepthPerLevel = 20;
+ wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
+ wpNavMenu.options.targetTolerance = 10;
+ wpNavMenu.init = function() {
+ this.jQueryExtensions();
+ };
+
+ /**
+ * @namespace wp.customize.Menus
+ */
+ api.Menus = api.Menus || {};
+
+ // Link settings.
+ api.Menus.data = {
+ itemTypes: [],
+ l10n: {},
+ settingTransport: 'refresh',
+ phpIntMax: 0,
+ defaultSettingValues: {
+ nav_menu: {},
+ nav_menu_item: {}
+ },
+ locationSlugMappedToName: {}
+ };
+ if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
+ $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
+ }
+
+ /**
+ * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
+ * serve as placeholders until Save & Publish happens.
+ *
+ * @alias wp.customize.Menus.generatePlaceholderAutoIncrementId
+ *
+ * @return {number}
+ */
+ api.Menus.generatePlaceholderAutoIncrementId = function() {
+ return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
+ };
+
+ /**
+ * wp.customize.Menus.AvailableItemModel
+ *
+ * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
+ *
+ * @class wp.customize.Menus.AvailableItemModel
+ * @augments Backbone.Model
+ */
+ api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
+ {
+ id: null // This is only used by Backbone.
+ },
+ api.Menus.data.defaultSettingValues.nav_menu_item
+ ) );
+
+ /**
+ * wp.customize.Menus.AvailableItemCollection
+ *
+ * Collection for available menu item models.
+ *
+ * @class wp.customize.Menus.AvailableItemCollection
+ * @augments Backbone.Collection
+ */
+ api.Menus.AvailableItemCollection = Backbone.Collection.extend(/** @lends wp.customize.Menus.AvailableItemCollection.prototype */{
+ model: api.Menus.AvailableItemModel,
+
+ sort_key: 'order',
+
+ comparator: function( item ) {
+ return -item.get( this.sort_key );
+ },
+
+ sortByField: function( fieldName ) {
+ this.sort_key = fieldName;
+ this.sort();
+ }
+ });
+ api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
+
+ /**
+ * Insert a new `auto-draft` post.
+ *
+ * @since 4.7.0
+ * @alias wp.customize.Menus.insertAutoDraftPost
+ *
+ * @param {object} params - Parameters for the draft post to create.
+ * @param {string} params.post_type - Post type to add.
+ * @param {string} params.post_title - Post title to use.
+ * @return {jQuery.promise} Promise resolved with the added post.
+ */
+ api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
+ var request, deferred = $.Deferred();
+
+ request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
+ 'customize-menus-nonce': api.settings.nonce['customize-menus'],
+ 'wp_customize': 'on',
+ 'customize_changeset_uuid': api.settings.changeset.uuid,
+ 'params': params
+ } );
+
+ request.done( function( response ) {
+ if ( response.post_id ) {
+ api( 'nav_menus_created_posts' ).set(
+ api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] )
+ );
+
+ if ( 'page' === params.post_type ) {
+
+ // Activate static front page controls as this could be the first page created.
+ if ( api.section.has( 'static_front_page' ) ) {
+ api.section( 'static_front_page' ).activate();
+ }
+
+ // Add new page to dropdown-pages controls.
+ api.control.each( function( control ) {
+ var select;
+ if ( 'dropdown-pages' === control.params.type ) {
+ select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' );
+ select.append( new Option( params.post_title, response.post_id ) );
+ }
+ } );
+ }
+ deferred.resolve( response );
+ }
+ } );
+
+ request.fail( function( response ) {
+ var error = response || '';
+
+ if ( 'undefined' !== typeof response.message ) {
+ error = response.message;
+ }
+
+ console.error( error );
+ deferred.rejectWith( error );
+ } );
+
+ return deferred.promise();
+ };
+
+ api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Menus.AvailableMenuItemsPanelView.prototype */{
+
+ el: '#available-menu-items',
+
+ events: {
+ 'input #menu-items-search': 'debounceSearch',
+ 'focus .menu-item-tpl': 'focus',
+ 'click .menu-item-tpl': '_submit',
+ 'click #custom-menu-item-submit': '_submitLink',
+ 'keypress #custom-menu-item-name': '_submitLink',
+ 'click .new-content-item .add-content': '_submitNew',
+ 'keypress .create-item-input': '_submitNew',
+ 'keydown': 'keyboardAccessible'
+ },
+
+ // Cache current selected menu item.
+ selected: null,
+
+ // Cache menu control that opened the panel.
+ currentMenuControl: null,
+ debounceSearch: null,
+ $search: null,
+ $clearResults: null,
+ searchTerm: '',
+ rendered: false,
+ pages: {},
+ sectionContent: '',
+ loading: false,
+ addingNew: false,
+
+ /**
+ * wp.customize.Menus.AvailableMenuItemsPanelView
+ *
+ * View class for the available menu items panel.
+ *
+ * @constructs wp.customize.Menus.AvailableMenuItemsPanelView
+ * @augments wp.Backbone.View
+ */
+ initialize: function() {
+ var self = this;
+
+ if ( ! api.panel.has( 'nav_menus' ) ) {
+ return;
+ }
+
+ this.$search = $( '#menu-items-search' );
+ this.$clearResults = this.$el.find( '.clear-results' );
+ this.sectionContent = this.$el.find( '.available-menu-items-list' );
+
+ this.debounceSearch = _.debounce( self.search, 500 );
+
+ _.bindAll( this, 'close' );
+
+ // If the available menu items panel is open and the customize controls are
+ // interacted with (other than an item being deleted), then close the
+ // available menu items panel. Also close on back button click.
+ $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
+ var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
+ isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
+ if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
+ self.close();
+ }
+ } );
+
+ // Clear the search results and trigger a `keyup` event to fire a new search.
+ this.$clearResults.on( 'click', function() {
+ self.$search.val( '' ).focus().trigger( 'keyup' );
+ } );
+
+ this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
+ $( this ).removeClass( 'invalid' );
+ });
+
+ // Load available items if it looks like we'll need them.
+ api.panel( 'nav_menus' ).container.bind( 'expanded', function() {
+ if ( ! self.rendered ) {
+ self.initList();
+ self.rendered = true;
+ }
+ });
+
+ // Load more items.
+ this.sectionContent.scroll( function() {
+ var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
+ visibleHeight = self.$el.find( '.accordion-section.open' ).height();
+
+ if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
+ var type = $( this ).data( 'type' ),
+ object = $( this ).data( 'object' );
+
+ if ( 'search' === type ) {
+ if ( self.searchTerm ) {
+ self.doSearch( self.pages.search );
+ }
+ } else {
+ self.loadItems( [
+ { type: type, object: object }
+ ] );
+ }
+ }
+ });
+
+ // Close the panel if the URL in the preview changes
+ api.previewer.bind( 'url', this.close );
+
+ self.delegateEvents();
+ },
+
+ // Search input change handler.
+ search: function( event ) {
+ var $searchSection = $( '#available-menu-items-search' ),
+ $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
+
+ if ( ! event ) {
+ return;
+ }
+
+ if ( this.searchTerm === event.target.value ) {
+ return;
+ }
+
+ if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
+ $otherSections.fadeOut( 100 );
+ $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
+ $searchSection.addClass( 'open' );
+ this.$clearResults.addClass( 'is-visible' );
+ } else if ( '' === event.target.value ) {
+ $searchSection.removeClass( 'open' );
+ $otherSections.show();
+ this.$clearResults.removeClass( 'is-visible' );
+ }
+
+ this.searchTerm = event.target.value;
+ this.pages.search = 1;
+ this.doSearch( 1 );
+ },
+
+ // Get search results.
+ doSearch: function( page ) {
+ var self = this, params,
+ $section = $( '#available-menu-items-search' ),
+ $content = $section.find( '.accordion-section-content' ),
+ itemTemplate = wp.template( 'available-menu-item' );
+
+ if ( self.currentRequest ) {
+ self.currentRequest.abort();
+ }
+
+ if ( page < 0 ) {
+ return;
+ } else if ( page > 1 ) {
+ $section.addClass( 'loading-more' );
+ $content.attr( 'aria-busy', 'true' );
+ wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
+ } else if ( '' === self.searchTerm ) {
+ $content.html( '' );
+ wp.a11y.speak( '' );
+ return;
+ }
+
+ $section.addClass( 'loading' );
+ self.loading = true;
+
+ params = api.previewer.query( { excludeCustomizedSaved: true } );
+ _.extend( params, {
+ 'customize-menus-nonce': api.settings.nonce['customize-menus'],
+ 'wp_customize': 'on',
+ 'search': self.searchTerm,
+ 'page': page
+ } );
+
+ self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
+
+ self.currentRequest.done(function( data ) {
+ var items;
+ if ( 1 === page ) {
+ // Clear previous results as it's a new search.
+ $content.empty();
+ }
+ $section.removeClass( 'loading loading-more' );
+ $content.attr( 'aria-busy', 'false' );
+ $section.addClass( 'open' );
+ self.loading = false;
+ items = new api.Menus.AvailableItemCollection( data.items );
+ self.collection.add( items.models );
+ items.each( function( menuItem ) {
+ $content.append( itemTemplate( menuItem.attributes ) );
+ } );
+ if ( 20 > items.length ) {
+ self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
+ } else {
+ self.pages.search = self.pages.search + 1;
+ }
+ if ( items && page > 1 ) {
+ wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
+ } else if ( items && page === 1 ) {
+ wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
+ }
+ });
+
+ self.currentRequest.fail(function( data ) {
+ // data.message may be undefined, for example when typing slow and the request is aborted.
+ if ( data.message ) {
+ $content.empty().append( $( '<li class="nothing-found"></li>' ).text( data.message ) );
+ wp.a11y.speak( data.message );
+ }
+ self.pages.search = -1;
+ });
+
+ self.currentRequest.always(function() {
+ $section.removeClass( 'loading loading-more' );
+ $content.attr( 'aria-busy', 'false' );
+ self.loading = false;
+ self.currentRequest = null;
+ });
+ },
+
+ // Render the individual items.
+ initList: function() {
+ var self = this;
+
+ // Render the template for each item by type.
+ _.each( api.Menus.data.itemTypes, function( itemType ) {
+ self.pages[ itemType.type + ':' + itemType.object ] = 0;
+ } );
+ self.loadItems( api.Menus.data.itemTypes );
+ },
+
+ /**
+ * Load available nav menu items.
+ *
+ * @since 4.3.0
+ * @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
+ * @access private
+ *
+ * @param {Array.<object>} itemTypes List of objects containing type and key.
+ * @param {string} deprecated Formerly the object parameter.
+ * @returns {void}
+ */
+ loadItems: function( itemTypes, deprecated ) {
+ var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {};
+ itemTemplate = wp.template( 'available-menu-item' );
+
+ if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
+ _itemTypes = [ { type: itemTypes, object: deprecated } ];
+ } else {
+ _itemTypes = itemTypes;
+ }
+
+ _.each( _itemTypes, function( itemType ) {
+ var container, name = itemType.type + ':' + itemType.object;
+ if ( -1 === self.pages[ name ] ) {
+ return; // Skip types for which there are no more results.
+ }
+ container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
+ container.find( '.accordion-section-title' ).addClass( 'loading' );
+ availableMenuItemContainers[ name ] = container;
+
+ requestItemTypes.push( {
+ object: itemType.object,
+ type: itemType.type,
+ page: self.pages[ name ]
+ } );
+ } );
+
+ if ( 0 === requestItemTypes.length ) {
+ return;
+ }
+
+ self.loading = true;
+
+ params = api.previewer.query( { excludeCustomizedSaved: true } );
+ _.extend( params, {
+ 'customize-menus-nonce': api.settings.nonce['customize-menus'],
+ 'wp_customize': 'on',
+ 'item_types': requestItemTypes
+ } );
+
+ request = wp.ajax.post( 'load-available-menu-items-customizer', params );
+
+ request.done(function( data ) {
+ var typeInner;
+ _.each( data.items, function( typeItems, name ) {
+ if ( 0 === typeItems.length ) {
+ if ( 0 === self.pages[ name ] ) {
+ availableMenuItemContainers[ name ].find( '.accordion-section-title' )
+ .addClass( 'cannot-expand' )
+ .removeClass( 'loading' )
+ .find( '.accordion-section-title > button' )
+ .prop( 'tabIndex', -1 );
+ }
+ self.pages[ name ] = -1;
+ return;
+ } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
+ availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).click();
+ }
+ typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
+ self.collection.add( typeItems.models );
+ typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
+ typeItems.each( function( menuItem ) {
+ typeInner.append( itemTemplate( menuItem.attributes ) );
+ } );
+ self.pages[ name ] += 1;
+ });
+ });
+ request.fail(function( data ) {
+ if ( typeof console !== 'undefined' && console.error ) {
+ console.error( data );
+ }
+ });
+ request.always(function() {
+ _.each( availableMenuItemContainers, function( container ) {
+ container.find( '.accordion-section-title' ).removeClass( 'loading' );
+ } );
+ self.loading = false;
+ });
+ },
+
+ // Adjust the height of each section of items to fit the screen.
+ itemSectionHeight: function() {
+ var sections, lists, totalHeight, accordionHeight, diff;
+ totalHeight = window.innerHeight;
+ sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
+ lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
+ accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
+ diff = totalHeight - accordionHeight;
+ if ( 120 < diff && 290 > diff ) {
+ sections.css( 'max-height', diff );
+ lists.css( 'max-height', ( diff - 60 ) );
+ }
+ },
+
+ // Highlights a menu item.
+ select: function( menuitemTpl ) {
+ this.selected = $( menuitemTpl );
+ this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
+ this.selected.addClass( 'selected' );
+ },
+
+ // Highlights a menu item on focus.
+ focus: function( event ) {
+ this.select( $( event.currentTarget ) );
+ },
+
+ // Submit handler for keypress and click on menu item.
+ _submit: function( event ) {
+ // Only proceed with keypress if it is Enter or Spacebar
+ if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
+ return;
+ }
+
+ this.submit( $( event.currentTarget ) );
+ },
+
+ // Adds a selected menu item to the menu.
+ submit: function( menuitemTpl ) {
+ var menuitemId, menu_item;
+
+ if ( ! menuitemTpl ) {
+ menuitemTpl = this.selected;
+ }
+
+ if ( ! menuitemTpl || ! this.currentMenuControl ) {
+ return;
+ }
+
+ this.select( menuitemTpl );
+
+ menuitemId = $( this.selected ).data( 'menu-item-id' );
+ menu_item = this.collection.findWhere( { id: menuitemId } );
+ if ( ! menu_item ) {
+ return;
+ }
+
+ this.currentMenuControl.addItemToMenu( menu_item.attributes );
+
+ $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
+ },
+
+ // Submit handler for keypress and click on custom menu item.
+ _submitLink: function( event ) {
+ // Only proceed with keypress if it is Enter.
+ if ( 'keypress' === event.type && 13 !== event.which ) {
+ return;
+ }
+
+ this.submitLink();
+ },
+
+ // Adds the custom menu item to the menu.
+ submitLink: function() {
+ var menuItem,
+ itemName = $( '#custom-menu-item-name' ),
+ itemUrl = $( '#custom-menu-item-url' ),
+ url = itemUrl.val().trim(),
+ urlRegex;
+
+ if ( ! this.currentMenuControl ) {
+ return;
+ }
+
+ /*
+ * Allow URLs including:
+ * - http://example.com/
+ * - //example.com
+ * - /directory/
+ * - ?query-param
+ * - #target
+ * - mailto:foo@example.com
+ *
+ * Any further validation will be handled on the server when the setting is attempted to be saved,
+ * so this pattern does not need to be complete.
+ */
+ urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
+
+ if ( '' === itemName.val() ) {
+ itemName.addClass( 'invalid' );
+ return;
+ } else if ( ! urlRegex.test( url ) ) {
+ itemUrl.addClass( 'invalid' );
+ return;
+ }
+
+ menuItem = {
+ 'title': itemName.val(),
+ 'url': url,
+ 'type': 'custom',
+ 'type_label': api.Menus.data.l10n.custom_label,
+ 'object': 'custom'
+ };
+
+ this.currentMenuControl.addItemToMenu( menuItem );
+
+ // Reset the custom link form.
+ itemUrl.val( '' ).attr( 'placeholder', 'https://' );
+ itemName.val( '' );
+ },
+
+ /**
+ * Submit handler for keypress (enter) on field and click on button.
+ *
+ * @since 4.7.0
+ * @private
+ *
+ * @param {jQuery.Event} event Event.
+ * @returns {void}
+ */
+ _submitNew: function( event ) {
+ var container;
+
+ // Only proceed with keypress if it is Enter.
+ if ( 'keypress' === event.type && 13 !== event.which ) {
+ return;
+ }
+
+ if ( this.addingNew ) {
+ return;
+ }
+
+ container = $( event.target ).closest( '.accordion-section' );
+
+ this.submitNew( container );
+ },
+
+ /**
+ * Creates a new object and adds an associated menu item to the menu.
+ *
+ * @since 4.7.0
+ * @private
+ *
+ * @param {jQuery} container
+ * @returns {void}
+ */
+ submitNew: function( container ) {
+ var panel = this,
+ itemName = container.find( '.create-item-input' ),
+ title = itemName.val(),
+ dataContainer = container.find( '.available-menu-items-list' ),
+ itemType = dataContainer.data( 'type' ),
+ itemObject = dataContainer.data( 'object' ),
+ itemTypeLabel = dataContainer.data( 'type_label' ),
+ promise;
+
+ if ( ! this.currentMenuControl ) {
+ return;
+ }
+
+ // Only posts are supported currently.
+ if ( 'post_type' !== itemType ) {
+ return;
+ }
+
+ if ( '' === $.trim( itemName.val() ) ) {
+ itemName.addClass( 'invalid' );
+ itemName.focus();
+ return;
+ } else {
+ itemName.removeClass( 'invalid' );
+ container.find( '.accordion-section-title' ).addClass( 'loading' );
+ }
+
+ panel.addingNew = true;
+ itemName.attr( 'disabled', 'disabled' );
+ promise = api.Menus.insertAutoDraftPost( {
+ post_title: title,
+ post_type: itemObject
+ } );
+ promise.done( function( data ) {
+ var availableItem, $content, itemElement;
+ availableItem = new api.Menus.AvailableItemModel( {
+ 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
+ 'title': itemName.val(),
+ 'type': itemType,
+ 'type_label': itemTypeLabel,
+ 'object': itemObject,
+ 'object_id': data.post_id,
+ 'url': data.url
+ } );
+
+ // Add new item to menu.
+ panel.currentMenuControl.addItemToMenu( availableItem.attributes );
+
+ // Add the new item to the list of available items.
+ api.Menus.availableMenuItemsPanel.collection.add( availableItem );
+ $content = container.find( '.available-menu-items-list' );
+ itemElement = $( wp.template( 'available-menu-item' )( availableItem.attributes ) );
+ itemElement.find( '.menu-item-handle:first' ).addClass( 'item-added' );
+ $content.prepend( itemElement );
+ $content.scrollTop();
+
+ // Reset the create content form.
+ itemName.val( '' ).removeAttr( 'disabled' );
+ panel.addingNew = false;
+ container.find( '.accordion-section-title' ).removeClass( 'loading' );
+ } );
+ },
+
+ // Opens the panel.
+ open: function( menuControl ) {
+ var panel = this, close;
+
+ this.currentMenuControl = menuControl;
+
+ this.itemSectionHeight();
+
+ if ( api.section.has( 'publish_settings' ) ) {
+ api.section( 'publish_settings' ).collapse();
+ }
+
+ $( 'body' ).addClass( 'adding-menu-items' );
+
+ close = function() {
+ panel.close();
+ $( this ).off( 'click', close );
+ };
+ $( '#customize-preview' ).on( 'click', close );
+
+ // Collapse all controls.
+ _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
+ control.collapseForm();
+ } );
+
+ this.$el.find( '.selected' ).removeClass( 'selected' );
+
+ this.$search.focus();
+ },
+
+ // Closes the panel
+ close: function( options ) {
+ options = options || {};
+
+ if ( options.returnFocus && this.currentMenuControl ) {
+ this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
+ }
+
+ this.currentMenuControl = null;
+ this.selected = null;
+
+ $( 'body' ).removeClass( 'adding-menu-items' );
+ $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
+
+ this.$search.val( '' ).trigger( 'keyup' );
+ },
+
+ // Add a few keyboard enhancements to the panel.
+ keyboardAccessible: function( event ) {
+ var isEnter = ( 13 === event.which ),
+ isEsc = ( 27 === event.which ),
+ isBackTab = ( 9 === event.which && event.shiftKey ),
+ isSearchFocused = $( event.target ).is( this.$search );
+
+ // If enter pressed but nothing entered, don't do anything
+ if ( isEnter && ! this.$search.val() ) {
+ return;
+ }
+
+ if ( isSearchFocused && isBackTab ) {
+ this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
+ event.preventDefault(); // Avoid additional back-tab.
+ } else if ( isEsc ) {
+ this.close( { returnFocus: true } );
+ }
+ }
+ });
+
+ /**
+ * wp.customize.Menus.MenusPanel
+ *
+ * Customizer panel for menus. This is used only for screen options management.
+ * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
+ *
+ * @class wp.customize.Menus.MenusPanel
+ * @augments wp.customize.Panel
+ */
+ api.Menus.MenusPanel = api.Panel.extend(/** @lends wp.customize.Menus.MenusPanel.prototype */{
+
+ attachEvents: function() {
+ api.Panel.prototype.attachEvents.call( this );
+
+ var panel = this,
+ panelMeta = panel.container.find( '.panel-meta' ),
+ help = panelMeta.find( '.customize-help-toggle' ),
+ content = panelMeta.find( '.customize-panel-description' ),
+ options = $( '#screen-options-wrap' ),
+ button = panelMeta.find( '.customize-screen-options-toggle' );
+ button.on( 'click keydown', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+ event.preventDefault();
+
+ // Hide description
+ if ( content.not( ':hidden' ) ) {
+ content.slideUp( 'fast' );
+ help.attr( 'aria-expanded', 'false' );
+ }
+
+ if ( 'true' === button.attr( 'aria-expanded' ) ) {
+ button.attr( 'aria-expanded', 'false' );
+ panelMeta.removeClass( 'open' );
+ panelMeta.removeClass( 'active-menu-screen-options' );
+ options.slideUp( 'fast' );
+ } else {
+ button.attr( 'aria-expanded', 'true' );
+ panelMeta.addClass( 'open' );
+ panelMeta.addClass( 'active-menu-screen-options' );
+ options.slideDown( 'fast' );
+ }
+
+ return false;
+ } );
+
+ // Help toggle
+ help.on( 'click keydown', function( event ) {
+ if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
+ return;
+ }
+ event.preventDefault();
+
+ if ( 'true' === button.attr( 'aria-expanded' ) ) {
+ button.attr( 'aria-expanded', 'false' );
+ help.attr( 'aria-expanded', 'true' );
+ panelMeta.addClass( 'open' );
+ panelMeta.removeClass( 'active-menu-screen-options' );
+ options.slideUp( 'fast' );
+ content.slideDown( 'fast' );
+ }
+ } );
+ },
+
+ /**
+ * Update field visibility when clicking on the field toggles.
+ */
+ ready: function() {
+ var panel = this;
+ panel.container.find( '.hide-column-tog' ).click( function() {
+ panel.saveManageColumnsState();
+ });
+
+ // Inject additional heading into the menu locations section's head container.
+ api.section( 'menu_locations', function( section ) {
+ section.headContainer.prepend(
+ wp.template( 'nav-menu-locations-header' )( api.Menus.data )
+ );
+ } );
+ },
+
+ /**
+ * Save hidden column states.
+ *
+ * @since 4.3.0
+ * @private
+ *
+ * @returns {void}
+ */
+ saveManageColumnsState: _.debounce( function() {
+ var panel = this;
+ if ( panel._updateHiddenColumnsRequest ) {
+ panel._updateHiddenColumnsRequest.abort();
+ }
+
+ panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
+ hidden: panel.hidden(),
+ screenoptionnonce: $( '#screenoptionnonce' ).val(),
+ page: 'nav-menus'
+ } );
+ panel._updateHiddenColumnsRequest.always( function() {
+ panel._updateHiddenColumnsRequest = null;
+ } );
+ }, 2000 ),
+
+ /**
+ * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
+ */
+ checked: function() {},
+
+ /**
+ * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
+ */
+ unchecked: function() {},
+
+ /**
+ * Get hidden fields.
+ *
+ * @since 4.3.0
+ * @private
+ *
+ * @returns {Array} Fields (columns) that are hidden.
+ */
+ hidden: function() {
+ return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
+ var id = this.id;
+ return id.substring( 0, id.length - 5 );
+ }).get().join( ',' );
+ }
+ } );
+
+ /**
+ * wp.customize.Menus.MenuSection
+ *
+ * Customizer section for menus. This is used only for lazy-loading child controls.
+ * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
+ *
+ * @class wp.customize.Menus.MenuSection
+ * @augments wp.customize.Section
+ */
+ api.Menus.MenuSection = api.Section.extend(/** @lends wp.customize.Menus.MenuSection.prototype */{
+
+ /**
+ * Initialize.
+ *
+ * @since 4.3.0
+ *
+ * @param {String} id
+ * @param {Object} options
+ */
+ initialize: function( id, options ) {
+ var section = this;
+ api.Section.prototype.initialize.call( section, id, options );
+ section.deferred.initSortables = $.Deferred();
+ },
+
+ /**
+ * Ready.
+ */
+ ready: function() {
+ var section = this, fieldActiveToggles, handleFieldActiveToggle;
+
+ if ( 'undefined' === typeof section.params.menu_id ) {
+ throw new Error( 'params.menu_id was not defined' );
+ }
+
+ /*
+ * Since newly created sections won't be registered in PHP, we need to prevent the
+ * preview's sending of the activeSections to result in this control
+ * being deactivated when the preview refreshes. So we can hook onto
+ * the setting that has the same ID and its presence can dictate
+ * whether the section is active.
+ */
+ section.active.validate = function() {
+ if ( ! api.has( section.id ) ) {
+ return false;
+ }
+ return !! api( section.id ).get();
+ };
+
+ section.populateControls();
+
+ section.navMenuLocationSettings = {};
+ section.assignedLocations = new api.Value( [] );
+
+ api.each(function( setting, id ) {
+ var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
+ if ( matches ) {
+ section.navMenuLocationSettings[ matches[1] ] = setting;
+ setting.bind( function() {
+ section.refreshAssignedLocations();
+ });
+ }
+ });
+
+ section.assignedLocations.bind(function( to ) {
+ section.updateAssignedLocationsInSectionTitle( to );
+ });
+
+ section.refreshAssignedLocations();
+
+ api.bind( 'pane-contents-reflowed', function() {
+ // Skip menus that have been removed.
+ if ( ! section.contentContainer.parent().length ) {
+ return;
+ }
+ section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
+ section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
+ section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
+ section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
+ section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
+ } );
+
+ /**
+ * Update the active field class for the content container for a given checkbox toggle.
+ *
+ * @this {jQuery}
+ * @returns {void}
+ */
+ handleFieldActiveToggle = function() {
+ var className = 'field-' + $( this ).val() + '-active';
+ section.contentContainer.toggleClass( className, $( this ).prop( 'checked' ) );
+ };
+ fieldActiveToggles = api.panel( 'nav_menus' ).contentContainer.find( '.metabox-prefs:first' ).find( '.hide-column-tog' );
+ fieldActiveToggles.each( handleFieldActiveToggle );
+ fieldActiveToggles.on( 'click', handleFieldActiveToggle );
+ },
+
+ populateControls: function() {
+ var section = this,
+ menuNameControlId,
+ menuLocationsControlId,
+ menuAutoAddControlId,
+ menuDeleteControlId,
+ menuControl,
+ menuNameControl,
+ menuLocationsControl,
+ menuAutoAddControl,
+ menuDeleteControl;
+
+ // Add the control for managing the menu name.
+ menuNameControlId = section.id + '[name]';
+ menuNameControl = api.control( menuNameControlId );
+ if ( ! menuNameControl ) {
+ menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
+ type: 'nav_menu_name',
+ label: api.Menus.data.l10n.menuNameLabel,
+ section: section.id,
+ priority: 0,
+ settings: {
+ 'default': section.id
+ }
+ } );
+ api.control.add( menuNameControl );
+ menuNameControl.active.set( true );
+ }
+
+ // Add the menu control.
+ menuControl = api.control( section.id );
+ if ( ! menuControl ) {
+ menuControl = new api.controlConstructor.nav_menu( section.id, {
+ type: 'nav_menu',
+ section: section.id,
+ priority: 998,
+ settings: {
+ 'default': section.id
+ },
+ menu_id: section.params.menu_id
+ } );
+ api.control.add( menuControl );
+ menuControl.active.set( true );
+ }
+
+ // Add the menu locations control.
+ menuLocationsControlId = section.id + '[locations]';
+ menuLocationsControl = api.control( menuLocationsControlId );
+ if ( ! menuLocationsControl ) {
+ menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
+ section: section.id,
+ priority: 999,
+ settings: {
+ 'default': section.id
+ },
+ menu_id: section.params.menu_id
+ } );
+ api.control.add( menuLocationsControl.id, menuLocationsControl );
+ menuControl.active.set( true );
+ }
+
+ // Add the control for managing the menu auto_add.
+ menuAutoAddControlId = section.id + '[auto_add]';
+ menuAutoAddControl = api.control( menuAutoAddControlId );
+ if ( ! menuAutoAddControl ) {
+ menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
+ type: 'nav_menu_auto_add',
+ label: '',
+ section: section.id,
+ priority: 1000,
+ settings: {
+ 'default': section.id
+ }
+ } );
+ api.control.add( menuAutoAddControl );
+ menuAutoAddControl.active.set( true );
+ }
+
+ // Add the control for deleting the menu
+ menuDeleteControlId = section.id + '[delete]';
+ menuDeleteControl = api.control( menuDeleteControlId );
+ if ( ! menuDeleteControl ) {
+ menuDeleteControl = new api.Control( menuDeleteControlId, {
+ section: section.id,
+ priority: 1001,
+ templateId: 'nav-menu-delete-button'
+ } );
+ api.control.add( menuDeleteControl.id, menuDeleteControl );
+ menuDeleteControl.active.set( true );
+ menuDeleteControl.deferred.embedded.done( function () {
+ menuDeleteControl.container.find( 'button' ).on( 'click', function() {
+ var menuId = section.params.menu_id;
+ var menuControl = api.Menus.getMenuControl( menuId );
+ menuControl.setting.set( false );
+ });
+ } );
+ }
+ },
+
+ /**
+ *
+ */
+ refreshAssignedLocations: function() {
+ var section = this,
+ menuTermId = section.params.menu_id,
+ currentAssignedLocations = [];
+ _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
+ if ( setting() === menuTermId ) {
+ currentAssignedLocations.push( themeLocation );
+ }
+ });
+ section.assignedLocations.set( currentAssignedLocations );
+ },
+
+ /**
+ * @param {Array} themeLocationSlugs Theme location slugs.
+ */
+ updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
+ var section = this,
+ $title;
+
+ $title = section.container.find( '.accordion-section-title:first' );
+ $title.find( '.menu-in-location' ).remove();
+ _.each( themeLocationSlugs, function( themeLocationSlug ) {
+ var $label, locationName;
+ $label = $( '<span class="menu-in-location"></span>' );
+ locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug