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