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