X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/dc1231b7312fbdca99e9e887cc2bb35a28f85cdc..4ea0dca21bda49aab5ccb91ec12bb4ef5924ed3e:/wp-admin/js/customize-nav-menus.js diff --git a/wp-admin/js/customize-nav-menus.js b/wp-admin/js/customize-nav-menus.js index f5296b65..a37d2c4e 100644 --- a/wp-admin/js/customize-nav-menus.js +++ b/wp-admin/js/customize-nav-menus.js @@ -17,15 +17,15 @@ // Link settings. api.Menus.data = { - nonce: '', itemTypes: [], l10n: {}, - menuItemTransport: 'postMessage', + settingTransport: 'refresh', phpIntMax: 0, defaultSettingValues: { nav_menu: {}, nav_menu_item: {} - } + }, + locationSlugMappedToName: {} }; if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) { $.extend( api.Menus.data, _wpCustomizeNavMenusSettings ); @@ -80,6 +80,66 @@ }); api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems ); + /** + * 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( '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(); + }; + /** * wp.customize.Menus.AvailableMenuItemsPanelView * @@ -100,6 +160,8 @@ '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' }, @@ -110,11 +172,13 @@ currentMenuControl: null, debounceSearch: null, $search: null, + $clearResults: null, searchTerm: '', rendered: false, pages: {}, sectionContent: '', loading: false, + addingNew: false, initialize: function() { var self = this; @@ -124,7 +188,8 @@ } this.$search = $( '#menu-items-search' ); - this.sectionContent = this.$el.find( '.accordion-section-content' ); + this.$clearResults = this.$el.find( '.clear-results' ); + this.sectionContent = this.$el.find( '.available-menu-items-list' ); this.debounceSearch = _.debounce( self.search, 500 ); @@ -141,17 +206,9 @@ } } ); - // Clear the search results. - $( '.clear-results' ).on( 'click keydown', function( event ) { - if ( event.type === 'keydown' && ( 13 !== event.which && 32 !== event.which ) ) { // "return" or "space" keys only - return; - } - - event.preventDefault(); - - $( '#menu-items-search' ).val( '' ).focus(); - event.target.value = ''; - self.search( event ); + // 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() { @@ -168,7 +225,7 @@ // Load more items. this.sectionContent.scroll( function() { - var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ), + 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 ) { @@ -180,13 +237,17 @@ self.doSearch( self.pages.search ); } } else { - self.loadItems( type, object ); + 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. @@ -206,17 +267,13 @@ $otherSections.fadeOut( 100 ); $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' ); $searchSection.addClass( 'open' ); - $searchSection.find( '.clear-results' ) - .prop( 'tabIndex', 0 ) - .addClass( 'is-visible' ); + this.$clearResults.addClass( 'is-visible' ); } else if ( '' === event.target.value ) { $searchSection.removeClass( 'open' ); $otherSections.show(); - $searchSection.find( '.clear-results' ) - .prop( 'tabIndex', -1 ) - .removeClass( 'is-visible' ); + this.$clearResults.removeClass( 'is-visible' ); } - + this.searchTerm = event.target.value; this.pages.search = 1; this.doSearch( 1 ); @@ -247,12 +304,14 @@ $section.addClass( 'loading' ); self.loading = true; - params = { - 'customize-menus-nonce': api.Menus.data.nonce, + + 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 ); @@ -286,7 +345,7 @@ 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 ) ); + $content.empty().append( $( '
  • ' ).text( data.message ) ); wp.a11y.speak( data.message ); } self.pages.search = -1; @@ -307,51 +366,86 @@ // Render the template for each item by type. _.each( api.Menus.data.itemTypes, function( itemType ) { self.pages[ itemType.type + ':' + itemType.object ] = 0; - self.loadItems( itemType.type, itemType.object ); // @todo we need to combine these Ajax requests. } ); + self.loadItems( api.Menus.data.itemTypes ); }, - // Load available menu items. - loadItems: function( type, object ) { - var self = this, params, request, itemTemplate, availableMenuItemContainer; + /** + * 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 ( -1 === self.pages[ type + ':' + object ] ) { + 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; } - availableMenuItemContainer = $( '#available-menu-items-' + type + '-' + object ); - availableMenuItemContainer.find( '.accordion-section-title' ).addClass( 'loading' ); + self.loading = true; - params = { - 'customize-menus-nonce': api.Menus.data.nonce, + + params = api.previewer.query( { excludeCustomizedSaved: true } ); + _.extend( params, { + 'customize-menus-nonce': api.settings.nonce['customize-menus'], 'wp_customize': 'on', - 'type': type, - 'object': object, - 'page': self.pages[ type + ':' + object ] - }; + 'item_types': requestItemTypes + } ); + request = wp.ajax.post( 'load-available-menu-items-customizer', params ); request.done(function( data ) { - var items, typeInner; - items = data.items; - if ( 0 === items.length ) { - if ( 0 === self.pages[ type + ':' + object ] ) { - availableMenuItemContainer - .addClass( 'cannot-expand' ) - .removeClass( 'loading' ) - .find( '.accordion-section-title > button' ) - .prop( 'tabIndex', -1 ); + 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(); } - self.pages[ type + ':' + object ] = -1; - return; - } - items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away? - self.collection.add( items.models ); - typeInner = availableMenuItemContainer.find( '.accordion-section-content' ); - items.each(function( menuItem ) { - typeInner.append( itemTemplate( menuItem.attributes ) ); + 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; }); - self.pages[ type + ':' + object ] += 1; }); request.fail(function( data ) { if ( typeof console !== 'undefined' && console.error ) { @@ -359,20 +453,24 @@ } }); request.always(function() { - availableMenuItemContainer.find( '.accordion-section-title' ).removeClass( 'loading' ); + _.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, totalHeight, accordionHeight, diff; + var sections, lists, totalHeight, accordionHeight, diff; totalHeight = window.innerHeight; sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' ); - accordionHeight = 46 * ( 2 + sections.length ) - 13; // Magic numbers. + 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 ) ); } }, @@ -456,7 +554,7 @@ 'url': itemUrl.val(), 'type': 'custom', 'type_label': api.Menus.data.l10n.custom_label, - 'object': '' + 'object': 'custom' }; this.currentMenuControl.addItemToMenu( menuItem ); @@ -466,6 +564,105 @@ 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; @@ -588,55 +785,62 @@ }, /** - * Show/hide/save screen options (columns). From common.js. + * Update field visibility when clicking on the field toggles. */ 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.container.find( '.hide-column-tog' ).click( function() { 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: function() { - var hidden = this.hidden(); - $.post( wp.ajax.settings.url, { - action: 'hidden-columns', - hidden: hidden, + /** + * 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 ), - checked: function( column ) { - this.container.addClass( 'field-' + column + '-active' ); - }, + /** + * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers. + */ + checked: function() {}, - unchecked: function( column ) { - this.container.removeClass( 'field-' + column + '-active' ); - }, + /** + * @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() { - this.hidden = function() { - return $( '.hide-column-tog' ).not( ':checked' ).map( function() { - var id = this.id; - return id.substring( id, id.length - 5 ); - }).get().join( ',' ); - }; + return $( '.hide-column-tog' ).not( ':checked' ).map( function() { + var id = this.id; + return id.substring( 0, id.length - 5 ); + }).get().join( ',' ); } } ); @@ -652,7 +856,9 @@ api.Menus.MenuSection = api.Section.extend({ /** - * @since Menu Customizer 0.3 + * Initialize. + * + * @since 4.3.0 * * @param {String} id * @param {Object} options @@ -664,10 +870,10 @@ }, /** - * + * Ready. */ ready: function() { - var section = this; + var section = this, fieldActiveToggles, handleFieldActiveToggle; if ( 'undefined' === typeof section.params.menu_id ) { throw new Error( 'params.menu_id was not defined' ); @@ -710,7 +916,7 @@ api.bind( 'pane-contents-reflowed', function() { // Skip menus that have been removed. - if ( ! section.container.parent().length ) { + if ( ! section.contentContainer.parent().length ) { return; } section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' }); @@ -719,6 +925,20 @@ 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() { @@ -804,29 +1024,31 @@ }, /** - * @param {array} themeLocations + * @param {Array} themeLocationSlugs Theme location slugs. */ - updateAssignedLocationsInSectionTitle: function( themeLocations ) { + updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) { var section = this, $title; $title = section.container.find( '.accordion-section-title:first' ); $title.find( '.menu-in-location' ).remove(); - _.each( themeLocations, function( themeLocation ) { - var $label = $( '' ); - $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', themeLocation ) ); + _.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 !== themeLocations.length ); + section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length ); }, onChangeExpanded: function( expanded, args ) { - var section = this; + var section = this, completeCallback; if ( expanded ) { - wpNavMenu.menuList = section.container.find( '.accordion-section-content:first' ); + wpNavMenu.menuList = section.contentContainer; wpNavMenu.targetList = wpNavMenu.menuList; // Add attributes needed by wpNavMenu @@ -839,13 +1061,22 @@ } } ); - 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(); + // 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 ); } @@ -865,7 +1096,7 @@ /** * Add behaviors for the accordion section. * - * @since Menu Customizer 0.3 + * @since 4.3.0 */ attachEvents: function() { var section = this; @@ -888,8 +1119,8 @@ onChangeExpanded: function( expanded ) { var section = this, button = section.container.find( '.add-menu-toggle' ), - content = section.container.find( '.new-menu-section-content' ), - customizer = section.container.closest( '.wp-full-overlay-sidebar-content' ); + content = section.contentContainer, + customizer = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ); if ( expanded ) { button.addClass( 'open' ); button.attr( 'aria-expanded', 'true' ); @@ -902,6 +1133,17 @@ 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' ); } }); @@ -927,9 +1169,26 @@ // @todo It would be better if this was added directly on the setting itself, as opposed to the control. control.setting.validate = function( value ) { - return parseInt( value, 10 ); + if ( '' === value ) { + return 0; + } else { + return parseInt( value, 10 ); + } }; + // Edit menu button. + control.container.find( '.edit-menu' ).on( 'click', function() { + var menuId = control.setting(); + api.section( 'nav_menu[' + menuId + ']' ).focus(); + }); + control.setting.bind( 'change', function() { + if ( 0 === control.setting() ) { + control.container.find( '.edit-menu' ).addClass( 'hidden' ); + } else { + control.container.find( '.edit-menu' ).removeClass( 'hidden' ); + } + }); + // Add/remove menus from the available options when they are added and removed. api.bind( 'add', function( setting ) { var option, menuId, matches = setting.id.match( navMenuIdRegex ); @@ -985,6 +1244,13 @@ */ initialize: function( id, options ) { var control = this; + control.expanded = new api.Value( false ); + control.expandedArgumentsQueue = []; + control.expanded.bind( function( expanded ) { + var args = control.expandedArgumentsQueue.shift(); + args = $.extend( {}, control.defaultExpandedArguments, args ); + control.onChangeExpanded( expanded, args ); + }); api.Control.prototype.initialize.call( control, id, options ); control.active.validate = function() { var value, section = api.section( control.section() ); @@ -998,11 +1264,11 @@ }, /** - * @since Menu Customizer 0.3 - * * Override the embed() method to do nothing, * so that the control isn't embedded on load, * unless the containing section is already expanded. + * + * @since 4.3.0 */ embed: function() { var control = this, @@ -1012,7 +1278,7 @@ return; } section = api.section( sectionId ); - if ( section && section.expanded() ) { + if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) { control.actuallyEmbed(); } }, @@ -1021,7 +1287,7 @@ * This function is called in Section.onChangeExpanded() so the control * will only get embedded when the Section is first expanded. * - * @since Menu Customizer 0.3 + * @since 4.3.0 */ actuallyEmbed: function() { var control = this; @@ -1132,7 +1398,11 @@ } }); if ( settingValue ) { - element.set( settingValue[ property ] ); + if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) { + element.set( settingValue[ property ].join( ' ' ) ); + } else { + element.set( settingValue[ property ] ); + } } }); @@ -1246,16 +1516,21 @@ return; } - var titleEl = control.container.find( '.menu-item-title' ); + var titleEl = control.container.find( '.menu-item-title' ), + titleText = item.title || item.original_title || api.Menus.data.l10n.untitled; + + if ( item._invalid ) { + titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText ); + } // Don't update to an empty title. - if ( item.title ) { + if ( item.title || item.original_title ) { titleEl - .text( item.title ) + .text( titleText ) .removeClass( 'no-title' ); } else { titleEl - .text( api.Menus.data.l10n.untitled ) + .text( titleText ) .addClass( 'no-title' ); } } ); @@ -1299,9 +1574,9 @@ 'menu-item-edit-inactive' ]; - if ( settingValue.invalid ) { - containerClasses.push( 'invalid' ); - control.params.title = api.Menus.data.invalidTitleTpl.replace( '%s', control.params.title ); + if ( settingValue._invalid ) { + containerClasses.push( 'menu-item-invalid' ); + control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title ); } else if ( 'draft' === settingValue.status ) { containerClasses.push( 'pending' ); control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title ); @@ -1346,32 +1621,90 @@ */ expandControlSection: function() { var $section = this.container.closest( '.accordion-section' ); - if ( ! $section.hasClass( 'open' ) ) { $section.find( '.accordion-section-title:first' ).trigger( 'click' ); } }, + /** + * @since 4.6.0 + * + * @param {Boolean} expanded + * @param {Object} [params] + * @returns {Boolean} false if state already applied + */ + _toggleExpanded: api.Section.prototype._toggleExpanded, + + /** + * @since 4.6.0 + * + * @param {Object} [params] + * @returns {Boolean} false if already expanded + */ + expand: api.Section.prototype.expand, + /** * Expand the menu item form control. + * + * @since 4.5.0 Added params.completeCallback. + * + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. */ - expandForm: function() { - this.toggleForm( true ); + expandForm: function( params ) { + this.expand( params ); }, + /** + * @since 4.6.0 + * + * @param {Object} [params] + * @returns {Boolean} false if already collapsed + */ + collapse: api.Section.prototype.collapse, + /** * Collapse the menu item form control. + * + * @since 4.5.0 Added params.completeCallback. + * + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. */ - collapseForm: function() { - this.toggleForm( false ); + collapseForm: function( params ) { + this.collapse( params ); }, /** * Expand or collapse the menu item control. * - * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility + * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) + * @since 4.5.0 Added params.completeCallback. + * + * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. */ - toggleForm: function( showOrHide ) { + toggleForm: function( showOrHide, params ) { + if ( typeof showOrHide === 'undefined' ) { + showOrHide = ! this.expanded(); + } + if ( showOrHide ) { + this.expand( params ); + } else { + this.collapse( params ); + } + }, + + /** + * Expand or collapse the menu item control. + * + * @since 4.6.0 + * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. + */ + onChangeExpanded: function( showOrHide, params ) { var self = this, $menuitem, $inside, complete; $menuitem = this.container; @@ -1382,6 +1715,9 @@ // Already expanded or collapsed. if ( $inside.is( ':visible' ) === showOrHide ) { + if ( params && params.completeCallback ) { + params.completeCallback(); + } return; } @@ -1398,6 +1734,10 @@ .removeClass( 'menu-item-edit-inactive' ) .addClass( 'menu-item-edit-active' ); self.container.trigger( 'expanded' ); + + if ( params && params.completeCallback ) { + params.completeCallback(); + } }; $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' ); @@ -1410,6 +1750,10 @@ .addClass( 'menu-item-edit-inactive' ) .removeClass( 'menu-item-edit-active' ); self.container.trigger( 'collapsed' ); + + if ( params && params.completeCallback ) { + params.completeCallback(); + } }; self.container.trigger( 'collapse' ); @@ -1422,11 +1766,41 @@ /** * Expand the containing menu section, expand the form, and focus on * the first input in the control. + * + * @since 4.5.0 Added params.completeCallback. + * + * @param {Object} [params] - Params object. + * @param {Function} [params.completeCallback] - Optional callback function when focus has completed. */ - focus: function() { - this.expandControlSection(); - this.expandForm(); - this.container.find( '.menu-item-settings :focusable:first' ).focus(); + focus: function( params ) { + params = params || {}; + var control = this, originalCompleteCallback = params.completeCallback, focusControl; + + focusControl = function() { + control.expandControlSection(); + + params.completeCallback = function() { + var focusable; + + // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583 + focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ); + focusable.first().focus(); + + if ( originalCompleteCallback ) { + originalCompleteCallback(); + } + }; + + control.expandForm( params ); + }; + + if ( api.section.has( control.section() ) ) { + api.section( control.section() ).expand( { + completeCallback: focusControl + } ); + } else { + focusControl(); + } }, /** @@ -1739,6 +2113,7 @@ */ ready: function() { var control = this, + section = api.section( control.section() ), menuId = control.params.menu_id, menu = control.setting(), name, @@ -1755,7 +2130,7 @@ * being deactivated. */ control.active.validate = function() { - var value, section = api.section( control.section() ); + var value; if ( section ) { value = section.active(); } else { @@ -1764,7 +2139,7 @@ return value; }; - control.$controlSection = control.container.closest( '.control-section' ); + control.$controlSection = section.headContainer; control.$sectionContent = control.container.closest( '.accordion-section-content' ); this._setupModel(); @@ -1867,6 +2242,9 @@ control.isSorting = false; + // Reset horizontal scroll position when done dragging. + control.$sectionContent.scrollLeft( 0 ); + _.each( menuItemContainerIds, function( menuItemContainerId ) { var menuItemId, menuItemControl, matches; matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' ); @@ -2035,11 +2413,11 @@ return; } - var section = control.container.closest( '.accordion-section' ), + var section = api.section( control.section() ), menuId = control.params.menu_id, - controlTitle = section.find( '.accordion-section-title' ), - sectionTitle = section.find( '.customize-section-title h3' ), - location = section.find( '.menu-in-location' ), + controlTitle = section.headContainer.find( '.accordion-section-title' ), + sectionTitle = section.contentContainer.find( '.customize-section-title h3' ), + location = section.headContainer.find( '.menu-in-location' ), action = sectionTitle.find( '.customize-action' ), name = displayNavMenuName( menu.name ); @@ -2063,7 +2441,7 @@ } ); // Update the nav menu name in all location checkboxes. - section.find( '.customize-control-checkbox input' ).each( function() { + section.contentContainer.find( '.customize-control-checkbox input' ).each( function() { if ( $( this ).prop( 'checked' ) ) { $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name ); } @@ -2251,7 +2629,7 @@ customizeId = 'nav_menu_item[' + String( placeholderId ) + ']'; settingArgs = { type: 'nav_menu_item', - transport: 'postMessage', + transport: api.Menus.data.settingTransport, previewer: api.previewer }; setting = api.create( customizeId, customizeId, {}, settingArgs ); @@ -2340,7 +2718,7 @@ // Register the menu control setting. api.create( customizeId, customizeId, {}, { type: 'nav_menu', - transport: 'postMessage', + transport: api.Menus.data.settingTransport, previewer: api.previewer } ); api( customizeId ).set( $.extend( @@ -2377,9 +2755,6 @@ // Focus on the new menu section. api.section( customizeId ).focus(); // @todo should we focus on the new menu's control and open the add-items panel? Thinking user flow... - - // Fix an issue with extra space at top immediately after creating new menu. - $( '#menu-to-edit' ).css( 'margin-top', 0 ); } }); @@ -2427,9 +2802,19 @@ } } ); - api.previewer.bind( 'refresh', function() { - api.previewer.refresh(); - }); + /* + * Reset the list of posts created in the customizer once published. + * The setting is updated quietly (bypassing events being triggered) + * so that the customized state doesn't become immediately dirty. + */ + api.state( 'changesetStatus' ).bind( function( status ) { + if ( 'publish' === status ) { + api( 'nav_menus_created_posts' )._value = []; + } + } ); + + // Open and focus menu control. + api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl ); } ); /** @@ -2442,7 +2827,7 @@ */ api.Menus.applySavedData = function( data ) { - var insertedMenuIdMapping = {}; + var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {}; _( data.nav_menu_updates ).each(function( update ) { var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount; @@ -2473,7 +2858,7 @@ newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']'; newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { type: 'nav_menu', - transport: 'postMessage', + transport: api.Menus.data.settingTransport, previewer: api.previewer } ); @@ -2573,6 +2958,13 @@ } } ); + // Build up mapping of nav_menu_item placeholder IDs to inserted IDs. + _( data.nav_menu_item_updates ).each(function( update ) { + if ( update.previous_post_id ) { + insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id; + } + }); + _( data.nav_menu_item_updates ).each(function( update ) { var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl; if ( 'inserted' === update.status ) { @@ -2598,6 +2990,14 @@ } settingValue = _.clone( settingValue ); + // If the parent menu item was also inserted, update the menu_item_parent to the new ID. + if ( settingValue.menu_item_parent < 0 ) { + if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) { + throw new Error( 'inserted ID for menu_item_parent not available' ); + } + settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ]; + } + // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id. if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) { settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ]; @@ -2606,7 +3006,7 @@ newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']'; newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { type: 'nav_menu_item', - transport: 'postMessage', + transport: api.Menus.data.settingTransport, previewer: api.previewer } ); @@ -2664,7 +3064,6 @@ */ api.Menus.focusMenuItemControl = function( menuItemId ) { var control = api.Menus.getMenuItemControl( menuItemId ); - if ( control ) { control.focus(); }