1 /* global _wpCustomizeNavMenusSettings, wpNavMenu, console */
2 ( function( api, wp, $ ) {
6 * Set up wpNavMenu for drag and drop.
8 wpNavMenu.originalInit = wpNavMenu.init;
9 wpNavMenu.options.menuItemDepthPerLevel = 20;
10 wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
11 wpNavMenu.options.targetTolerance = 10;
12 wpNavMenu.init = function() {
13 this.jQueryExtensions();
16 api.Menus = api.Menus || {};
22 settingTransport: 'refresh',
24 defaultSettingValues: {
28 locationSlugMappedToName: {}
30 if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
31 $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
35 * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
36 * serve as placeholders until Save & Publish happens.
40 api.Menus.generatePlaceholderAutoIncrementId = function() {
41 return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
45 * wp.customize.Menus.AvailableItemModel
47 * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
50 * @augments Backbone.Model
52 api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
54 id: null // This is only used by Backbone.
56 api.Menus.data.defaultSettingValues.nav_menu_item
60 * wp.customize.Menus.AvailableItemCollection
62 * Collection for available menu item models.
65 * @augments Backbone.Model
67 api.Menus.AvailableItemCollection = Backbone.Collection.extend({
68 model: api.Menus.AvailableItemModel,
72 comparator: function( item ) {
73 return -item.get( this.sort_key );
76 sortByField: function( fieldName ) {
77 this.sort_key = fieldName;
81 api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
84 * wp.customize.Menus.AvailableMenuItemsPanelView
86 * View class for the available menu items panel.
89 * @augments wp.Backbone.View
90 * @augments Backbone.View
92 api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend({
94 el: '#available-menu-items',
97 'input #menu-items-search': 'debounceSearch',
98 'keyup #menu-items-search': 'debounceSearch',
99 'focus .menu-item-tpl': 'focus',
100 'click .menu-item-tpl': '_submit',
101 'click #custom-menu-item-submit': '_submitLink',
102 'keypress #custom-menu-item-name': '_submitLink',
103 'keydown': 'keyboardAccessible'
106 // Cache current selected menu item.
109 // Cache menu control that opened the panel.
110 currentMenuControl: null,
111 debounceSearch: null,
119 initialize: function() {
122 if ( ! api.panel.has( 'nav_menus' ) ) {
126 this.$search = $( '#menu-items-search' );
127 this.sectionContent = this.$el.find( '.accordion-section-content' );
129 this.debounceSearch = _.debounce( self.search, 500 );
131 _.bindAll( this, 'close' );
133 // If the available menu items panel is open and the customize controls are
134 // interacted with (other than an item being deleted), then close the
135 // available menu items panel. Also close on back button click.
136 $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
137 var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
138 isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
139 if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
144 // Clear the search results.
145 $( '.clear-results' ).on( 'click', function() {
146 self.$search.val( '' ).focus().trigger( 'keyup' );
149 this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
150 $( this ).removeClass( 'invalid' );
153 // Load available items if it looks like we'll need them.
154 api.panel( 'nav_menus' ).container.bind( 'expanded', function() {
155 if ( ! self.rendered ) {
157 self.rendered = true;
162 this.sectionContent.scroll( function() {
163 var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ),
164 visibleHeight = self.$el.find( '.accordion-section.open' ).height();
166 if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
167 var type = $( this ).data( 'type' ),
168 object = $( this ).data( 'object' );
170 if ( 'search' === type ) {
171 if ( self.searchTerm ) {
172 self.doSearch( self.pages.search );
175 self.loadItems( type, object );
180 // Close the panel if the URL in the preview changes
181 api.previewer.bind( 'url', this.close );
183 self.delegateEvents();
186 // Search input change handler.
187 search: function( event ) {
188 var $searchSection = $( '#available-menu-items-search' ),
189 $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
195 if ( this.searchTerm === event.target.value ) {
199 if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
200 $otherSections.fadeOut( 100 );
201 $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
202 $searchSection.addClass( 'open' );
203 $searchSection.find( '.clear-results' ).addClass( 'is-visible' );
204 } else if ( '' === event.target.value ) {
205 $searchSection.removeClass( 'open' );
206 $otherSections.show();
207 $searchSection.find( '.clear-results' ).removeClass( 'is-visible' );
210 this.searchTerm = event.target.value;
211 this.pages.search = 1;
215 // Get search results.
216 doSearch: function( page ) {
217 var self = this, params,
218 $section = $( '#available-menu-items-search' ),
219 $content = $section.find( '.accordion-section-content' ),
220 itemTemplate = wp.template( 'available-menu-item' );
222 if ( self.currentRequest ) {
223 self.currentRequest.abort();
228 } else if ( page > 1 ) {
229 $section.addClass( 'loading-more' );
230 $content.attr( 'aria-busy', 'true' );
231 wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
232 } else if ( '' === self.searchTerm ) {
238 $section.addClass( 'loading' );
241 'customize-menus-nonce': api.settings.nonce['customize-menus'],
242 'wp_customize': 'on',
243 'search': self.searchTerm,
247 self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
249 self.currentRequest.done(function( data ) {
252 // Clear previous results as it's a new search.
255 $section.removeClass( 'loading loading-more' );
256 $content.attr( 'aria-busy', 'false' );
257 $section.addClass( 'open' );
258 self.loading = false;
259 items = new api.Menus.AvailableItemCollection( data.items );
260 self.collection.add( items.models );
261 items.each( function( menuItem ) {
262 $content.append( itemTemplate( menuItem.attributes ) );
264 if ( 20 > items.length ) {
265 self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
267 self.pages.search = self.pages.search + 1;
269 if ( items && page > 1 ) {
270 wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
271 } else if ( items && page === 1 ) {
272 wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
276 self.currentRequest.fail(function( data ) {
277 // data.message may be undefined, for example when typing slow and the request is aborted.
278 if ( data.message ) {
279 $content.empty().append( $( '<p class="nothing-found"></p>' ).text( data.message ) );
280 wp.a11y.speak( data.message );
282 self.pages.search = -1;
285 self.currentRequest.always(function() {
286 $section.removeClass( 'loading loading-more' );
287 $content.attr( 'aria-busy', 'false' );
288 self.loading = false;
289 self.currentRequest = null;
293 // Render the individual items.
294 initList: function() {
297 // Render the template for each item by type.
298 _.each( api.Menus.data.itemTypes, function( itemType ) {
299 self.pages[ itemType.type + ':' + itemType.object ] = 0;
300 self.loadItems( itemType.type, itemType.object ); // @todo we need to combine these Ajax requests.
304 // Load available menu items.
305 loadItems: function( type, object ) {
306 var self = this, params, request, itemTemplate, availableMenuItemContainer;
307 itemTemplate = wp.template( 'available-menu-item' );
309 if ( -1 === self.pages[ type + ':' + object ] ) {
312 availableMenuItemContainer = $( '#available-menu-items-' + type + '-' + object );
313 availableMenuItemContainer.find( '.accordion-section-title' ).addClass( 'loading' );
316 'customize-menus-nonce': api.settings.nonce['customize-menus'],
317 'wp_customize': 'on',
320 'page': self.pages[ type + ':' + object ]
322 request = wp.ajax.post( 'load-available-menu-items-customizer', params );
324 request.done(function( data ) {
325 var items, typeInner;
327 if ( 0 === items.length ) {
328 if ( 0 === self.pages[ type + ':' + object ] ) {
329 availableMenuItemContainer
330 .addClass( 'cannot-expand' )
331 .removeClass( 'loading' )
332 .find( '.accordion-section-title > button' )
333 .prop( 'tabIndex', -1 );
335 self.pages[ type + ':' + object ] = -1;
338 items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
339 self.collection.add( items.models );
340 typeInner = availableMenuItemContainer.find( '.accordion-section-content' );
341 items.each(function( menuItem ) {
342 typeInner.append( itemTemplate( menuItem.attributes ) );
344 self.pages[ type + ':' + object ] += 1;
346 request.fail(function( data ) {
347 if ( typeof console !== 'undefined' && console.error ) {
348 console.error( data );
351 request.always(function() {
352 availableMenuItemContainer.find( '.accordion-section-title' ).removeClass( 'loading' );
353 self.loading = false;
357 // Adjust the height of each section of items to fit the screen.
358 itemSectionHeight: function() {
359 var sections, totalHeight, accordionHeight, diff;
360 totalHeight = window.innerHeight;
361 sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
362 accordionHeight = 46 * ( 2 + sections.length ) - 13; // Magic numbers.
363 diff = totalHeight - accordionHeight;
364 if ( 120 < diff && 290 > diff ) {
365 sections.css( 'max-height', diff );
369 // Highlights a menu item.
370 select: function( menuitemTpl ) {
371 this.selected = $( menuitemTpl );
372 this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
373 this.selected.addClass( 'selected' );
376 // Highlights a menu item on focus.
377 focus: function( event ) {
378 this.select( $( event.currentTarget ) );
381 // Submit handler for keypress and click on menu item.
382 _submit: function( event ) {
383 // Only proceed with keypress if it is Enter or Spacebar
384 if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
388 this.submit( $( event.currentTarget ) );
391 // Adds a selected menu item to the menu.
392 submit: function( menuitemTpl ) {
393 var menuitemId, menu_item;
395 if ( ! menuitemTpl ) {
396 menuitemTpl = this.selected;
399 if ( ! menuitemTpl || ! this.currentMenuControl ) {
403 this.select( menuitemTpl );
405 menuitemId = $( this.selected ).data( 'menu-item-id' );
406 menu_item = this.collection.findWhere( { id: menuitemId } );
411 this.currentMenuControl.addItemToMenu( menu_item.attributes );
413 $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
416 // Submit handler for keypress and click on custom menu item.
417 _submitLink: function( event ) {
418 // Only proceed with keypress if it is Enter.
419 if ( 'keypress' === event.type && 13 !== event.which ) {
426 // Adds the custom menu item to the menu.
427 submitLink: function() {
429 itemName = $( '#custom-menu-item-name' ),
430 itemUrl = $( '#custom-menu-item-url' );
432 if ( ! this.currentMenuControl ) {
436 if ( '' === itemName.val() ) {
437 itemName.addClass( 'invalid' );
439 } else if ( '' === itemUrl.val() || 'http://' === itemUrl.val() ) {
440 itemUrl.addClass( 'invalid' );
445 'title': itemName.val(),
446 'url': itemUrl.val(),
448 'type_label': api.Menus.data.l10n.custom_label,
452 this.currentMenuControl.addItemToMenu( menuItem );
454 // Reset the custom link form.
455 itemUrl.val( 'http://' );
460 open: function( menuControl ) {
461 this.currentMenuControl = menuControl;
463 this.itemSectionHeight();
465 $( 'body' ).addClass( 'adding-menu-items' );
467 // Collapse all controls.
468 _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
469 control.collapseForm();
472 this.$el.find( '.selected' ).removeClass( 'selected' );
474 this.$search.focus();
478 close: function( options ) {
479 options = options || {};
481 if ( options.returnFocus && this.currentMenuControl ) {
482 this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
485 this.currentMenuControl = null;
486 this.selected = null;
488 $( 'body' ).removeClass( 'adding-menu-items' );
489 $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
491 this.$search.val( '' );
494 // Add a few keyboard enhancements to the panel.
495 keyboardAccessible: function( event ) {
496 var isEnter = ( 13 === event.which ),
497 isEsc = ( 27 === event.which ),
498 isBackTab = ( 9 === event.which && event.shiftKey ),
499 isSearchFocused = $( event.target ).is( this.$search );
501 // If enter pressed but nothing entered, don't do anything
502 if ( isEnter && ! this.$search.val() ) {
506 if ( isSearchFocused && isBackTab ) {
507 this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
508 event.preventDefault(); // Avoid additional back-tab.
509 } else if ( isEsc ) {
510 this.close( { returnFocus: true } );
516 * wp.customize.Menus.MenusPanel
518 * Customizer panel for menus. This is used only for screen options management.
519 * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
522 * @augments wp.customize.Panel
524 api.Menus.MenusPanel = api.Panel.extend({
526 attachEvents: function() {
527 api.Panel.prototype.attachEvents.call( this );
530 panelMeta = panel.container.find( '.panel-meta' ),
531 help = panelMeta.find( '.customize-help-toggle' ),
532 content = panelMeta.find( '.customize-panel-description' ),
533 options = $( '#screen-options-wrap' ),
534 button = panelMeta.find( '.customize-screen-options-toggle' );
535 button.on( 'click keydown', function( event ) {
536 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
539 event.preventDefault();
542 if ( content.not( ':hidden' ) ) {
543 content.slideUp( 'fast' );
544 help.attr( 'aria-expanded', 'false' );
547 if ( 'true' === button.attr( 'aria-expanded' ) ) {
548 button.attr( 'aria-expanded', 'false' );
549 panelMeta.removeClass( 'open' );
550 panelMeta.removeClass( 'active-menu-screen-options' );
551 options.slideUp( 'fast' );
553 button.attr( 'aria-expanded', 'true' );
554 panelMeta.addClass( 'open' );
555 panelMeta.addClass( 'active-menu-screen-options' );
556 options.slideDown( 'fast' );
563 help.on( 'click keydown', function( event ) {
564 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
567 event.preventDefault();
569 if ( 'true' === button.attr( 'aria-expanded' ) ) {
570 button.attr( 'aria-expanded', 'false' );
571 help.attr( 'aria-expanded', 'true' );
572 panelMeta.addClass( 'open' );
573 panelMeta.removeClass( 'active-menu-screen-options' );
574 options.slideUp( 'fast' );
575 content.slideDown( 'fast' );
581 * Show/hide/save screen options (columns). From common.js.
585 this.container.find( '.hide-column-tog' ).click( function() {
586 var $t = $( this ), column = $t.val();
587 if ( $t.prop( 'checked' ) ) {
588 panel.checked( column );
590 panel.unchecked( column );
593 panel.saveManageColumnsState();
595 this.container.find( '.hide-column-tog' ).each( function() {
596 var $t = $( this ), column = $t.val();
597 if ( $t.prop( 'checked' ) ) {
598 panel.checked( column );
600 panel.unchecked( column );
605 saveManageColumnsState: _.debounce( function() {
607 if ( panel._updateHiddenColumnsRequest ) {
608 panel._updateHiddenColumnsRequest.abort();
611 panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
612 hidden: panel.hidden(),
613 screenoptionnonce: $( '#screenoptionnonce' ).val(),
616 panel._updateHiddenColumnsRequest.always( function() {
617 panel._updateHiddenColumnsRequest = null;
621 checked: function( column ) {
622 this.container.addClass( 'field-' + column + '-active' );
625 unchecked: function( column ) {
626 this.container.removeClass( 'field-' + column + '-active' );
630 return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
632 return id.substring( 0, id.length - 5 );
633 }).get().join( ',' );
638 * wp.customize.Menus.MenuSection
640 * Customizer section for menus. This is used only for lazy-loading child controls.
641 * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
644 * @augments wp.customize.Section
646 api.Menus.MenuSection = api.Section.extend({
649 * @since Menu Customizer 0.3
652 * @param {Object} options
654 initialize: function( id, options ) {
656 api.Section.prototype.initialize.call( section, id, options );
657 section.deferred.initSortables = $.Deferred();
666 if ( 'undefined' === typeof section.params.menu_id ) {
667 throw new Error( 'params.menu_id was not defined' );
671 * Since newly created sections won't be registered in PHP, we need to prevent the
672 * preview's sending of the activeSections to result in this control
673 * being deactivated when the preview refreshes. So we can hook onto
674 * the setting that has the same ID and its presence can dictate
675 * whether the section is active.
677 section.active.validate = function() {
678 if ( ! api.has( section.id ) ) {
681 return !! api( section.id ).get();
684 section.populateControls();
686 section.navMenuLocationSettings = {};
687 section.assignedLocations = new api.Value( [] );
689 api.each(function( setting, id ) {
690 var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
692 section.navMenuLocationSettings[ matches[1] ] = setting;
693 setting.bind( function() {
694 section.refreshAssignedLocations();
699 section.assignedLocations.bind(function( to ) {
700 section.updateAssignedLocationsInSectionTitle( to );
703 section.refreshAssignedLocations();
705 api.bind( 'pane-contents-reflowed', function() {
706 // Skip menus that have been removed.
707 if ( ! section.container.parent().length ) {
710 section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
711 section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
712 section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
713 section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
714 section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
718 populateControls: function() {
719 var section = this, menuNameControlId, menuAutoAddControlId, menuControl, menuNameControl, menuAutoAddControl;
721 // Add the control for managing the menu name.
722 menuNameControlId = section.id + '[name]';
723 menuNameControl = api.control( menuNameControlId );
724 if ( ! menuNameControl ) {
725 menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
727 type: 'nav_menu_name',
728 content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '-name" class="customize-control customize-control-nav_menu_name"></li>', // @todo core should do this for us; see #30741
729 label: api.Menus.data.l10n.menuNameLabel,
734 'default': section.id
738 api.control.add( menuNameControl.id, menuNameControl );
739 menuNameControl.active.set( true );
742 // Add the menu control.
743 menuControl = api.control( section.id );
744 if ( ! menuControl ) {
745 menuControl = new api.controlConstructor.nav_menu( section.id, {
748 content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '" class="customize-control customize-control-nav_menu"></li>', // @todo core should do this for us; see #30741
753 'default': section.id
755 menu_id: section.params.menu_id
758 api.control.add( menuControl.id, menuControl );
759 menuControl.active.set( true );
762 // Add the control for managing the menu auto_add.
763 menuAutoAddControlId = section.id + '[auto_add]';
764 menuAutoAddControl = api.control( menuAutoAddControlId );
765 if ( ! menuAutoAddControl ) {
766 menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
768 type: 'nav_menu_auto_add',
769 content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '-auto-add" class="customize-control customize-control-nav_menu_auto_add"></li>', // @todo core should do this for us
775 'default': section.id
779 api.control.add( menuAutoAddControl.id, menuAutoAddControl );
780 menuAutoAddControl.active.set( true );
788 refreshAssignedLocations: function() {
790 menuTermId = section.params.menu_id,
791 currentAssignedLocations = [];
792 _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
793 if ( setting() === menuTermId ) {
794 currentAssignedLocations.push( themeLocation );
797 section.assignedLocations.set( currentAssignedLocations );
801 * @param {array} themeLocations
803 updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
807 $title = section.container.find( '.accordion-section-title:first' );
808 $title.find( '.menu-in-location' ).remove();
809 _.each( themeLocationSlugs, function( themeLocationSlug ) {
810 var $label, locationName;
811 $label = $( '<span class="menu-in-location"></span>' );
812 locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ];
813 $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) );
814 $title.append( $label );
817 section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length );
821 onChangeExpanded: function( expanded, args ) {
825 wpNavMenu.menuList = section.container.find( '.accordion-section-content:first' );
826 wpNavMenu.targetList = wpNavMenu.menuList;
828 // Add attributes needed by wpNavMenu
829 $( '#menu-to-edit' ).removeAttr( 'id' );
830 wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
832 _.each( api.section( section.id ).controls(), function( control ) {
833 if ( 'nav_menu_item' === control.params.type ) {
834 control.actuallyEmbed();
838 if ( 'resolved' !== section.deferred.initSortables.state() ) {
839 wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
840 section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
842 // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated.
843 api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
846 api.Section.prototype.onChangeExpanded.call( section, expanded, args );
851 * wp.customize.Menus.NewMenuSection
853 * Customizer section for new menus.
854 * Note that 'new_menu' must match the WP_Customize_New_Menu_Section::$type.
857 * @augments wp.customize.Section
859 api.Menus.NewMenuSection = api.Section.extend({
862 * Add behaviors for the accordion section.
864 * @since Menu Customizer 0.3
866 attachEvents: function() {
868 this.container.on( 'click', '.add-menu-toggle', function() {
869 if ( section.expanded() ) {
878 * Update UI to reflect expanded state.
882 * @param {Boolean} expanded
884 onChangeExpanded: function( expanded ) {
886 button = section.container.find( '.add-menu-toggle' ),
887 content = section.container.find( '.new-menu-section-content' ),
888 customizer = section.container.closest( '.wp-full-overlay-sidebar-content' );
890 button.addClass( 'open' );
891 button.attr( 'aria-expanded', 'true' );
892 content.slideDown( 'fast', function() {
893 customizer.scrollTop( customizer.height() );
896 button.removeClass( 'open' );
897 button.attr( 'aria-expanded', 'false' );
898 content.slideUp( 'fast' );
899 content.find( '.menu-name-field' ).removeClass( 'invalid' );
905 * wp.customize.Menus.MenuLocationControl
907 * Customizer control for menu locations (rendered as a <select>).
908 * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
911 * @augments wp.customize.Control
913 api.Menus.MenuLocationControl = api.Control.extend({
914 initialize: function( id, options ) {
916 matches = id.match( /^nav_menu_locations\[(.+?)]/ );
917 control.themeLocation = matches[1];
918 api.Control.prototype.initialize.call( control, id, options );
922 var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
924 // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
925 control.setting.validate = function( value ) {
926 return parseInt( value, 10 );
930 control.container.find( '.edit-menu' ).on( 'click', function() {
931 var menuId = control.setting();
932 api.section( 'nav_menu[' + menuId + ']' ).focus();
934 control.setting.bind( 'change', function() {
935 if ( 0 === control.setting() ) {
936 control.container.find( '.edit-menu' ).addClass( 'hidden' );
938 control.container.find( '.edit-menu' ).removeClass( 'hidden' );
942 // Add/remove menus from the available options when they are added and removed.
943 api.bind( 'add', function( setting ) {
944 var option, menuId, matches = setting.id.match( navMenuIdRegex );
945 if ( ! matches || false === setting() ) {
949 option = new Option( displayNavMenuName( setting().name ), menuId );
950 control.container.find( 'select' ).append( option );
952 api.bind( 'remove', function( setting ) {
953 var menuId, matches = setting.id.match( navMenuIdRegex );
957 menuId = parseInt( matches[1], 10 );
958 if ( control.setting() === menuId ) {
959 control.setting.set( '' );
961 control.container.find( 'option[value=' + menuId + ']' ).remove();
963 api.bind( 'change', function( setting ) {
964 var menuId, matches = setting.id.match( navMenuIdRegex );
968 menuId = parseInt( matches[1], 10 );
969 if ( false === setting() ) {
970 if ( control.setting() === menuId ) {
971 control.setting.set( '' );
973 control.container.find( 'option[value=' + menuId + ']' ).remove();
975 control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
982 * wp.customize.Menus.MenuItemControl
984 * Customizer control for menu items.
985 * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
988 * @augments wp.customize.Control
990 api.Menus.MenuItemControl = api.Control.extend({
995 initialize: function( id, options ) {
997 control.expanded = new api.Value( false );
998 control.expandedArgumentsQueue = [];
999 control.expanded.bind( function( expanded ) {
1000 var args = control.expandedArgumentsQueue.shift();
1001 args = $.extend( {}, control.defaultExpandedArguments, args );
1002 control.onChangeExpanded( expanded, args );
1004 api.Control.prototype.initialize.call( control, id, options );
1005 control.active.validate = function() {
1006 var value, section = api.section( control.section() );
1008 value = section.active();
1017 * @since Menu Customizer 0.3
1019 * Override the embed() method to do nothing,
1020 * so that the control isn't embedded on load,
1021 * unless the containing section is already expanded.
1025 sectionId = control.section(),
1027 if ( ! sectionId ) {
1030 section = api.section( sectionId );
1031 if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
1032 control.actuallyEmbed();
1037 * This function is called in Section.onChangeExpanded() so the control
1038 * will only get embedded when the Section is first expanded.
1040 * @since Menu Customizer 0.3
1042 actuallyEmbed: function() {
1044 if ( 'resolved' === control.deferred.embedded.state() ) {
1047 control.renderContent();
1048 control.deferred.embedded.resolve(); // This triggers control.ready().
1052 * Set up the control.
1055 if ( 'undefined' === typeof this.params.menu_item_id ) {
1056 throw new Error( 'params.menu_item_id was not defined' );
1059 this._setupControlToggle();
1060 this._setupReorderUI();
1061 this._setupUpdateUI();
1062 this._setupRemoveUI();
1063 this._setupLinksUI();
1064 this._setupTitleUI();
1068 * Show/hide the settings when clicking on the menu item handle.
1070 _setupControlToggle: function() {
1073 this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
1075 e.stopPropagation();
1076 var menuControl = control.getMenuControl();
1077 if ( menuControl.isReordering || menuControl.isSorting ) {
1080 control.toggleForm();
1085 * Set up the menu-item-reorder-nav
1087 _setupReorderUI: function() {
1088 var control = this, template, $reorderNav;
1090 template = wp.template( 'menu-item-reorder-nav' );
1092 // Add the menu item reordering elements to the menu item control.
1093 control.container.find( '.item-controls' ).after( template );
1095 // Handle clicks for up/down/left-right on the reorder nav.
1096 $reorderNav = control.container.find( '.menu-item-reorder-nav' );
1097 $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
1098 var moveBtn = $( this );
1101 var isMoveUp = moveBtn.is( '.menus-move-up' ),
1102 isMoveDown = moveBtn.is( '.menus-move-down' ),
1103 isMoveLeft = moveBtn.is( '.menus-move-left' ),
1104 isMoveRight = moveBtn.is( '.menus-move-right' );
1108 } else if ( isMoveDown ) {
1110 } else if ( isMoveLeft ) {
1112 } else if ( isMoveRight ) {
1113 control.moveRight();
1116 moveBtn.focus(); // Re-focus after the container was moved.
1121 * Set up event handlers for menu item updating.
1123 _setupUpdateUI: function() {
1125 settingValue = control.setting();
1127 control.elements = {};
1128 control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
1129 control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
1130 control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
1131 control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
1132 control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
1133 control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
1134 control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
1135 // @todo allow other elements, added by plugins, to be automatically picked up here; allow additional values to be added to setting array.
1137 _.each( control.elements, function( element, property ) {
1138 element.bind(function( value ) {
1139 if ( element.element.is( 'input[type=checkbox]' ) ) {
1140 value = ( value ) ? element.element.val() : '';
1143 var settingValue = control.setting();
1144 if ( settingValue && settingValue[ property ] !== value ) {
1145 settingValue = _.clone( settingValue );
1146 settingValue[ property ] = value;
1147 control.setting.set( settingValue );
1150 if ( settingValue ) {
1151 if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) {
1152 element.set( settingValue[ property ].join( ' ' ) );
1154 element.set( settingValue[ property ] );
1159 control.setting.bind(function( to, from ) {
1160 var itemId = control.params.menu_item_id,
1161 followingSiblingItemControls = [],
1162 childrenItemControls = [],
1165 if ( false === to ) {
1166 menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
1167 control.container.remove();
1169 _.each( menuControl.getMenuItemControls(), function( otherControl ) {
1170 if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
1171 followingSiblingItemControls.push( otherControl );
1172 } else if ( otherControl.setting().menu_item_parent === itemId ) {
1173 childrenItemControls.push( otherControl );
1177 // Shift all following siblings by the number of children this item has.
1178 _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
1179 var value = _.clone( followingSiblingItemControl.setting() );
1180 value.position += childrenItemControls.length;
1181 followingSiblingItemControl.setting.set( value );
1184 // Now move the children up to be the new subsequent siblings.
1185 _.each( childrenItemControls, function( childrenItemControl, i ) {
1186 var value = _.clone( childrenItemControl.setting() );
1187 value.position = from.position + i;
1188 value.menu_item_parent = from.menu_item_parent;
1189 childrenItemControl.setting.set( value );
1192 menuControl.debouncedReflowMenuItems();
1194 // Update the elements' values to match the new setting properties.
1195 _.each( to, function( value, key ) {
1196 if ( control.elements[ key] ) {
1197 control.elements[ key ].set( to[ key ] );
1200 control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
1202 // Handle UI updates when the position or depth (parent) change.
1203 if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
1204 control.getMenuControl().debouncedReflowMenuItems();
1211 * Set up event handlers for menu item deletion.
1213 _setupRemoveUI: function() {
1214 var control = this, $removeBtn;
1216 // Configure delete button.
1217 $removeBtn = control.container.find( '.item-delete' );
1219 $removeBtn.on( 'click', function() {
1220 // Find an adjacent element to add focus to when this menu item goes away
1221 var addingItems = true, $adjacentFocusTarget, $next, $prev;
1223 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
1224 addingItems = false;
1227 $next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first();
1228 $prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first();
1230 if ( $next.length ) {
1231 $adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
1232 } else if ( $prev.length ) {
1233 $adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
1235 $adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first();
1238 control.container.slideUp( function() {
1239 control.setting.set( false );
1240 wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
1241 $adjacentFocusTarget.focus(); // keyboard accessibility
1246 _setupLinksUI: function() {
1249 // Configure original link.
1250 $origBtn = this.container.find( 'a.original-link' );
1252 $origBtn.on( 'click', function( e ) {
1254 api.previewer.previewUrl( e.target.toString() );
1259 * Update item handle title when changed.
1261 _setupTitleUI: function() {
1264 control.setting.bind( function( item ) {
1269 var titleEl = control.container.find( '.menu-item-title' ),
1270 titleText = item.title || api.Menus.data.l10n.untitled;
1272 if ( item._invalid ) {
1273 titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText );
1276 // Don't update to an empty title.
1280 .removeClass( 'no-title' );
1284 .addClass( 'no-title' );
1293 getDepth: function() {
1294 var control = this, setting = control.setting(), depth = 0;
1298 while ( setting && setting.menu_item_parent ) {
1300 control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
1304 setting = control.setting();
1310 * Amend the control's params with the data necessary for the JS template just in time.
1312 renderContent: function() {
1314 settingValue = control.setting(),
1317 control.params.title = settingValue.title || '';
1318 control.params.depth = control.getDepth();
1319 control.container.data( 'item-depth', control.params.depth );
1320 containerClasses = [
1322 'menu-item-depth-' + String( control.params.depth ),
1323 'menu-item-' + settingValue.object,
1324 'menu-item-edit-inactive'
1327 if ( settingValue._invalid ) {
1328 containerClasses.push( 'menu-item-invalid' );
1329 control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title );
1330 } else if ( 'draft' === settingValue.status ) {
1331 containerClasses.push( 'pending' );
1332 control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
1335 control.params.el_classes = containerClasses.join( ' ' );
1336 control.params.item_type_label = settingValue.type_label;
1337 control.params.item_type = settingValue.type;
1338 control.params.url = settingValue.url;
1339 control.params.target = settingValue.target;
1340 control.params.attr_title = settingValue.attr_title;
1341 control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
1342 control.params.attr_title = settingValue.attr_title;
1343 control.params.xfn = settingValue.xfn;
1344 control.params.description = settingValue.description;
1345 control.params.parent = settingValue.menu_item_parent;
1346 control.params.original_title = settingValue.original_title || '';
1348 control.container.addClass( control.params.el_classes );
1350 api.Control.prototype.renderContent.call( control );
1353 /***********************************************************************
1354 * Begin public API methods
1355 **********************************************************************/
1358 * @return {wp.customize.controlConstructor.nav_menu|null}
1360 getMenuControl: function() {
1361 var control = this, settingValue = control.setting();
1362 if ( settingValue && settingValue.nav_menu_term_id ) {
1363 return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
1370 * Expand the accordion section containing a control
1372 expandControlSection: function() {
1373 var $section = this.container.closest( '.accordion-section' );
1375 if ( ! $section.hasClass( 'open' ) ) {
1376 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
1383 * @param {Boolean} expanded
1384 * @param {Object} [params]
1385 * @returns {Boolean} false if state already applied
1387 _toggleExpanded: api.Section.prototype._toggleExpanded,
1392 * @param {Object} [params]
1393 * @returns {Boolean} false if already expanded
1395 expand: api.Section.prototype.expand,
1398 * Expand the menu item form control.
1400 * @since 4.5.0 Added params.completeCallback.
1402 * @param {Object} [params] - Optional params.
1403 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
1405 expandForm: function( params ) {
1406 this.expand( params );
1412 * @param {Object} [params]
1413 * @returns {Boolean} false if already collapsed
1415 collapse: api.Section.prototype.collapse,
1418 * Collapse the menu item form control.
1420 * @since 4.5.0 Added params.completeCallback.
1422 * @param {Object} [params] - Optional params.
1423 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
1425 collapseForm: function( params ) {
1426 this.collapse( params );
1430 * Expand or collapse the menu item control.
1432 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
1433 * @since 4.5.0 Added params.completeCallback.
1435 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
1436 * @param {Object} [params] - Optional params.
1437 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
1439 toggleForm: function( showOrHide, params ) {
1440 if ( typeof showOrHide === 'undefined' ) {
1441 showOrHide = ! this.expanded();
1444 this.expand( params );
1446 this.collapse( params );
1451 * Expand or collapse the menu item control.
1454 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
1455 * @param {Object} [params] - Optional params.
1456 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
1458 onChangeExpanded: function( showOrHide, params ) {
1459 var self = this, $menuitem, $inside, complete;
1461 $menuitem = this.container;
1462 $inside = $menuitem.find( '.menu-item-settings:first' );
1463 if ( 'undefined' === typeof showOrHide ) {
1464 showOrHide = ! $inside.is( ':visible' );
1467 // Already expanded or collapsed.
1468 if ( $inside.is( ':visible' ) === showOrHide ) {
1469 if ( params && params.completeCallback ) {
1470 params.completeCallback();
1476 // Close all other menu item controls before expanding this one.
1477 api.control.each( function( otherControl ) {
1478 if ( self.params.type === otherControl.params.type && self !== otherControl ) {
1479 otherControl.collapseForm();
1483 complete = function() {
1485 .removeClass( 'menu-item-edit-inactive' )
1486 .addClass( 'menu-item-edit-active' );
1487 self.container.trigger( 'expanded' );
1489 if ( params && params.completeCallback ) {
1490 params.completeCallback();
1494 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' );
1495 $inside.slideDown( 'fast', complete );
1497 self.container.trigger( 'expand' );
1499 complete = function() {
1501 .addClass( 'menu-item-edit-inactive' )
1502 .removeClass( 'menu-item-edit-active' );
1503 self.container.trigger( 'collapsed' );
1505 if ( params && params.completeCallback ) {
1506 params.completeCallback();
1510 self.container.trigger( 'collapse' );
1512 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' );
1513 $inside.slideUp( 'fast', complete );
1518 * Expand the containing menu section, expand the form, and focus on
1519 * the first input in the control.
1521 * @since 4.5.0 Added params.completeCallback.
1523 * @param {Object} [params] - Params object.
1524 * @param {Function} [params.completeCallback] - Optional callback function when focus has completed.
1526 focus: function( params ) {
1527 params = params || {};
1528 var control = this, originalCompleteCallback = params.completeCallback;
1530 control.expandControlSection();
1532 params.completeCallback = function() {
1535 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
1536 focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' );
1537 focusable.first().focus();
1539 if ( originalCompleteCallback ) {
1540 originalCompleteCallback();
1544 control.expandForm( params );
1548 * Move menu item up one in the menu.
1550 moveUp: function() {
1551 this._changePosition( -1 );
1552 wp.a11y.speak( api.Menus.data.l10n.movedUp );
1556 * Move menu item up one in the menu.
1558 moveDown: function() {
1559 this._changePosition( 1 );
1560 wp.a11y.speak( api.Menus.data.l10n.movedDown );
1563 * Move menu item and all children up one level of depth.
1565 moveLeft: function() {
1566 this._changeDepth( -1 );
1567 wp.a11y.speak( api.Menus.data.l10n.movedLeft );
1571 * Move menu item and children one level deeper, as a submenu of the previous item.
1573 moveRight: function() {
1574 this._changeDepth( 1 );
1575 wp.a11y.speak( api.Menus.data.l10n.movedRight );
1579 * Note that this will trigger a UI update, causing child items to
1580 * move as well and cardinal order class names to be updated.
1584 * @param {Number} offset 1|-1
1586 _changePosition: function( offset ) {
1589 settingValue = _.clone( control.setting() ),
1590 siblingSettings = [],
1593 if ( 1 !== offset && -1 !== offset ) {
1594 throw new Error( 'Offset changes by 1 are only supported.' );
1597 // Skip moving deleted items.
1598 if ( ! control.setting() ) {
1602 // Locate the other items under the same parent (siblings).
1603 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
1604 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
1605 siblingSettings.push( otherControl.setting );
1608 siblingSettings.sort(function( a, b ) {
1609 return a().position - b().position;
1612 realPosition = _.indexOf( siblingSettings, control.setting );
1613 if ( -1 === realPosition ) {
1614 throw new Error( 'Expected setting to be among siblings.' );
1617 // Skip doing anything if the item is already at the edge in the desired direction.
1618 if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
1619 // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
1623 // Update any adjacent menu item setting to take on this item's position.
1624 adjacentSetting = siblingSettings[ realPosition + offset ];
1625 if ( adjacentSetting ) {
1626 adjacentSetting.set( $.extend(
1627 _.clone( adjacentSetting() ),
1629 position: settingValue.position
1634 settingValue.position += offset;
1635 control.setting.set( settingValue );
1639 * Note that this will trigger a UI update, causing child items to
1640 * move as well and cardinal order class names to be updated.
1644 * @param {Number} offset 1|-1
1646 _changeDepth: function( offset ) {
1647 if ( 1 !== offset && -1 !== offset ) {
1648 throw new Error( 'Offset changes by 1 are only supported.' );
1651 settingValue = _.clone( control.setting() ),
1652 siblingControls = [],
1657 // Locate the other items under the same parent (siblings).
1658 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
1659 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
1660 siblingControls.push( otherControl );
1663 siblingControls.sort(function( a, b ) {
1664 return a.setting().position - b.setting().position;
1667 realPosition = _.indexOf( siblingControls, control );
1668 if ( -1 === realPosition ) {
1669 throw new Error( 'Expected control to be among siblings.' );
1672 if ( -1 === offset ) {
1673 // Skip moving left an item that is already at the top level.
1674 if ( ! settingValue.menu_item_parent ) {
1678 parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' );
1680 // Make this control the parent of all the following siblings.
1681 _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) {
1682 siblingControl.setting.set(
1685 siblingControl.setting(),
1687 menu_item_parent: control.params.menu_item_id,
1694 // Increase the positions of the parent item's subsequent children to make room for this one.
1695 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
1696 var otherControlSettingValue, isControlToBeShifted;
1697 isControlToBeShifted = (
1698 otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent &&
1699 otherControl.setting().position > parentControl.setting().position
1701 if ( isControlToBeShifted ) {
1702 otherControlSettingValue = _.clone( otherControl.setting() );
1703 otherControl.setting.set(
1705 otherControlSettingValue,
1706 { position: otherControlSettingValue.position + 1 }
1712 // Make this control the following sibling of its parent item.
1713 settingValue.position = parentControl.setting().position + 1;
1714 settingValue.menu_item_parent = parentControl.setting().menu_item_parent;
1715 control.setting.set( settingValue );
1717 } else if ( 1 === offset ) {
1718 // Skip moving right an item that doesn't have a previous sibling.
1719 if ( realPosition === 0 ) {
1723 // Make the control the last child of the previous sibling.
1724 siblingControl = siblingControls[ realPosition - 1 ];
1725 settingValue.menu_item_parent = siblingControl.params.menu_item_id;
1726 settingValue.position = 0;
1727 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
1728 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
1729 settingValue.position = Math.max( settingValue.position, otherControl.setting().position );
1732 settingValue.position += 1;
1733 control.setting.set( settingValue );
1739 * wp.customize.Menus.MenuNameControl
1741 * Customizer control for a nav menu's name.
1744 * @augments wp.customize.Control
1746 api.Menus.MenuNameControl = api.Control.extend({
1750 settingValue = control.setting();
1753 * Since the control is not registered in PHP, we need to prevent the
1754 * preview's sending of the activeControls to result in this control
1755 * being deactivated.
1757 control.active.validate = function() {
1758 var value, section = api.section( control.section() );
1760 value = section.active();
1767 control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) );
1769 control.nameElement.bind(function( value ) {
1770 var settingValue = control.setting();
1771 if ( settingValue && settingValue.name !== value ) {
1772 settingValue = _.clone( settingValue );
1773 settingValue.name = value;
1774 control.setting.set( settingValue );
1777 if ( settingValue ) {
1778 control.nameElement.set( settingValue.name );
1781 control.setting.bind(function( object ) {
1783 control.nameElement.set( object.name );
1791 * wp.customize.Menus.MenuAutoAddControl
1793 * Customizer control for a nav menu's auto add.
1796 * @augments wp.customize.Control
1798 api.Menus.MenuAutoAddControl = api.Control.extend({
1802 settingValue = control.setting();
1805 * Since the control is not registered in PHP, we need to prevent the
1806 * preview's sending of the activeControls to result in this control
1807 * being deactivated.
1809 control.active.validate = function() {
1810 var value, section = api.section( control.section() );
1812 value = section.active();
1819 control.autoAddElement = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) );
1821 control.autoAddElement.bind(function( value ) {
1822 var settingValue = control.setting();
1823 if ( settingValue && settingValue.name !== value ) {
1824 settingValue = _.clone( settingValue );
1825 settingValue.auto_add = value;
1826 control.setting.set( settingValue );
1829 if ( settingValue ) {
1830 control.autoAddElement.set( settingValue.auto_add );
1833 control.setting.bind(function( object ) {
1835 control.autoAddElement.set( object.auto_add );
1843 * wp.customize.Menus.MenuControl
1845 * Customizer control for menus.
1846 * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type
1849 * @augments wp.customize.Control
1851 api.Menus.MenuControl = api.Control.extend({
1853 * Set up the control.
1857 menuId = control.params.menu_id,
1858 menu = control.setting(),
1863 if ( 'undefined' === typeof this.params.menu_id ) {
1864 throw new Error( 'params.menu_id was not defined' );
1868 * Since the control is not registered in PHP, we need to prevent the
1869 * preview's sending of the activeControls to result in this control
1870 * being deactivated.
1872 control.active.validate = function() {
1873 var value, section = api.section( control.section() );
1875 value = section.active();
1882 control.$controlSection = control.container.closest( '.control-section' );
1883 control.$sectionContent = control.container.closest( '.accordion-section-content' );
1887 api.section( control.section(), function( section ) {
1888 section.deferred.initSortables.done(function( menuList ) {
1889 control._setupSortable( menuList );
1893 this._setupAddition();
1894 this._setupLocations();
1897 // Add menu to Custom Menu widgets.
1899 name = displayNavMenuName( menu.name );
1901 // Add the menu to the existing controls.
1902 api.control.each( function( widgetControl ) {
1903 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
1906 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).show();
1907 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).hide();
1909 select = widgetControl.container.find( 'select' );
1910 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
1911 select.append( new Option( name, menuId ) );
1915 // Add the menu to the widget template.
1916 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
1917 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).show();
1918 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).hide();
1919 select = widgetTemplate.find( '.widget-inside select:first' );
1920 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
1921 select.append( new Option( name, menuId ) );
1927 * Update ordering of menu item controls when the setting is updated.
1929 _setupModel: function() {
1931 menuId = control.params.menu_id;
1933 control.setting.bind( function( to ) {
1935 if ( false === to ) {
1936 control._handleDeletion();
1938 // Update names in the Custom Menu widgets.
1939 name = displayNavMenuName( to.name );
1940 api.control.each( function( widgetControl ) {
1941 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
1944 var select = widgetControl.container.find( 'select' );
1945 select.find( 'option[value=' + String( menuId ) + ']' ).text( name );
1950 control.container.find( '.menu-delete' ).on( 'click', function( event ) {
1951 event.stopPropagation();
1952 event.preventDefault();
1953 control.setting.set( false );
1958 * Allow items in each menu to be re-ordered, and for the order to be previewed.
1960 * Notice that the UI aspects here are handled by wpNavMenu.initSortables()
1961 * which is called in MenuSection.onChangeExpanded()
1963 * @param {object} menuList - The element that has sortable().
1965 _setupSortable: function( menuList ) {
1968 if ( ! menuList.is( control.$sectionContent ) ) {
1969 throw new Error( 'Unexpected menuList.' );
1972 menuList.on( 'sortstart', function() {
1973 control.isSorting = true;
1976 menuList.on( 'sortstop', function() {
1977 setTimeout( function() { // Next tick.
1978 var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ),
1979 menuItemControls = [],
1983 control.isSorting = false;
1985 // Reset horizontal scroll position when done dragging.
1986 control.$sectionContent.scrollLeft( 0 );
1988 _.each( menuItemContainerIds, function( menuItemContainerId ) {
1989 var menuItemId, menuItemControl, matches;
1990 matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
1994 menuItemId = parseInt( matches[1], 10 );
1995 menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
1996 if ( menuItemControl ) {
1997 menuItemControls.push( menuItemControl );
2001 _.each( menuItemControls, function( menuItemControl ) {
2002 if ( false === menuItemControl.setting() ) {
2003 // Skip deleted items.
2006 var setting = _.clone( menuItemControl.setting() );
2009 setting.position = position;
2010 menuItemControl.priority( priority );
2012 // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value.
2013 setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 );
2014 if ( ! setting.menu_item_parent ) {
2015 setting.menu_item_parent = 0;
2018 menuItemControl.setting.set( setting );
2023 control.isReordering = false;
2026 * Keyboard-accessible reordering.
2028 this.container.find( '.reorder-toggle' ).on( 'click', function() {
2029 control.toggleReordering( ! control.isReordering );
2034 * Set up UI for adding a new menu item.
2036 _setupAddition: function() {
2039 this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) {
2040 if ( self.$sectionContent.hasClass( 'reordering' ) ) {
2044 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
2045 $( this ).attr( 'aria-expanded', 'true' );
2046 api.Menus.availableMenuItemsPanel.open( self );
2048 $( this ).attr( 'aria-expanded', 'false' );
2049 api.Menus.availableMenuItemsPanel.close();
2050 event.stopPropagation();
2055 _handleDeletion: function() {
2058 menuId = control.params.menu_id,
2062 section = api.section( control.section() );
2063 removeSection = function() {
2064 section.container.remove();
2065 api.section.remove( section.id );
2068 if ( section && section.expanded() ) {
2070 completeCallback: function() {
2072 wp.a11y.speak( api.Menus.data.l10n.menuDeleted );
2073 api.panel( 'nav_menus' ).focus();
2080 api.each(function( setting ) {
2081 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
2086 // Remove the menu from any Custom Menu widgets.
2087 api.control.each(function( widgetControl ) {
2088 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2091 var select = widgetControl.container.find( 'select' );
2092 if ( select.val() === String( menuId ) ) {
2093 select.prop( 'selectedIndex', 0 ).trigger( 'change' );
2096 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2097 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2098 widgetControl.container.find( 'option[value=' + String( menuId ) + ']' ).remove();
2101 // Remove the menu to the nav menu widget template.
2102 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
2103 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2104 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2105 widgetTemplate.find( 'option[value=' + String( menuId ) + ']' ).remove();
2108 // Setup theme location checkboxes.
2109 _setupLocations: function() {
2112 control.container.find( '.assigned-menu-location' ).each(function() {
2113 var container = $( this ),
2114 checkbox = container.find( 'input[type=checkbox]' ),
2116 updateSelectedMenuLabel,
2117 navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
2119 updateSelectedMenuLabel = function( selectedMenuId ) {
2120 var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' );
2121 if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
2122 container.find( '.theme-location-set' ).hide();
2124 container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) );
2128 element = new api.Element( checkbox );
2129 element.set( navMenuLocationSetting.get() === control.params.menu_id );
2131 checkbox.on( 'change', function() {
2132 // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well.
2133 navMenuLocationSetting.set( this.checked ? control.params.menu_id : 0 );
2136 navMenuLocationSetting.bind(function( selectedMenuId ) {
2137 element.set( selectedMenuId === control.params.menu_id );
2138 updateSelectedMenuLabel( selectedMenuId );
2140 updateSelectedMenuLabel( navMenuLocationSetting.get() );
2146 * Update Section Title as menu name is changed.
2148 _setupTitle: function() {
2151 control.setting.bind( function( menu ) {
2156 var section = control.container.closest( '.accordion-section' ),
2157 menuId = control.params.menu_id,
2158 controlTitle = section.find( '.accordion-section-title' ),
2159 sectionTitle = section.find( '.customize-section-title h3' ),
2160 location = section.find( '.menu-in-location' ),
2161 action = sectionTitle.find( '.customize-action' ),
2162 name = displayNavMenuName( menu.name );
2164 // Update the control title
2165 controlTitle.text( name );
2166 if ( location.length ) {
2167 location.appendTo( controlTitle );
2170 // Update the section title
2171 sectionTitle.text( name );
2172 if ( action.length ) {
2173 action.prependTo( sectionTitle );
2176 // Update the nav menu name in location selects.
2177 api.control.each( function( control ) {
2178 if ( /^nav_menu_locations\[/.test( control.id ) ) {
2179 control.container.find( 'option[value=' + menuId + ']' ).text( name );
2183 // Update the nav menu name in all location checkboxes.
2184 section.find( '.customize-control-checkbox input' ).each( function() {
2185 if ( $( this ).prop( 'checked' ) ) {
2186 $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name );
2192 /***********************************************************************
2193 * Begin public API methods
2194 **********************************************************************/
2197 * Enable/disable the reordering UI
2199 * @param {Boolean} showOrHide to enable/disable reordering
2201 toggleReordering: function( showOrHide ) {
2202 var addNewItemBtn = this.container.find( '.add-new-menu-item' ),
2203 reorderBtn = this.container.find( '.reorder-toggle' ),
2204 itemsTitle = this.$sectionContent.find( '.item-title' );
2206 showOrHide = Boolean( showOrHide );
2208 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
2212 this.isReordering = showOrHide;
2213 this.$sectionContent.toggleClass( 'reordering', showOrHide );
2214 this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' );
2215 if ( this.isReordering ) {
2216 addNewItemBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
2217 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOff );
2218 wp.a11y.speak( api.Menus.data.l10n.reorderModeOn );
2219 itemsTitle.attr( 'aria-hidden', 'false' );
2221 addNewItemBtn.removeAttr( 'tabindex aria-hidden' );
2222 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOn );
2223 wp.a11y.speak( api.Menus.data.l10n.reorderModeOff );
2224 itemsTitle.attr( 'aria-hidden', 'true' );
2228 _( this.getMenuItemControls() ).each( function( formControl ) {
2229 formControl.collapseForm();
2235 * @return {wp.customize.controlConstructor.nav_menu_item[]}
2237 getMenuItemControls: function() {
2238 var menuControl = this,
2239 menuItemControls = [],
2240 menuTermId = menuControl.params.menu_id;
2242 api.control.each(function( control ) {
2243 if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) {
2244 menuItemControls.push( control );
2248 return menuItemControls;
2252 * Make sure that each menu item control has the proper depth.
2254 reflowMenuItems: function() {
2255 var menuControl = this,
2256 menuItemControls = menuControl.getMenuItemControls(),
2259 reflowRecursively = function( context ) {
2260 var currentMenuItemControls = [],
2261 thisParent = context.currentParent;
2262 _.each( context.menuItemControls, function( menuItemControl ) {
2263 if ( thisParent === menuItemControl.setting().menu_item_parent ) {
2264 currentMenuItemControls.push( menuItemControl );
2265 // @todo We could remove this item from menuItemControls now, for efficiency.
2268 currentMenuItemControls.sort( function( a, b ) {
2269 return a.setting().position - b.setting().position;
2272 _.each( currentMenuItemControls, function( menuItemControl ) {
2274 context.currentAbsolutePosition += 1;
2275 menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order.
2278 if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) {
2279 _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) {
2280 menuItemControl.container.removeClass( className );
2282 menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) );
2284 menuItemControl.container.data( 'item-depth', context.currentDepth );
2286 // Process any children items.
2287 context.currentDepth += 1;
2288 context.currentParent = menuItemControl.params.menu_item_id;
2289 reflowRecursively( context );
2290 context.currentDepth -= 1;
2291 context.currentParent = thisParent;
2294 // Update class names for reordering controls.
2295 if ( currentMenuItemControls.length ) {
2296 _( currentMenuItemControls ).each(function( menuItemControl ) {
2297 menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' );
2298 if ( 0 === context.currentDepth ) {
2299 menuItemControl.container.addClass( 'move-left-disabled' );
2300 } else if ( 10 === context.currentDepth ) {
2301 menuItemControl.container.addClass( 'move-right-disabled' );
2305 currentMenuItemControls[0].container
2306 .addClass( 'move-up-disabled' )
2307 .addClass( 'move-right-disabled' )
2308 .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length );
2309 currentMenuItemControls[ currentMenuItemControls.length - 1 ].container
2310 .addClass( 'move-down-disabled' )
2311 .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length );
2315 reflowRecursively( {
2316 menuItemControls: menuItemControls,
2319 currentAbsolutePosition: 0
2322 menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 );
2326 * Note that this function gets debounced so that when a lot of setting
2327 * changes are made at once, for instance when moving a menu item that
2328 * has child items, this function will only be called once all of the
2329 * settings have been updated.
2331 debouncedReflowMenuItems: _.debounce( function() {
2332 this.reflowMenuItems.apply( this, arguments );
2336 * Add a new item to this menu.
2338 * @param {object} item - Value for the nav_menu_item setting to be created.
2339 * @returns {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance.
2341 addItemToMenu: function( item ) {
2342 var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10;
2344 _.each( menuControl.getMenuItemControls(), function( control ) {
2345 if ( false === control.setting() ) {
2348 priority = Math.max( priority, control.priority() );
2349 if ( 0 === control.setting().menu_item_parent ) {
2350 position = Math.max( position, control.setting().position );
2358 api.Menus.data.defaultSettingValues.nav_menu_item,
2361 nav_menu_term_id: menuControl.params.menu_id,
2362 original_title: item.title,
2366 delete item.id; // only used by Backbone
2368 placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
2369 customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
2371 type: 'nav_menu_item',
2372 transport: api.Menus.data.settingTransport,
2373 previewer: api.previewer
2375 setting = api.create( customizeId, customizeId, {}, settingArgs );
2376 setting.set( item ); // Change from initial empty object to actual item to mark as dirty.
2378 // Add the menu item control.
2379 menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, {
2381 type: 'nav_menu_item',
2382 content: '<li id="customize-control-nav_menu_item-' + String( placeholderId ) + '" class="customize-control customize-control-nav_menu_item"></li>',
2383 section: menuControl.id,
2387 'default': customizeId
2389 menu_item_id: placeholderId
2391 previewer: api.previewer
2394 api.control.add( customizeId, menuItemControl );
2396 menuControl.debouncedReflowMenuItems();
2398 wp.a11y.speak( api.Menus.data.l10n.itemAdded );
2400 return menuItemControl;
2405 * wp.customize.Menus.NewMenuControl
2407 * Customizer control for creating new menus and handling deletion of existing menus.
2408 * Note that 'new_menu' must match the WP_Customize_New_Menu_Control::$type.
2411 * @augments wp.customize.Control
2413 api.Menus.NewMenuControl = api.Control.extend({
2415 * Set up the control.
2418 this._bindHandlers();
2421 _bindHandlers: function() {
2423 name = $( '#customize-control-new_menu_name input' ),
2424 submit = $( '#create-new-menu-submit' );
2425 name.on( 'keydown', function( event ) {
2426 if ( 13 === event.which ) { // Enter.
2430 submit.on( 'click', function( event ) {
2432 event.stopPropagation();
2433 event.preventDefault();
2438 * Create the new menu with the name supplied.
2440 submit: function() {
2443 container = control.container.closest( '.accordion-section-new-menu' ),
2444 nameInput = container.find( '.menu-name-field' ).first(),
2445 name = nameInput.val(),
2448 placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
2451 nameInput.addClass( 'invalid' );
2456 customizeId = 'nav_menu[' + String( placeholderId ) + ']';
2458 // Register the menu control setting.
2459 api.create( customizeId, customizeId, {}, {
2461 transport: api.Menus.data.settingTransport,
2462 previewer: api.previewer
2464 api( customizeId ).set( $.extend(
2466 api.Menus.data.defaultSettingValues.nav_menu,
2473 * Add the menu section (and its controls).
2474 * Note that this will automatically create the required controls
2475 * inside via the Section's ready method.
2477 menuSection = new api.Menus.MenuSection( customizeId, {
2481 title: displayNavMenuName( name ),
2482 customizeAction: api.Menus.data.l10n.customizingMenus,
2485 menu_id: placeholderId
2488 api.section.add( customizeId, menuSection );
2490 // Clear name field.
2491 nameInput.val( '' );
2492 nameInput.removeClass( 'invalid' );
2494 wp.a11y.speak( api.Menus.data.l10n.menuAdded );
2496 // Focus on the new menu section.
2497 api.section( customizeId ).focus(); // @todo should we focus on the new menu's control and open the add-items panel? Thinking user flow...
2499 // Fix an issue with extra space at top immediately after creating new menu.
2500 $( '#menu-to-edit' ).css( 'margin-top', 0 );
2505 * Extends wp.customize.controlConstructor with control constructor for
2506 * menu_location, menu_item, nav_menu, and new_menu.
2508 $.extend( api.controlConstructor, {
2509 nav_menu_location: api.Menus.MenuLocationControl,
2510 nav_menu_item: api.Menus.MenuItemControl,
2511 nav_menu: api.Menus.MenuControl,
2512 nav_menu_name: api.Menus.MenuNameControl,
2513 nav_menu_auto_add: api.Menus.MenuAutoAddControl,
2514 new_menu: api.Menus.NewMenuControl
2518 * Extends wp.customize.panelConstructor with section constructor for menus.
2520 $.extend( api.panelConstructor, {
2521 nav_menus: api.Menus.MenusPanel
2525 * Extends wp.customize.sectionConstructor with section constructor for menu.
2527 $.extend( api.sectionConstructor, {
2528 nav_menu: api.Menus.MenuSection,
2529 new_menu: api.Menus.NewMenuSection
2533 * Init Customizer for menus.
2535 api.bind( 'ready', function() {
2537 // Set up the menu items panel.
2538 api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({
2539 collection: api.Menus.availableMenuItems
2542 api.bind( 'saved', function( data ) {
2543 if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
2544 api.Menus.applySavedData( data );
2548 // Open and focus menu control.
2549 api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl );
2553 * When customize_save comes back with a success, make sure any inserted
2554 * nav menus and items are properly re-added with their newly-assigned IDs.
2556 * @param {object} data
2557 * @param {array} data.nav_menu_updates
2558 * @param {array} data.nav_menu_item_updates
2560 api.Menus.applySavedData = function( data ) {
2562 var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {};
2564 _( data.nav_menu_updates ).each(function( update ) {
2565 var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount;
2566 if ( 'inserted' === update.status ) {
2567 if ( ! update.previous_term_id ) {
2568 throw new Error( 'Expected previous_term_id' );
2570 if ( ! update.term_id ) {
2571 throw new Error( 'Expected term_id' );
2573 oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']';
2574 if ( ! api.has( oldCustomizeId ) ) {
2575 throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
2577 oldSetting = api( oldCustomizeId );
2578 if ( ! api.section.has( oldCustomizeId ) ) {
2579 throw new Error( 'Expected control to exist: ' + oldCustomizeId );
2581 oldSection = api.section( oldCustomizeId );
2583 settingValue = oldSetting.get();
2584 if ( ! settingValue ) {
2585 throw new Error( 'Did not expect setting to be empty (deleted).' );
2587 settingValue = $.extend( _.clone( settingValue ), update.saved_value );
2589 insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
2590 newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
2591 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
2593 transport: api.Menus.data.settingTransport,
2594 previewer: api.previewer
2597 if ( oldSection.expanded() ) {
2598 oldSection.collapse();
2601 // Add the menu section.
2602 newSection = new api.Menus.MenuSection( newCustomizeId, {
2606 title: settingValue.name,
2607 customizeAction: api.Menus.data.l10n.customizingMenus,
2609 priority: oldSection.priority.get(),
2611 menu_id: update.term_id
2615 // Add new control for the new menu.
2616 api.section.add( newCustomizeId, newSection );
2618 // Update the values for nav menus in Custom Menu controls.
2619 api.control.each( function( setting ) {
2620 if ( ! setting.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== setting.params.widget_id_base ) {
2623 var select, oldMenuOption, newMenuOption;
2624 select = setting.container.find( 'select' );
2625 oldMenuOption = select.find( 'option[value=' + String( update.previous_term_id ) + ']' );
2626 newMenuOption = select.find( 'option[value=' + String( update.term_id ) + ']' );
2627 newMenuOption.prop( 'selected', oldMenuOption.prop( 'selected' ) );
2628 oldMenuOption.remove();
2631 // Delete the old placeholder nav_menu.
2632 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
2633 oldSetting.set( false );
2634 oldSetting.preview();
2635 newSetting.preview();
2636 oldSetting._dirty = false;
2638 // Remove nav_menu section.
2639 oldSection.container.remove();
2640 api.section.remove( oldCustomizeId );
2642 // Update the nav_menu widget to reflect removed placeholder menu.
2644 api.each(function( setting ) {
2645 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
2649 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
2650 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2651 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2652 widgetTemplate.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
2654 // Update the nav_menu_locations[...] controls to remove the placeholder menus from the dropdown options.
2655 wp.customize.control.each(function( control ){
2656 if ( /^nav_menu_locations\[/.test( control.id ) ) {
2657 control.container.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
2661 // Update nav_menu_locations to reference the new ID.
2662 api.each( function( setting ) {
2663 var wasSaved = api.state( 'saved' ).get();
2664 if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) {
2665 setting.set( update.term_id );
2666 setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update().
2667 api.state( 'saved' ).set( wasSaved );
2672 if ( oldSection.expanded.get() ) {
2673 // @todo This doesn't seem to be working.
2674 newSection.expand();
2676 } else if ( 'updated' === update.status ) {
2677 customizeId = 'nav_menu[' + String( update.term_id ) + ']';
2678 if ( ! api.has( customizeId ) ) {
2679 throw new Error( 'Expected setting to exist: ' + customizeId );
2682 // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name).
2683 setting = api( customizeId );
2684 if ( ! _.isEqual( update.saved_value, setting.get() ) ) {
2685 wasSaved = api.state( 'saved' ).get();
2686 setting.set( update.saved_value );
2687 setting._dirty = false;
2688 api.state( 'saved' ).set( wasSaved );
2693 // Build up mapping of nav_menu_item placeholder IDs to inserted IDs.
2694 _( data.nav_menu_item_updates ).each(function( update ) {
2695 if ( update.previous_post_id ) {
2696 insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id;
2700 _( data.nav_menu_item_updates ).each(function( update ) {
2701 var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl;
2702 if ( 'inserted' === update.status ) {
2703 if ( ! update.previous_post_id ) {
2704 throw new Error( 'Expected previous_post_id' );
2706 if ( ! update.post_id ) {
2707 throw new Error( 'Expected post_id' );
2709 oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']';
2710 if ( ! api.has( oldCustomizeId ) ) {
2711 throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
2713 oldSetting = api( oldCustomizeId );
2714 if ( ! api.control.has( oldCustomizeId ) ) {
2715 throw new Error( 'Expected control to exist: ' + oldCustomizeId );
2717 oldControl = api.control( oldCustomizeId );
2719 settingValue = oldSetting.get();
2720 if ( ! settingValue ) {
2721 throw new Error( 'Did not expect setting to be empty (deleted).' );
2723 settingValue = _.clone( settingValue );
2725 // If the parent menu item was also inserted, update the menu_item_parent to the new ID.
2726 if ( settingValue.menu_item_parent < 0 ) {
2727 if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) {
2728 throw new Error( 'inserted ID for menu_item_parent not available' );
2730 settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ];
2733 // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id.
2734 if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) {
2735 settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ];
2738 newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
2739 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
2740 type: 'nav_menu_item',
2741 transport: api.Menus.data.settingTransport,
2742 previewer: api.previewer
2745 // Add the menu control.
2746 newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, {
2748 type: 'nav_menu_item',
2749 content: '<li id="customize-control-nav_menu_item-' + String( update.post_id ) + '" class="customize-control customize-control-nav_menu_item"></li>',
2750 menu_id: update.post_id,
2751 section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']',
2752 priority: oldControl.priority.get(),
2755 'default': newCustomizeId
2757 menu_item_id: update.post_id
2759 previewer: api.previewer
2762 // Remove old control.
2763 oldControl.container.remove();
2764 api.control.remove( oldCustomizeId );
2766 // Add new control to take its place.
2767 api.control.add( newCustomizeId, newControl );
2769 // Delete the placeholder and preview the new setting.
2770 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
2771 oldSetting.set( false );
2772 oldSetting.preview();
2773 newSetting.preview();
2774 oldSetting._dirty = false;
2776 newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) );
2781 * Update the settings for any nav_menu widgets that had selected a placeholder ID.
2783 _.each( data.widget_nav_menu_updates, function( widgetSettingValue, widgetSettingId ) {
2784 var setting = api( widgetSettingId );
2786 setting._value = widgetSettingValue;
2787 setting.preview(); // Send to the preview now so that menu refresh will use the inserted menu.
2793 * Focus a menu item control.
2795 * @param {string} menuItemId
2797 api.Menus.focusMenuItemControl = function( menuItemId ) {
2798 var control = api.Menus.getMenuItemControl( menuItemId );
2806 * Get the control for a given menu.
2809 * @return {wp.customize.controlConstructor.menus[]}
2811 api.Menus.getMenuControl = function( menuId ) {
2812 return api.control( 'nav_menu[' + menuId + ']' );
2816 * Given a menu item ID, get the control associated with it.
2818 * @param {string} menuItemId
2819 * @return {object|null}
2821 api.Menus.getMenuItemControl = function( menuItemId ) {
2822 return api.control( menuItemIdToSettingId( menuItemId ) );
2826 * @param {String} menuItemId
2828 function menuItemIdToSettingId( menuItemId ) {
2829 return 'nav_menu_item[' + menuItemId + ']';
2833 * Apply sanitize_text_field()-like logic to the supplied name, returning a
2834 * "unnammed" fallback string if the name is then empty.
2836 * @param {string} name
2839 function displayNavMenuName( name ) {
2841 name = $( '<div>' ).text( name ).html(); // Emulate esc_html() which is used in wp-admin/nav-menus.php.
2842 name = $.trim( name );
2843 return name || api.Menus.data.l10n.unnamed;
2846 })( wp.customize, wp, jQuery );