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