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