/* 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(); }; 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. * * @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. * * @constructor * @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. * * @constructor * @augments Backbone.Model */ api.Menus.AvailableItemCollection = Backbone.Collection.extend({ 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 ); api.Menus.insertedAutoDrafts = []; /** * Insert a new `auto-draft` post. * * @since 4.7.0 * @access public * * @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', 'params': params } ); request.done( function( response ) { if ( response.post_id ) { api.Menus.insertedAutoDrafts.push( response.post_id ); api( 'nav_menus_created_posts' ).set( _.clone( api.Menus.insertedAutoDrafts ) ); 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(); }; /** * wp.customize.Menus.AvailableMenuItemsPanelView * * View class for the available menu items panel. * * @constructor * @augments wp.Backbone.View * @augments Backbone.View */ api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend({ el: '#available-menu-items', events: { 'input #menu-items-search': 'debounceSearch', 'keyup #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, 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( $( '
  • ' ).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.} 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' ); if ( ! this.currentMenuControl ) { return; } if ( '' === itemName.val() ) { itemName.addClass( 'invalid' ); return; } else if ( '' === itemUrl.val() || 'http://' === itemUrl.val() ) { itemUrl.addClass( 'invalid' ); return; } menuItem = { 'title': itemName.val(), 'url': itemUrl.val(), 'type': 'custom', 'type_label': api.Menus.data.l10n.custom_label, 'object': 'custom' }; this.currentMenuControl.addItemToMenu( menuItem ); // Reset the custom link form. itemUrl.val( 'http://' ); 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 ) { this.currentMenuControl = menuControl; this.itemSectionHeight(); $( 'body' ).addClass( 'adding-menu-items' ); // 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( '' ); }, // 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. * * @constructor * @augments wp.customize.Panel */ api.Menus.MenusPanel = api.Panel.extend({ 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' ); } } ); }, /** * Show/hide/save screen options (columns). From common.js. */ ready: function() { var panel = this; this.container.find( '.hide-column-tog' ).click( function() { var $t = $( this ), column = $t.val(); if ( $t.prop( 'checked' ) ) { panel.checked( column ); } else { panel.unchecked( column ); } panel.saveManageColumnsState(); }); this.container.find( '.hide-column-tog' ).each( function() { var $t = $( this ), column = $t.val(); if ( $t.prop( 'checked' ) ) { panel.checked( column ); } else { panel.unchecked( column ); } }); }, 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 ), checked: function( column ) { this.container.addClass( 'field-' + column + '-active' ); }, unchecked: function( column ) { this.container.removeClass( 'field-' + column + '-active' ); }, 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. * * @constructor * @augments wp.customize.Section */ api.Menus.MenuSection = api.Section.extend({ /** * 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; 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' }); } ); }, populateControls: function() { var section = this, menuNameControlId, menuAutoAddControlId, menuControl, menuNameControl, menuAutoAddControl; // 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, { params: { type: 'nav_menu_name', content: '
  • ', // @todo core should do this for us; see #30741 label: api.Menus.data.l10n.menuNameLabel, active: true, section: section.id, priority: 0, settings: { 'default': section.id } } } ); api.control.add( menuNameControl.id, menuNameControl ); menuNameControl.active.set( true ); } // Add the menu control. menuControl = api.control( section.id ); if ( ! menuControl ) { menuControl = new api.controlConstructor.nav_menu( section.id, { params: { type: 'nav_menu', content: '
  • ', // @todo core should do this for us; see #30741 section: section.id, priority: 998, active: true, settings: { 'default': section.id }, menu_id: section.params.menu_id } } ); api.control.add( menuControl.id, menuControl ); 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, { params: { type: 'nav_menu_auto_add', content: '
  • ', // @todo core should do this for us label: '', active: true, section: section.id, priority: 999, settings: { 'default': section.id } } } ); api.control.add( menuAutoAddControl.id, menuAutoAddControl ); menuAutoAddControl.active.set( true ); } }, /** * */ 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 = $( '' ); locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ]; $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) ); $title.append( $label ); }); section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length ); }, onChangeExpanded: function( expanded, args ) { var section = this, completeCallback; if ( expanded ) { wpNavMenu.menuList = section.contentContainer; wpNavMenu.targetList = wpNavMenu.menuList; // Add attributes needed by wpNavMenu $( '#menu-to-edit' ).removeAttr( 'id' ); wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' ); _.each( api.section( section.id ).controls(), function( control ) { if ( 'nav_menu_item' === control.params.type ) { control.actuallyEmbed(); } } ); // Make sure Sortables is initialized after the section has been expanded to prevent `offset` issues. if ( args.completeCallback ) { completeCallback = args.completeCallback; } args.completeCallback = function() { if ( 'resolved' !== section.deferred.initSortables.state() ) { wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above. section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable. // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated. api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems(); } if ( _.isFunction( completeCallback ) ) { completeCallback(); } }; } api.Section.prototype.onChangeExpanded.call( section, expanded, args ); } }); /** * wp.customize.Menus.NewMenuSection * * Customizer section for new menus. * Note that 'new_menu' must match the WP_Customize_New_Menu_Section::$type. * * @constructor * @augments wp.customize.Section */ api.Menus.NewMenuSection = api.Section.extend({ /** * Add behaviors for the accordion section. * * @since 4.3.0 */ attachEvents: function() { var section = this; this.container.on( 'click', '.add-menu-toggle', function() { if ( section.expanded() ) { section.collapse(); } else { section.expand(); } }); }, /** * Update UI to reflect expanded state. * * @since 4.1.0 * * @param {Boolean} expanded */ onChangeExpanded: function( expanded ) { var section = this, button = section.container.find( '.add-menu-toggle' ), content = section.contentContainer, customizer = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ); if ( expanded ) { button.addClass( 'open' ); button.attr( 'aria-expanded', 'true' ); content.slideDown( 'fast', function() { customizer.scrollTop( customizer.height() ); }); } else { button.removeClass( 'open' ); button.attr( 'aria-expanded', 'false' ); content.slideUp( 'fast' ); content.find( '.menu-name-field' ).removeClass( 'invalid' ); } }, /** * Find the content element. * * @since 4.7.0 * * @returns {jQuery} Content UL element. */ getContent: function() { return this.container.find( 'ul:first' ); } }); /** * wp.customize.Menus.MenuLocationControl * * Customizer control for menu locations (rendered as a