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