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