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