[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * @output wp-admin/js/customize-nav-menus.js 3 */ 4 5 /* global _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( '' ).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' ).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 ( '' === $.trim( itemName.val() ) ) { 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.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( $.trim( $( this ).val() ) ); 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 trimmedTitle = $.trim( item.title ); 1892 1893 titleText = trimmedTitle || item.original_title || api.Menus.data.l10n.untitled; 1894 1895 if ( item._invalid ) { 1896 titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText ); 1897 } 1898 1899 // Don't update to an empty title. 1900 if ( trimmedTitle || item.original_title ) { 1901 titleEl 1902 .text( titleText ) 1903 .removeClass( 'no-title' ); 1904 } else { 1905 titleEl 1906 .text( titleText ) 1907 .addClass( 'no-title' ); 1908 } 1909 } ); 1910 }, 1911 1912 /** 1913 * 1914 * @return {number} 1915 */ 1916 getDepth: function() { 1917 var control = this, setting = control.setting(), depth = 0; 1918 if ( ! setting ) { 1919 return 0; 1920 } 1921 while ( setting && setting.menu_item_parent ) { 1922 depth += 1; 1923 control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' ); 1924 if ( ! control ) { 1925 break; 1926 } 1927 setting = control.setting(); 1928 } 1929 return depth; 1930 }, 1931 1932 /** 1933 * Amend the control's params with the data necessary for the JS template just in time. 1934 */ 1935 renderContent: function() { 1936 var control = this, 1937 settingValue = control.setting(), 1938 containerClasses; 1939 1940 control.params.title = settingValue.title || ''; 1941 control.params.depth = control.getDepth(); 1942 control.container.data( 'item-depth', control.params.depth ); 1943 containerClasses = [ 1944 'menu-item', 1945 'menu-item-depth-' + String( control.params.depth ), 1946 'menu-item-' + settingValue.object, 1947 'menu-item-edit-inactive' 1948 ]; 1949 1950 if ( settingValue._invalid ) { 1951 containerClasses.push( 'menu-item-invalid' ); 1952 control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title ); 1953 } else if ( 'draft' === settingValue.status ) { 1954 containerClasses.push( 'pending' ); 1955 control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title ); 1956 } 1957 1958 control.params.el_classes = containerClasses.join( ' ' ); 1959 control.params.item_type_label = settingValue.type_label; 1960 control.params.item_type = settingValue.type; 1961 control.params.url = settingValue.url; 1962 control.params.target = settingValue.target; 1963 control.params.attr_title = settingValue.attr_title; 1964 control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes; 1965 control.params.xfn = settingValue.xfn; 1966 control.params.description = settingValue.description; 1967 control.params.parent = settingValue.menu_item_parent; 1968 control.params.original_title = settingValue.original_title || ''; 1969 1970 control.container.addClass( control.params.el_classes ); 1971 1972 api.Control.prototype.renderContent.call( control ); 1973 }, 1974 1975 /*********************************************************************** 1976 * Begin public API methods 1977 **********************************************************************/ 1978 1979 /** 1980 * @return {wp.customize.controlConstructor.nav_menu|null} 1981 */ 1982 getMenuControl: function() { 1983 var control = this, settingValue = control.setting(); 1984 if ( settingValue && settingValue.nav_menu_term_id ) { 1985 return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' ); 1986 } else { 1987 return null; 1988 } 1989 }, 1990 1991 /** 1992 * Expand the accordion section containing a control 1993 */ 1994 expandControlSection: function() { 1995 var $section = this.container.closest( '.accordion-section' ); 1996 if ( ! $section.hasClass( 'open' ) ) { 1997 $section.find( '.accordion-section-title:first' ).trigger( 'click' ); 1998 } 1999 }, 2000 2001 /** 2002 * @since 4.6.0 2003 * 2004 * @param {Boolean} expanded 2005 * @param {Object} [params] 2006 * @return {Boolean} False if state already applied. 2007 */ 2008 _toggleExpanded: api.Section.prototype._toggleExpanded, 2009 2010 /** 2011 * @since 4.6.0 2012 * 2013 * @param {Object} [params] 2014 * @return {Boolean} False if already expanded. 2015 */ 2016 expand: api.Section.prototype.expand, 2017 2018 /** 2019 * Expand the menu item form control. 2020 * 2021 * @since 4.5.0 Added params.completeCallback. 2022 * 2023 * @param {Object} [params] - Optional params. 2024 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. 2025 */ 2026 expandForm: function( params ) { 2027 this.expand( params ); 2028 }, 2029 2030 /** 2031 * @since 4.6.0 2032 * 2033 * @param {Object} [params] 2034 * @return {Boolean} False if already collapsed. 2035 */ 2036 collapse: api.Section.prototype.collapse, 2037 2038 /** 2039 * Collapse the menu item form control. 2040 * 2041 * @since 4.5.0 Added params.completeCallback. 2042 * 2043 * @param {Object} [params] - Optional params. 2044 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. 2045 */ 2046 collapseForm: function( params ) { 2047 this.collapse( params ); 2048 }, 2049 2050 /** 2051 * Expand or collapse the menu item control. 2052 * 2053 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) 2054 * @since 4.5.0 Added params.completeCallback. 2055 * 2056 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility 2057 * @param {Object} [params] - Optional params. 2058 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. 2059 */ 2060 toggleForm: function( showOrHide, params ) { 2061 if ( typeof showOrHide === 'undefined' ) { 2062 showOrHide = ! this.expanded(); 2063 } 2064 if ( showOrHide ) { 2065 this.expand( params ); 2066 } else { 2067 this.collapse( params ); 2068 } 2069 }, 2070 2071 /** 2072 * Expand or collapse the menu item control. 2073 * 2074 * @since 4.6.0 2075 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility 2076 * @param {Object} [params] - Optional params. 2077 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. 2078 */ 2079 onChangeExpanded: function( showOrHide, params ) { 2080 var self = this, $menuitem, $inside, complete; 2081 2082 $menuitem = this.container; 2083 $inside = $menuitem.find( '.menu-item-settings:first' ); 2084 if ( 'undefined' === typeof showOrHide ) { 2085 showOrHide = ! $inside.is( ':visible' ); 2086 } 2087 2088 // Already expanded or collapsed. 2089 if ( $inside.is( ':visible' ) === showOrHide ) { 2090 if ( params && params.completeCallback ) { 2091 params.completeCallback(); 2092 } 2093 return; 2094 } 2095 2096 if ( showOrHide ) { 2097 // Close all other menu item controls before expanding this one. 2098 api.control.each( function( otherControl ) { 2099 if ( self.params.type === otherControl.params.type && self !== otherControl ) { 2100 otherControl.collapseForm(); 2101 } 2102 } ); 2103 2104 complete = function() { 2105 $menuitem 2106 .removeClass( 'menu-item-edit-inactive' ) 2107 .addClass( 'menu-item-edit-active' ); 2108 self.container.trigger( 'expanded' ); 2109 2110 if ( params && params.completeCallback ) { 2111 params.completeCallback(); 2112 } 2113 }; 2114 2115 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' ); 2116 $inside.slideDown( 'fast', complete ); 2117 2118 self.container.trigger( 'expand' ); 2119 } else { 2120 complete = function() { 2121 $menuitem 2122 .addClass( 'menu-item-edit-inactive' ) 2123 .removeClass( 'menu-item-edit-active' ); 2124 self.container.trigger( 'collapsed' ); 2125 2126 if ( params && params.completeCallback ) { 2127 params.completeCallback(); 2128 } 2129 }; 2130 2131 self.container.trigger( 'collapse' ); 2132 2133 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' ); 2134 $inside.slideUp( 'fast', complete ); 2135 } 2136 }, 2137 2138 /** 2139 * Expand the containing menu section, expand the form, and focus on 2140 * the first input in the control. 2141 * 2142 * @since 4.5.0 Added params.completeCallback. 2143 * 2144 * @param {Object} [params] - Params object. 2145 * @param {Function} [params.completeCallback] - Optional callback function when focus has completed. 2146 */ 2147 focus: function( params ) { 2148 params = params || {}; 2149 var control = this, originalCompleteCallback = params.completeCallback, focusControl; 2150 2151 focusControl = function() { 2152 control.expandControlSection(); 2153 2154 params.completeCallback = function() { 2155 var focusable; 2156 2157 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583 2158 focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ); 2159 focusable.first().focus(); 2160 2161 if ( originalCompleteCallback ) { 2162 originalCompleteCallback(); 2163 } 2164 }; 2165 2166 control.expandForm( params ); 2167 }; 2168 2169 if ( api.section.has( control.section() ) ) { 2170 api.section( control.section() ).expand( { 2171 completeCallback: focusControl 2172 } ); 2173 } else { 2174 focusControl(); 2175 } 2176 }, 2177 2178 /** 2179 * Move menu item up one in the menu. 2180 */ 2181 moveUp: function() { 2182 this._changePosition( -1 ); 2183 wp.a11y.speak( api.Menus.data.l10n.movedUp ); 2184 }, 2185 2186 /** 2187 * Move menu item up one in the menu. 2188 */ 2189 moveDown: function() { 2190 this._changePosition( 1 ); 2191 wp.a11y.speak( api.Menus.data.l10n.movedDown ); 2192 }, 2193 /** 2194 * Move menu item and all children up one level of depth. 2195 */ 2196 moveLeft: function() { 2197 this._changeDepth( -1 ); 2198 wp.a11y.speak( api.Menus.data.l10n.movedLeft ); 2199 }, 2200 2201 /** 2202 * Move menu item and children one level deeper, as a submenu of the previous item. 2203 */ 2204 moveRight: function() { 2205 this._changeDepth( 1 ); 2206 wp.a11y.speak( api.Menus.data.l10n.movedRight ); 2207 }, 2208 2209 /** 2210 * Note that this will trigger a UI update, causing child items to 2211 * move as well and cardinal order class names to be updated. 2212 * 2213 * @private 2214 * 2215 * @param {number} offset 1|-1 2216 */ 2217 _changePosition: function( offset ) { 2218 var control = this, 2219 adjacentSetting, 2220 settingValue = _.clone( control.setting() ), 2221 siblingSettings = [], 2222 realPosition; 2223 2224 if ( 1 !== offset && -1 !== offset ) { 2225 throw new Error( 'Offset changes by 1 are only supported.' ); 2226 } 2227 2228 // Skip moving deleted items. 2229 if ( ! control.setting() ) { 2230 return; 2231 } 2232 2233 // Locate the other items under the same parent (siblings). 2234 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { 2235 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) { 2236 siblingSettings.push( otherControl.setting ); 2237 } 2238 }); 2239 siblingSettings.sort(function( a, b ) { 2240 return a().position - b().position; 2241 }); 2242 2243 realPosition = _.indexOf( siblingSettings, control.setting ); 2244 if ( -1 === realPosition ) { 2245 throw new Error( 'Expected setting to be among siblings.' ); 2246 } 2247 2248 // Skip doing anything if the item is already at the edge in the desired direction. 2249 if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) { 2250 // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent? 2251 return; 2252 } 2253 2254 // Update any adjacent menu item setting to take on this item's position. 2255 adjacentSetting = siblingSettings[ realPosition + offset ]; 2256 if ( adjacentSetting ) { 2257 adjacentSetting.set( $.extend( 2258 _.clone( adjacentSetting() ), 2259 { 2260 position: settingValue.position 2261 } 2262 ) ); 2263 } 2264 2265 settingValue.position += offset; 2266 control.setting.set( settingValue ); 2267 }, 2268 2269 /** 2270 * Note that this will trigger a UI update, causing child items to 2271 * move as well and cardinal order class names to be updated. 2272 * 2273 * @private 2274 * 2275 * @param {number} offset 1|-1 2276 */ 2277 _changeDepth: function( offset ) { 2278 if ( 1 !== offset && -1 !== offset ) { 2279 throw new Error( 'Offset changes by 1 are only supported.' ); 2280 } 2281 var control = this, 2282 settingValue = _.clone( control.setting() ), 2283 siblingControls = [], 2284 realPosition, 2285 siblingControl, 2286 parentControl; 2287 2288 // Locate the other items under the same parent (siblings). 2289 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { 2290 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) { 2291 siblingControls.push( otherControl ); 2292 } 2293 }); 2294 siblingControls.sort(function( a, b ) { 2295 return a.setting().position - b.setting().position; 2296 }); 2297 2298 realPosition = _.indexOf( siblingControls, control ); 2299 if ( -1 === realPosition ) { 2300 throw new Error( 'Expected control to be among siblings.' ); 2301 } 2302 2303 if ( -1 === offset ) { 2304 // Skip moving left an item that is already at the top level. 2305 if ( ! settingValue.menu_item_parent ) { 2306 return; 2307 } 2308 2309 parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' ); 2310 2311 // Make this control the parent of all the following siblings. 2312 _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) { 2313 siblingControl.setting.set( 2314 $.extend( 2315 {}, 2316 siblingControl.setting(), 2317 { 2318 menu_item_parent: control.params.menu_item_id, 2319 position: i 2320 } 2321 ) 2322 ); 2323 }); 2324 2325 // Increase the positions of the parent item's subsequent children to make room for this one. 2326 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { 2327 var otherControlSettingValue, isControlToBeShifted; 2328 isControlToBeShifted = ( 2329 otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent && 2330 otherControl.setting().position > parentControl.setting().position 2331 ); 2332 if ( isControlToBeShifted ) { 2333 otherControlSettingValue = _.clone( otherControl.setting() ); 2334 otherControl.setting.set( 2335 $.extend( 2336 otherControlSettingValue, 2337 { position: otherControlSettingValue.position + 1 } 2338 ) 2339 ); 2340 } 2341 }); 2342 2343 // Make this control the following sibling of its parent item. 2344 settingValue.position = parentControl.setting().position + 1; 2345 settingValue.menu_item_parent = parentControl.setting().menu_item_parent; 2346 control.setting.set( settingValue ); 2347 2348 } else if ( 1 === offset ) { 2349 // Skip moving right an item that doesn't have a previous sibling. 2350 if ( realPosition === 0 ) { 2351 return; 2352 } 2353 2354 // Make the control the last child of the previous sibling. 2355 siblingControl = siblingControls[ realPosition - 1 ]; 2356 settingValue.menu_item_parent = siblingControl.params.menu_item_id; 2357 settingValue.position = 0; 2358 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { 2359 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) { 2360 settingValue.position = Math.max( settingValue.position, otherControl.setting().position ); 2361 } 2362 }); 2363 settingValue.position += 1; 2364 control.setting.set( settingValue ); 2365 } 2366 } 2367 } ); 2368 2369 /** 2370 * wp.customize.Menus.MenuNameControl 2371 * 2372 * Customizer control for a nav menu's name. 2373 * 2374 * @class wp.customize.Menus.MenuNameControl 2375 * @augments wp.customize.Control 2376 */ 2377 api.Menus.MenuNameControl = api.Control.extend(/** @lends wp.customize.Menus.MenuNameControl.prototype */{ 2378 2379 ready: function() { 2380 var control = this; 2381 2382 if ( control.setting ) { 2383 var settingValue = control.setting(); 2384 2385 control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) ); 2386 2387 control.nameElement.bind(function( value ) { 2388 var settingValue = control.setting(); 2389 if ( settingValue && settingValue.name !== value ) { 2390 settingValue = _.clone( settingValue ); 2391 settingValue.name = value; 2392 control.setting.set( settingValue ); 2393 } 2394 }); 2395 if ( settingValue ) { 2396 control.nameElement.set( settingValue.name ); 2397 } 2398 2399 control.setting.bind(function( object ) { 2400 if ( object ) { 2401 control.nameElement.set( object.name ); 2402 } 2403 }); 2404 } 2405 } 2406 }); 2407 2408 /** 2409 * wp.customize.Menus.MenuLocationsControl 2410 * 2411 * Customizer control for a nav menu's locations. 2412 * 2413 * @since 4.9.0 2414 * @class wp.customize.Menus.MenuLocationsControl 2415 * @augments wp.customize.Control 2416 */ 2417 api.Menus.MenuLocationsControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationsControl.prototype */{ 2418 2419 /** 2420 * Set up the control. 2421 * 2422 * @since 4.9.0 2423 */ 2424 ready: function () { 2425 var control = this; 2426 2427 control.container.find( '.assigned-menu-location' ).each(function() { 2428 var container = $( this ), 2429 checkbox = container.find( 'input[type=checkbox]' ), 2430 element = new api.Element( checkbox ), 2431 navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' ), 2432 isNewMenu = control.params.menu_id === '', 2433 updateCheckbox = isNewMenu ? _.noop : function( checked ) { 2434 element.set( checked ); 2435 }, 2436 updateSetting = isNewMenu ? _.noop : function( checked ) { 2437 navMenuLocationSetting.set( checked ? control.params.menu_id : 0 ); 2438 }, 2439 updateSelectedMenuLabel = function( selectedMenuId ) { 2440 var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' ); 2441 if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) { 2442 container.find( '.theme-location-set' ).hide(); 2443 } else { 2444 container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) ); 2445 } 2446 }; 2447 2448 updateCheckbox( navMenuLocationSetting.get() === control.params.menu_id ); 2449 2450 checkbox.on( 'change', function() { 2451 // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well. 2452 updateSetting( this.checked ); 2453 } ); 2454 2455 navMenuLocationSetting.bind( function( selectedMenuId ) { 2456 updateCheckbox( selectedMenuId === control.params.menu_id ); 2457 updateSelectedMenuLabel( selectedMenuId ); 2458 } ); 2459 updateSelectedMenuLabel( navMenuLocationSetting.get() ); 2460 }); 2461 }, 2462 2463 /** 2464 * Set the selected locations. 2465 * 2466 * This method sets the selected locations and allows us to do things like 2467 * set the default location for a new menu. 2468 * 2469 * @since 4.9.0 2470 * 2471 * @param {Object.<string,boolean>} selections - A map of location selections. 2472 * @return {void} 2473 */ 2474 setSelections: function( selections ) { 2475 this.container.find( '.menu-location' ).each( function( i, checkboxNode ) { 2476 var locationId = checkboxNode.dataset.locationId; 2477 checkboxNode.checked = locationId in selections ? selections[ locationId ] : false; 2478 } ); 2479 } 2480 }); 2481 2482 /** 2483 * wp.customize.Menus.MenuAutoAddControl 2484 * 2485 * Customizer control for a nav menu's auto add. 2486 * 2487 * @class wp.customize.Menus.MenuAutoAddControl 2488 * @augments wp.customize.Control 2489 */ 2490 api.Menus.MenuAutoAddControl = api.Control.extend(/** @lends wp.customize.Menus.MenuAutoAddControl.prototype */{ 2491 2492 ready: function() { 2493 var control = this, 2494 settingValue = control.setting(); 2495 2496 /* 2497 * Since the control is not registered in PHP, we need to prevent the 2498 * preview's sending of the activeControls to result in this control 2499 * being deactivated. 2500 */ 2501 control.active.validate = function() { 2502 var value, section = api.section( control.section() ); 2503 if ( section ) { 2504 value = section.active(); 2505 } else { 2506 value = false; 2507 } 2508 return value; 2509 }; 2510 2511 control.autoAddElement = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) ); 2512 2513 control.autoAddElement.bind(function( value ) { 2514 var settingValue = control.setting(); 2515 if ( settingValue && settingValue.name !== value ) { 2516 settingValue = _.clone( settingValue ); 2517 settingValue.auto_add = value; 2518 control.setting.set( settingValue ); 2519 } 2520 }); 2521 if ( settingValue ) { 2522 control.autoAddElement.set( settingValue.auto_add ); 2523 } 2524 2525 control.setting.bind(function( object ) { 2526 if ( object ) { 2527 control.autoAddElement.set( object.auto_add ); 2528 } 2529 }); 2530 } 2531 2532 }); 2533 2534 /** 2535 * wp.customize.Menus.MenuControl 2536 * 2537 * Customizer control for menus. 2538 * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type 2539 * 2540 * @class wp.customize.Menus.MenuControl 2541 * @augments wp.customize.Control 2542 */ 2543 api.Menus.MenuControl = api.Control.extend(/** @lends wp.customize.Menus.MenuControl.prototype */{ 2544 /** 2545 * Set up the control. 2546 */ 2547 ready: function() { 2548 var control = this, 2549 section = api.section( control.section() ), 2550 menuId = control.params.menu_id, 2551 menu = control.setting(), 2552 name, 2553 widgetTemplate, 2554 select; 2555 2556 if ( 'undefined' === typeof this.params.menu_id ) { 2557 throw new Error( 'params.menu_id was not defined' ); 2558 } 2559 2560 /* 2561 * Since the control is not registered in PHP, we need to prevent the 2562 * preview's sending of the activeControls to result in this control 2563 * being deactivated. 2564 */ 2565 control.active.validate = function() { 2566 var value; 2567 if ( section ) { 2568 value = section.active(); 2569 } else { 2570 value = false; 2571 } 2572 return value; 2573 }; 2574 2575 control.$controlSection = section.headContainer; 2576 control.$sectionContent = control.container.closest( '.accordion-section-content' ); 2577 2578 this._setupModel(); 2579 2580 api.section( control.section(), function( section ) { 2581 section.deferred.initSortables.done(function( menuList ) { 2582 control._setupSortable( menuList ); 2583 }); 2584 } ); 2585 2586 this._setupAddition(); 2587 this._setupTitle(); 2588 2589 // Add menu to Navigation Menu widgets. 2590 if ( menu ) { 2591 name = displayNavMenuName( menu.name ); 2592 2593 // Add the menu to the existing controls. 2594 api.control.each( function( widgetControl ) { 2595 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) { 2596 return; 2597 } 2598 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).show(); 2599 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).hide(); 2600 2601 select = widgetControl.container.find( 'select' ); 2602 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) { 2603 select.append( new Option( name, menuId ) ); 2604 } 2605 } ); 2606 2607 // Add the menu to the widget template. 2608 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' ); 2609 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).show(); 2610 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).hide(); 2611 select = widgetTemplate.find( '.widget-inside select:first' ); 2612 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) { 2613 select.append( new Option( name, menuId ) ); 2614 } 2615 } 2616 2617 /* 2618 * Wait for menu items to be added. 2619 * Ideally, we'd bind to an event indicating construction is complete, 2620 * but deferring appears to be the best option today. 2621 */ 2622 _.defer( function () { 2623 control.updateInvitationVisibility(); 2624 } ); 2625 }, 2626 2627 /** 2628 * Update ordering of menu item controls when the setting is updated. 2629 */ 2630 _setupModel: function() { 2631 var control = this, 2632 menuId = control.params.menu_id; 2633 2634 control.setting.bind( function( to ) { 2635 var name; 2636 if ( false === to ) { 2637 control._handleDeletion(); 2638 } else { 2639 // Update names in the Navigation Menu widgets. 2640 name = displayNavMenuName( to.name ); 2641 api.control.each( function( widgetControl ) { 2642 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) { 2643 return; 2644 } 2645 var select = widgetControl.container.find( 'select' ); 2646 select.find( 'option[value=' + String( menuId ) + ']' ).text( name ); 2647 }); 2648 } 2649 } ); 2650 }, 2651 2652 /** 2653 * Allow items in each menu to be re-ordered, and for the order to be previewed. 2654 * 2655 * Notice that the UI aspects here are handled by wpNavMenu.initSortables() 2656 * which is called in MenuSection.onChangeExpanded() 2657 * 2658 * @param {Object} menuList - The element that has sortable(). 2659 */ 2660 _setupSortable: function( menuList ) { 2661 var control = this; 2662 2663 if ( ! menuList.is( control.$sectionContent ) ) { 2664 throw new Error( 'Unexpected menuList.' ); 2665 } 2666 2667 menuList.on( 'sortstart', function() { 2668 control.isSorting = true; 2669 }); 2670 2671 menuList.on( 'sortstop', function() { 2672 setTimeout( function() { // Next tick. 2673 var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ), 2674 menuItemControls = [], 2675 position = 0, 2676 priority = 10; 2677 2678 control.isSorting = false; 2679 2680 // Reset horizontal scroll position when done dragging. 2681 control.$sectionContent.scrollLeft( 0 ); 2682 2683 _.each( menuItemContainerIds, function( menuItemContainerId ) { 2684 var menuItemId, menuItemControl, matches; 2685 matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' ); 2686 if ( ! matches ) { 2687 return; 2688 } 2689 menuItemId = parseInt( matches[1], 10 ); 2690 menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' ); 2691 if ( menuItemControl ) { 2692 menuItemControls.push( menuItemControl ); 2693 } 2694 } ); 2695 2696 _.each( menuItemControls, function( menuItemControl ) { 2697 if ( false === menuItemControl.setting() ) { 2698 // Skip deleted items. 2699 return; 2700 } 2701 var setting = _.clone( menuItemControl.setting() ); 2702 position += 1; 2703 priority += 1; 2704 setting.position = position; 2705 menuItemControl.priority( priority ); 2706 2707 // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value. 2708 setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 ); 2709 if ( ! setting.menu_item_parent ) { 2710 setting.menu_item_parent = 0; 2711 } 2712 2713 menuItemControl.setting.set( setting ); 2714 }); 2715 }); 2716 2717 }); 2718 control.isReordering = false; 2719 2720 /** 2721 * Keyboard-accessible reordering. 2722 */ 2723 this.container.find( '.reorder-toggle' ).on( 'click', function() { 2724 control.toggleReordering( ! control.isReordering ); 2725 } ); 2726 }, 2727 2728 /** 2729 * Set up UI for adding a new menu item. 2730 */ 2731 _setupAddition: function() { 2732 var self = this; 2733 2734 this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) { 2735 if ( self.$sectionContent.hasClass( 'reordering' ) ) { 2736 return; 2737 } 2738 2739 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) { 2740 $( this ).attr( 'aria-expanded', 'true' ); 2741 api.Menus.availableMenuItemsPanel.open( self ); 2742 } else { 2743 $( this ).attr( 'aria-expanded', 'false' ); 2744 api.Menus.availableMenuItemsPanel.close(); 2745 event.stopPropagation(); 2746 } 2747 } ); 2748 }, 2749 2750 _handleDeletion: function() { 2751 var control = this, 2752 section, 2753 menuId = control.params.menu_id, 2754 removeSection, 2755 widgetTemplate, 2756 navMenuCount = 0; 2757 section = api.section( control.section() ); 2758 removeSection = function() { 2759 section.container.remove(); 2760 api.section.remove( section.id ); 2761 }; 2762 2763 if ( section && section.expanded() ) { 2764 section.collapse({ 2765 completeCallback: function() { 2766 removeSection(); 2767 wp.a11y.speak( api.Menus.data.l10n.menuDeleted ); 2768 api.panel( 'nav_menus' ).focus(); 2769 } 2770 }); 2771 } else { 2772 removeSection(); 2773 } 2774 2775 api.each(function( setting ) { 2776 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) { 2777 navMenuCount += 1; 2778 } 2779 }); 2780 2781 // Remove the menu from any Navigation Menu widgets. 2782 api.control.each(function( widgetControl ) { 2783 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) { 2784 return; 2785 } 2786 var select = widgetControl.container.find( 'select' ); 2787 if ( select.val() === String( menuId ) ) { 2788 select.prop( 'selectedIndex', 0 ).trigger( 'change' ); 2789 } 2790 2791 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount ); 2792 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount ); 2793 widgetControl.container.find( 'option[value=' + String( menuId ) + ']' ).remove(); 2794 }); 2795 2796 // Remove the menu to the nav menu widget template. 2797 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' ); 2798 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount ); 2799 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount ); 2800 widgetTemplate.find( 'option[value=' + String( menuId ) + ']' ).remove(); 2801 }, 2802 2803 /** 2804 * Update Section Title as menu name is changed. 2805 */ 2806 _setupTitle: function() { 2807 var control = this; 2808 2809 control.setting.bind( function( menu ) { 2810 if ( ! menu ) { 2811 return; 2812 } 2813 2814 var section = api.section( control.section() ), 2815 menuId = control.params.menu_id, 2816 controlTitle = section.headContainer.find( '.accordion-section-title' ), 2817 sectionTitle = section.contentContainer.find( '.customize-section-title h3' ), 2818 location = section.headContainer.find( '.menu-in-location' ), 2819 action = sectionTitle.find( '.customize-action' ), 2820 name = displayNavMenuName( menu.name ); 2821 2822 // Update the control title. 2823 controlTitle.text( name ); 2824 if ( location.length ) { 2825 location.appendTo( controlTitle ); 2826 } 2827 2828 // Update the section title. 2829 sectionTitle.text( name ); 2830 if ( action.length ) { 2831 action.prependTo( sectionTitle ); 2832 } 2833 2834 // Update the nav menu name in location selects. 2835 api.control.each( function( control ) { 2836 if ( /^nav_menu_locations\[/.test( control.id ) ) { 2837 control.container.find( 'option[value=' + menuId + ']' ).text( name ); 2838 } 2839 } ); 2840 2841 // Update the nav menu name in all location checkboxes. 2842 section.contentContainer.find( '.customize-control-checkbox input' ).each( function() { 2843 if ( $( this ).prop( 'checked' ) ) { 2844 $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name ); 2845 } 2846 } ); 2847 } ); 2848 }, 2849 2850 /*********************************************************************** 2851 * Begin public API methods 2852 **********************************************************************/ 2853 2854 /** 2855 * Enable/disable the reordering UI 2856 * 2857 * @param {boolean} showOrHide to enable/disable reordering 2858 */ 2859 toggleReordering: function( showOrHide ) { 2860 var addNewItemBtn = this.container.find( '.add-new-menu-item' ), 2861 reorderBtn = this.container.find( '.reorder-toggle' ), 2862 itemsTitle = this.$sectionContent.find( '.item-title' ); 2863 2864 showOrHide = Boolean( showOrHide ); 2865 2866 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) { 2867 return; 2868 } 2869 2870 this.isReordering = showOrHide; 2871 this.$sectionContent.toggleClass( 'reordering', showOrHide ); 2872 this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' ); 2873 if ( this.isReordering ) { 2874 addNewItemBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); 2875 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOff ); 2876 wp.a11y.speak( api.Menus.data.l10n.reorderModeOn ); 2877 itemsTitle.attr( 'aria-hidden', 'false' ); 2878 } else { 2879 addNewItemBtn.removeAttr( 'tabindex aria-hidden' ); 2880 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOn ); 2881 wp.a11y.speak( api.Menus.data.l10n.reorderModeOff ); 2882 itemsTitle.attr( 'aria-hidden', 'true' ); 2883 } 2884 2885 if ( showOrHide ) { 2886 _( this.getMenuItemControls() ).each( function( formControl ) { 2887 formControl.collapseForm(); 2888 } ); 2889 } 2890 }, 2891 2892 /** 2893 * @return {wp.customize.controlConstructor.nav_menu_item[]} 2894 */ 2895 getMenuItemControls: function() { 2896 var menuControl = this, 2897 menuItemControls = [], 2898 menuTermId = menuControl.params.menu_id; 2899 2900 api.control.each(function( control ) { 2901 if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) { 2902 menuItemControls.push( control ); 2903 } 2904 }); 2905 2906 return menuItemControls; 2907 }, 2908 2909 /** 2910 * Make sure that each menu item control has the proper depth. 2911 */ 2912 reflowMenuItems: function() { 2913 var menuControl = this, 2914 menuItemControls = menuControl.getMenuItemControls(), 2915 reflowRecursively; 2916 2917 reflowRecursively = function( context ) { 2918 var currentMenuItemControls = [], 2919 thisParent = context.currentParent; 2920 _.each( context.menuItemControls, function( menuItemControl ) { 2921 if ( thisParent === menuItemControl.setting().menu_item_parent ) { 2922 currentMenuItemControls.push( menuItemControl ); 2923 // @todo We could remove this item from menuItemControls now, for efficiency. 2924 } 2925 }); 2926 currentMenuItemControls.sort( function( a, b ) { 2927 return a.setting().position - b.setting().position; 2928 }); 2929 2930 _.each( currentMenuItemControls, function( menuItemControl ) { 2931 // Update position. 2932 context.currentAbsolutePosition += 1; 2933 menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order. 2934 2935 // Update depth. 2936 if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) { 2937 _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) { 2938 menuItemControl.container.removeClass( className ); 2939 }); 2940 menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) ); 2941 } 2942 menuItemControl.container.data( 'item-depth', context.currentDepth ); 2943 2944 // Process any children items. 2945 context.currentDepth += 1; 2946 context.currentParent = menuItemControl.params.menu_item_id; 2947 reflowRecursively( context ); 2948 context.currentDepth -= 1; 2949 context.currentParent = thisParent; 2950 }); 2951 2952 // Update class names for reordering controls. 2953 if ( currentMenuItemControls.length ) { 2954 _( currentMenuItemControls ).each(function( menuItemControl ) { 2955 menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' ); 2956 if ( 0 === context.currentDepth ) { 2957 menuItemControl.container.addClass( 'move-left-disabled' ); 2958 } else if ( 10 === context.currentDepth ) { 2959 menuItemControl.container.addClass( 'move-right-disabled' ); 2960 } 2961 }); 2962 2963 currentMenuItemControls[0].container 2964 .addClass( 'move-up-disabled' ) 2965 .addClass( 'move-right-disabled' ) 2966 .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length ); 2967 currentMenuItemControls[ currentMenuItemControls.length - 1 ].container 2968 .addClass( 'move-down-disabled' ) 2969 .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length ); 2970 } 2971 }; 2972 2973 reflowRecursively( { 2974 menuItemControls: menuItemControls, 2975 currentParent: 0, 2976 currentDepth: 0, 2977 currentAbsolutePosition: 0 2978 } ); 2979 2980 menuControl.updateInvitationVisibility( menuItemControls ); 2981 menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 ); 2982 }, 2983 2984 /** 2985 * Note that this function gets debounced so that when a lot of setting 2986 * changes are made at once, for instance when moving a menu item that 2987 * has child items, this function will only be called once all of the 2988 * settings have been updated. 2989 */ 2990 debouncedReflowMenuItems: _.debounce( function() { 2991 this.reflowMenuItems.apply( this, arguments ); 2992 }, 0 ), 2993 2994 /** 2995 * Add a new item to this menu. 2996 * 2997 * @param {Object} item - Value for the nav_menu_item setting to be created. 2998 * @return {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance. 2999 */ 3000 addItemToMenu: function( item ) { 3001 var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10, 3002 originalItemId = item.id || ''; 3003 3004 _.each( menuControl.getMenuItemControls(), function( control ) { 3005 if ( false === control.setting() ) { 3006 return; 3007 } 3008 priority = Math.max( priority, control.priority() ); 3009 if ( 0 === control.setting().menu_item_parent ) { 3010 position = Math.max( position, control.setting().position ); 3011 } 3012 }); 3013 position += 1; 3014 priority += 1; 3015 3016 item = $.extend( 3017 {}, 3018 api.Menus.data.defaultSettingValues.nav_menu_item, 3019 item, 3020 { 3021 nav_menu_term_id: menuControl.params.menu_id, 3022 original_title: item.title, 3023 position: position 3024 } 3025 ); 3026 delete item.id; // Only used by Backbone. 3027 3028 placeholderId = api.Menus.generatePlaceholderAutoIncrementId(); 3029 customizeId = 'nav_menu_item[' + String( placeholderId ) + ']'; 3030 settingArgs = { 3031 type: 'nav_menu_item', 3032 transport: api.Menus.data.settingTransport, 3033 previewer: api.previewer 3034 }; 3035 setting = api.create( customizeId, customizeId, {}, settingArgs ); 3036 setting.set( item ); // Change from initial empty object to actual item to mark as dirty. 3037 3038 // Add the menu item control. 3039 menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, { 3040 type: 'nav_menu_item', 3041 section: menuControl.id, 3042 priority: priority, 3043 settings: { 3044 'default': customizeId 3045 }, 3046 menu_item_id: placeholderId, 3047 original_item_id: originalItemId 3048 } ); 3049 3050 api.control.add( menuItemControl ); 3051 setting.preview(); 3052 menuControl.debouncedReflowMenuItems(); 3053 3054 wp.a11y.speak( api.Menus.data.l10n.itemAdded ); 3055 3056 return menuItemControl; 3057 }, 3058 3059 /** 3060 * Show an invitation to add new menu items when there are no menu items. 3061 * 3062 * @since 4.9.0 3063 * 3064 * @param {wp.customize.controlConstructor.nav_menu_item[]} optionalMenuItemControls 3065 */ 3066 updateInvitationVisibility: function ( optionalMenuItemControls ) { 3067 var menuItemControls = optionalMenuItemControls || this.getMenuItemControls(); 3068 3069 this.container.find( '.new-menu-item-invitation' ).toggle( menuItemControls.length === 0 ); 3070 } 3071 } ); 3072 3073 /** 3074 * Extends wp.customize.controlConstructor with control constructor for 3075 * menu_location, menu_item, nav_menu, and new_menu. 3076 */ 3077 $.extend( api.controlConstructor, { 3078 nav_menu_location: api.Menus.MenuLocationControl, 3079 nav_menu_item: api.Menus.MenuItemControl, 3080 nav_menu: api.Menus.MenuControl, 3081 nav_menu_name: api.Menus.MenuNameControl, 3082 nav_menu_locations: api.Menus.MenuLocationsControl, 3083 nav_menu_auto_add: api.Menus.MenuAutoAddControl 3084 }); 3085 3086 /** 3087 * Extends wp.customize.panelConstructor with section constructor for menus. 3088 */ 3089 $.extend( api.panelConstructor, { 3090 nav_menus: api.Menus.MenusPanel 3091 }); 3092 3093 /** 3094 * Extends wp.customize.sectionConstructor with section constructor for menu. 3095 */ 3096 $.extend( api.sectionConstructor, { 3097 nav_menu: api.Menus.MenuSection, 3098 new_menu: api.Menus.NewMenuSection 3099 }); 3100 3101 /** 3102 * Init Customizer for menus. 3103 */ 3104 api.bind( 'ready', function() { 3105 3106 // Set up the menu items panel. 3107 api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({ 3108 collection: api.Menus.availableMenuItems 3109 }); 3110 3111 api.bind( 'saved', function( data ) { 3112 if ( data.nav_menu_updates || data.nav_menu_item_updates ) { 3113 api.Menus.applySavedData( data ); 3114 } 3115 } ); 3116 3117 /* 3118 * Reset the list of posts created in the customizer once published. 3119 * The setting is updated quietly (bypassing events being triggered) 3120 * so that the customized state doesn't become immediately dirty. 3121 */ 3122 api.state( 'changesetStatus' ).bind( function( status ) { 3123 if ( 'publish' === status ) { 3124 api( 'nav_menus_created_posts' )._value = []; 3125 } 3126 } ); 3127 3128 // Open and focus menu control. 3129 api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl ); 3130 } ); 3131 3132 /** 3133 * When customize_save comes back with a success, make sure any inserted 3134 * nav menus and items are properly re-added with their newly-assigned IDs. 3135 * 3136 * @alias wp.customize.Menus.applySavedData 3137 * 3138 * @param {Object} data 3139 * @param {Array} data.nav_menu_updates 3140 * @param {Array} data.nav_menu_item_updates 3141 */ 3142 api.Menus.applySavedData = function( data ) { 3143 3144 var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {}; 3145 3146 _( data.nav_menu_updates ).each(function( update ) { 3147 var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount, shouldExpandNewSection; 3148 if ( 'inserted' === update.status ) { 3149 if ( ! update.previous_term_id ) { 3150 throw new Error( 'Expected previous_term_id' ); 3151 } 3152 if ( ! update.term_id ) { 3153 throw new Error( 'Expected term_id' ); 3154 } 3155 oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']'; 3156 if ( ! api.has( oldCustomizeId ) ) { 3157 throw new Error( 'Expected setting to exist: ' + oldCustomizeId ); 3158 } 3159 oldSetting = api( oldCustomizeId ); 3160 if ( ! api.section.has( oldCustomizeId ) ) { 3161 throw new Error( 'Expected control to exist: ' + oldCustomizeId ); 3162 } 3163 oldSection = api.section( oldCustomizeId ); 3164 3165 settingValue = oldSetting.get(); 3166 if ( ! settingValue ) { 3167 throw new Error( 'Did not expect setting to be empty (deleted).' ); 3168 } 3169 settingValue = $.extend( _.clone( settingValue ), update.saved_value ); 3170 3171 insertedMenuIdMapping[ update.previous_term_id ] = update.term_id; 3172 newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']'; 3173 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { 3174 type: 'nav_menu', 3175 transport: api.Menus.data.settingTransport, 3176 previewer: api.previewer 3177 } ); 3178 3179 shouldExpandNewSection = oldSection.expanded(); 3180 if ( shouldExpandNewSection ) { 3181 oldSection.collapse(); 3182 } 3183 3184 // Add the menu section. 3185 newSection = new api.Menus.MenuSection( newCustomizeId, { 3186 panel: 'nav_menus', 3187 title: settingValue.name, 3188 customizeAction: api.Menus.data.l10n.customizingMenus, 3189 type: 'nav_menu', 3190 priority: oldSection.priority.get(), 3191 menu_id: update.term_id 3192 } ); 3193 3194 // Add new control for the new menu. 3195 api.section.add( newSection ); 3196 3197 // Update the values for nav menus in Navigation Menu controls. 3198 api.control.each( function( setting ) { 3199 if ( ! setting.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== setting.params.widget_id_base ) { 3200 return; 3201 } 3202 var select, oldMenuOption, newMenuOption; 3203 select = setting.container.find( 'select' ); 3204 oldMenuOption = select.find( 'option[value=' + String( update.previous_term_id ) + ']' ); 3205 newMenuOption = select.find( 'option[value=' + String( update.term_id ) + ']' ); 3206 newMenuOption.prop( 'selected', oldMenuOption.prop( 'selected' ) ); 3207 oldMenuOption.remove(); 3208 } ); 3209 3210 // Delete the old placeholder nav_menu. 3211 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set. 3212 oldSetting.set( false ); 3213 oldSetting.preview(); 3214 newSetting.preview(); 3215 oldSetting._dirty = false; 3216 3217 // Remove nav_menu section. 3218 oldSection.container.remove(); 3219 api.section.remove( oldCustomizeId ); 3220 3221 // Update the nav_menu widget to reflect removed placeholder menu. 3222 navMenuCount = 0; 3223 api.each(function( setting ) { 3224 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) { 3225 navMenuCount += 1; 3226 } 3227 }); 3228 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' ); 3229 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount ); 3230 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount ); 3231 widgetTemplate.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove(); 3232 3233 // Update the nav_menu_locations[...] controls to remove the placeholder menus from the dropdown options. 3234 wp.customize.control.each(function( control ){ 3235 if ( /^nav_menu_locations\[/.test( control.id ) ) { 3236 control.container.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove(); 3237 } 3238 }); 3239 3240 // Update nav_menu_locations to reference the new ID. 3241 api.each( function( setting ) { 3242 var wasSaved = api.state( 'saved' ).get(); 3243 if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) { 3244 setting.set( update.term_id ); 3245 setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update(). 3246 api.state( 'saved' ).set( wasSaved ); 3247 setting.preview(); 3248 } 3249 } ); 3250 3251 if ( shouldExpandNewSection ) { 3252 newSection.expand(); 3253 } 3254 } else if ( 'updated' === update.status ) { 3255 customizeId = 'nav_menu[' + String( update.term_id ) + ']'; 3256 if ( ! api.has( customizeId ) ) { 3257 throw new Error( 'Expected setting to exist: ' + customizeId ); 3258 } 3259 3260 // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name). 3261 setting = api( customizeId ); 3262 if ( ! _.isEqual( update.saved_value, setting.get() ) ) { 3263 wasSaved = api.state( 'saved' ).get(); 3264 setting.set( update.saved_value ); 3265 setting._dirty = false; 3266 api.state( 'saved' ).set( wasSaved ); 3267 } 3268 } 3269 } ); 3270 3271 // Build up mapping of nav_menu_item placeholder IDs to inserted IDs. 3272 _( data.nav_menu_item_updates ).each(function( update ) { 3273 if ( update.previous_post_id ) { 3274 insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id; 3275 } 3276 }); 3277 3278 _( data.nav_menu_item_updates ).each(function( update ) { 3279 var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl; 3280 if ( 'inserted' === update.status ) { 3281 if ( ! update.previous_post_id ) { 3282 throw new Error( 'Expected previous_post_id' ); 3283 } 3284 if ( ! update.post_id ) { 3285 throw new Error( 'Expected post_id' ); 3286 } 3287 oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']'; 3288 if ( ! api.has( oldCustomizeId ) ) { 3289 throw new Error( 'Expected setting to exist: ' + oldCustomizeId ); 3290 } 3291 oldSetting = api( oldCustomizeId ); 3292 if ( ! api.control.has( oldCustomizeId ) ) { 3293 throw new Error( 'Expected control to exist: ' + oldCustomizeId ); 3294 } 3295 oldControl = api.control( oldCustomizeId ); 3296 3297 settingValue = oldSetting.get(); 3298 if ( ! settingValue ) { 3299 throw new Error( 'Did not expect setting to be empty (deleted).' ); 3300 } 3301 settingValue = _.clone( settingValue ); 3302 3303 // If the parent menu item was also inserted, update the menu_item_parent to the new ID. 3304 if ( settingValue.menu_item_parent < 0 ) { 3305 if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) { 3306 throw new Error( 'inserted ID for menu_item_parent not available' ); 3307 } 3308 settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ]; 3309 } 3310 3311 // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id. 3312 if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) { 3313 settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ]; 3314 } 3315 3316 newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']'; 3317 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { 3318 type: 'nav_menu_item', 3319 transport: api.Menus.data.settingTransport, 3320 previewer: api.previewer 3321 } ); 3322 3323 // Add the menu control. 3324 newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, { 3325 type: 'nav_menu_item', 3326 menu_id: update.post_id, 3327 section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']', 3328 priority: oldControl.priority.get(), 3329 settings: { 3330 'default': newCustomizeId 3331 }, 3332 menu_item_id: update.post_id 3333 } ); 3334 3335 // Remove old control. 3336 oldControl.container.remove(); 3337 api.control.remove( oldCustomizeId ); 3338 3339 // Add new control to take its place. 3340 api.control.add( newControl ); 3341 3342 // Delete the placeholder and preview the new setting. 3343 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set. 3344 oldSetting.set( false ); 3345 oldSetting.preview(); 3346 newSetting.preview(); 3347 oldSetting._dirty = false; 3348 3349 newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) ); 3350 } 3351 }); 3352 3353 /* 3354 * Update the settings for any nav_menu widgets that had selected a placeholder ID. 3355 */ 3356 _.each( data.widget_nav_menu_updates, function( widgetSettingValue, widgetSettingId ) { 3357 var setting = api( widgetSettingId ); 3358 if ( setting ) { 3359 setting._value = widgetSettingValue; 3360 setting.preview(); // Send to the preview now so that menu refresh will use the inserted menu. 3361 } 3362 }); 3363 }; 3364 3365 /** 3366 * Focus a menu item control. 3367 * 3368 * @alias wp.customize.Menus.focusMenuItemControl 3369 * 3370 * @param {string} menuItemId 3371 */ 3372 api.Menus.focusMenuItemControl = function( menuItemId ) { 3373 var control = api.Menus.getMenuItemControl( menuItemId ); 3374 if ( control ) { 3375 control.focus(); 3376 } 3377 }; 3378 3379 /** 3380 * Get the control for a given menu. 3381 * 3382 * @alias wp.customize.Menus.getMenuControl 3383 * 3384 * @param menuId 3385 * @return {wp.customize.controlConstructor.menus[]} 3386 */ 3387 api.Menus.getMenuControl = function( menuId ) { 3388 return api.control( 'nav_menu[' + menuId + ']' ); 3389 }; 3390 3391 /** 3392 * Given a menu item ID, get the control associated with it. 3393 * 3394 * @alias wp.customize.Menus.getMenuItemControl 3395 * 3396 * @param {string} menuItemId 3397 * @return {Object|null} 3398 */ 3399 api.Menus.getMenuItemControl = function( menuItemId ) { 3400 return api.control( menuItemIdToSettingId( menuItemId ) ); 3401 }; 3402 3403 /** 3404 * @alias wp.customize.Menus~menuItemIdToSettingId 3405 * 3406 * @param {string} menuItemId 3407 */ 3408 function menuItemIdToSettingId( menuItemId ) { 3409 return 'nav_menu_item[' + menuItemId + ']'; 3410 } 3411 3412 /** 3413 * Apply sanitize_text_field()-like logic to the supplied name, returning a 3414 * "unnammed" fallback string if the name is then empty. 3415 * 3416 * @alias wp.customize.Menus~displayNavMenuName 3417 * 3418 * @param {string} name 3419 * @return {string} 3420 */ 3421 function displayNavMenuName( name ) { 3422 name = name || ''; 3423 name = wp.sanitize.stripTagsAndEncodeText( name ); // Remove any potential tags from name. 3424 name = name.toString().trim(); 3425 return name || api.Menus.data.l10n.unnamed; 3426 } 3427 3428 })( wp.customize, wp, jQuery );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Tue Jan 26 08:20:01 2021 | Cross-referenced by PHPXref |