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