PageRenderTime 67ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/src/js/_enqueues/wp/customize/nav-menus.js

https://gitlab.com/morganestes/wordpress-develop
JavaScript | 1658 lines | 1088 code | 211 blank | 359 comment | 141 complexity | 38d2fed4e56612065a2f26b4c538a1b3 MD5 | raw file
  1. /* global _wpCustomizeNavMenusSettings, wpNavMenu, console */
  2. ( function( api, wp, $ ) {
  3. 'use strict';
  4. /**
  5. * Set up wpNavMenu for drag and drop.
  6. */
  7. wpNavMenu.originalInit = wpNavMenu.init;
  8. wpNavMenu.options.menuItemDepthPerLevel = 20;
  9. wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
  10. wpNavMenu.options.targetTolerance = 10;
  11. wpNavMenu.init = function() {
  12. this.jQueryExtensions();
  13. };
  14. /**
  15. * @namespace wp.customize.Menus
  16. */
  17. api.Menus = api.Menus || {};
  18. // Link settings.
  19. api.Menus.data = {
  20. itemTypes: [],
  21. l10n: {},
  22. settingTransport: 'refresh',
  23. phpIntMax: 0,
  24. defaultSettingValues: {
  25. nav_menu: {},
  26. nav_menu_item: {}
  27. },
  28. locationSlugMappedToName: {}
  29. };
  30. if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
  31. $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
  32. }
  33. /**
  34. * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
  35. * serve as placeholders until Save & Publish happens.
  36. *
  37. * @alias wp.customize.Menus.generatePlaceholderAutoIncrementId
  38. *
  39. * @return {number}
  40. */
  41. api.Menus.generatePlaceholderAutoIncrementId = function() {
  42. return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
  43. };
  44. /**
  45. * wp.customize.Menus.AvailableItemModel
  46. *
  47. * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
  48. *
  49. * @class wp.customize.Menus.AvailableItemModel
  50. * @augments Backbone.Model
  51. */
  52. api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
  53. {
  54. id: null // This is only used by Backbone.
  55. },
  56. api.Menus.data.defaultSettingValues.nav_menu_item
  57. ) );
  58. /**
  59. * wp.customize.Menus.AvailableItemCollection
  60. *
  61. * Collection for available menu item models.
  62. *
  63. * @class wp.customize.Menus.AvailableItemCollection
  64. * @augments Backbone.Collection
  65. */
  66. api.Menus.AvailableItemCollection = Backbone.Collection.extend(/** @lends wp.customize.Menus.AvailableItemCollection.prototype */{
  67. model: api.Menus.AvailableItemModel,
  68. sort_key: 'order',
  69. comparator: function( item ) {
  70. return -item.get( this.sort_key );
  71. },
  72. sortByField: function( fieldName ) {
  73. this.sort_key = fieldName;
  74. this.sort();
  75. }
  76. });
  77. api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
  78. /**
  79. * Insert a new `auto-draft` post.
  80. *
  81. * @since 4.7.0
  82. * @alias wp.customize.Menus.insertAutoDraftPost
  83. *
  84. * @param {object} params - Parameters for the draft post to create.
  85. * @param {string} params.post_type - Post type to add.
  86. * @param {string} params.post_title - Post title to use.
  87. * @return {jQuery.promise} Promise resolved with the added post.
  88. */
  89. api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
  90. var request, deferred = $.Deferred();
  91. request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
  92. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  93. 'wp_customize': 'on',
  94. 'customize_changeset_uuid': api.settings.changeset.uuid,
  95. 'params': params
  96. } );
  97. request.done( function( response ) {
  98. if ( response.post_id ) {
  99. api( 'nav_menus_created_posts' ).set(
  100. api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] )
  101. );
  102. if ( 'page' === params.post_type ) {
  103. // Activate static front page controls as this could be the first page created.
  104. if ( api.section.has( 'static_front_page' ) ) {
  105. api.section( 'static_front_page' ).activate();
  106. }
  107. // Add new page to dropdown-pages controls.
  108. api.control.each( function( control ) {
  109. var select;
  110. if ( 'dropdown-pages' === control.params.type ) {
  111. select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' );
  112. select.append( new Option( params.post_title, response.post_id ) );
  113. }
  114. } );
  115. }
  116. deferred.resolve( response );
  117. }
  118. } );
  119. request.fail( function( response ) {
  120. var error = response || '';
  121. if ( 'undefined' !== typeof response.message ) {
  122. error = response.message;
  123. }
  124. console.error( error );
  125. deferred.rejectWith( error );
  126. } );
  127. return deferred.promise();
  128. };
  129. api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Menus.AvailableMenuItemsPanelView.prototype */{
  130. el: '#available-menu-items',
  131. events: {
  132. 'input #menu-items-search': 'debounceSearch',
  133. 'keyup #menu-items-search': 'debounceSearch',
  134. 'focus .menu-item-tpl': 'focus',
  135. 'click .menu-item-tpl': '_submit',
  136. 'click #custom-menu-item-submit': '_submitLink',
  137. 'keypress #custom-menu-item-name': '_submitLink',
  138. 'click .new-content-item .add-content': '_submitNew',
  139. 'keypress .create-item-input': '_submitNew',
  140. 'keydown': 'keyboardAccessible'
  141. },
  142. // Cache current selected menu item.
  143. selected: null,
  144. // Cache menu control that opened the panel.
  145. currentMenuControl: null,
  146. debounceSearch: null,
  147. $search: null,
  148. $clearResults: null,
  149. searchTerm: '',
  150. rendered: false,
  151. pages: {},
  152. sectionContent: '',
  153. loading: false,
  154. addingNew: false,
  155. /**
  156. * wp.customize.Menus.AvailableMenuItemsPanelView
  157. *
  158. * View class for the available menu items panel.
  159. *
  160. * @constructs wp.customize.Menus.AvailableMenuItemsPanelView
  161. * @augments wp.Backbone.View
  162. */
  163. initialize: function() {
  164. var self = this;
  165. if ( ! api.panel.has( 'nav_menus' ) ) {
  166. return;
  167. }
  168. this.$search = $( '#menu-items-search' );
  169. this.$clearResults = this.$el.find( '.clear-results' );
  170. this.sectionContent = this.$el.find( '.available-menu-items-list' );
  171. this.debounceSearch = _.debounce( self.search, 500 );
  172. _.bindAll( this, 'close' );
  173. // If the available menu items panel is open and the customize controls are
  174. // interacted with (other than an item being deleted), then close the
  175. // available menu items panel. Also close on back button click.
  176. $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
  177. var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
  178. isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
  179. if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
  180. self.close();
  181. }
  182. } );
  183. // Clear the search results and trigger a `keyup` event to fire a new search.
  184. this.$clearResults.on( 'click', function() {
  185. self.$search.val( '' ).focus().trigger( 'keyup' );
  186. } );
  187. this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
  188. $( this ).removeClass( 'invalid' );
  189. });
  190. // Load available items if it looks like we'll need them.
  191. api.panel( 'nav_menus' ).container.bind( 'expanded', function() {
  192. if ( ! self.rendered ) {
  193. self.initList();
  194. self.rendered = true;
  195. }
  196. });
  197. // Load more items.
  198. this.sectionContent.scroll( function() {
  199. var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
  200. visibleHeight = self.$el.find( '.accordion-section.open' ).height();
  201. if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
  202. var type = $( this ).data( 'type' ),
  203. object = $( this ).data( 'object' );
  204. if ( 'search' === type ) {
  205. if ( self.searchTerm ) {
  206. self.doSearch( self.pages.search );
  207. }
  208. } else {
  209. self.loadItems( [
  210. { type: type, object: object }
  211. ] );
  212. }
  213. }
  214. });
  215. // Close the panel if the URL in the preview changes
  216. api.previewer.bind( 'url', this.close );
  217. self.delegateEvents();
  218. },
  219. // Search input change handler.
  220. search: function( event ) {
  221. var $searchSection = $( '#available-menu-items-search' ),
  222. $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
  223. if ( ! event ) {
  224. return;
  225. }
  226. if ( this.searchTerm === event.target.value ) {
  227. return;
  228. }
  229. if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
  230. $otherSections.fadeOut( 100 );
  231. $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
  232. $searchSection.addClass( 'open' );
  233. this.$clearResults.addClass( 'is-visible' );
  234. } else if ( '' === event.target.value ) {
  235. $searchSection.removeClass( 'open' );
  236. $otherSections.show();
  237. this.$clearResults.removeClass( 'is-visible' );
  238. }
  239. this.searchTerm = event.target.value;
  240. this.pages.search = 1;
  241. this.doSearch( 1 );
  242. },
  243. // Get search results.
  244. doSearch: function( page ) {
  245. var self = this, params,
  246. $section = $( '#available-menu-items-search' ),
  247. $content = $section.find( '.accordion-section-content' ),
  248. itemTemplate = wp.template( 'available-menu-item' );
  249. if ( self.currentRequest ) {
  250. self.currentRequest.abort();
  251. }
  252. if ( page < 0 ) {
  253. return;
  254. } else if ( page > 1 ) {
  255. $section.addClass( 'loading-more' );
  256. $content.attr( 'aria-busy', 'true' );
  257. wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
  258. } else if ( '' === self.searchTerm ) {
  259. $content.html( '' );
  260. wp.a11y.speak( '' );
  261. return;
  262. }
  263. $section.addClass( 'loading' );
  264. self.loading = true;
  265. params = api.previewer.query( { excludeCustomizedSaved: true } );
  266. _.extend( params, {
  267. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  268. 'wp_customize': 'on',
  269. 'search': self.searchTerm,
  270. 'page': page
  271. } );
  272. self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
  273. self.currentRequest.done(function( data ) {
  274. var items;
  275. if ( 1 === page ) {
  276. // Clear previous results as it's a new search.
  277. $content.empty();
  278. }
  279. $section.removeClass( 'loading loading-more' );
  280. $content.attr( 'aria-busy', 'false' );
  281. $section.addClass( 'open' );
  282. self.loading = false;
  283. items = new api.Menus.AvailableItemCollection( data.items );
  284. self.collection.add( items.models );
  285. items.each( function( menuItem ) {
  286. $content.append( itemTemplate( menuItem.attributes ) );
  287. } );
  288. if ( 20 > items.length ) {
  289. self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
  290. } else {
  291. self.pages.search = self.pages.search + 1;
  292. }
  293. if ( items && page > 1 ) {
  294. wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
  295. } else if ( items && page === 1 ) {
  296. wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
  297. }
  298. });
  299. self.currentRequest.fail(function( data ) {
  300. // data.message may be undefined, for example when typing slow and the request is aborted.
  301. if ( data.message ) {
  302. $content.empty().append( $( '<li class="nothing-found"></li>' ).text( data.message ) );
  303. wp.a11y.speak( data.message );
  304. }
  305. self.pages.search = -1;
  306. });
  307. self.currentRequest.always(function() {
  308. $section.removeClass( 'loading loading-more' );
  309. $content.attr( 'aria-busy', 'false' );
  310. self.loading = false;
  311. self.currentRequest = null;
  312. });
  313. },
  314. // Render the individual items.
  315. initList: function() {
  316. var self = this;
  317. // Render the template for each item by type.
  318. _.each( api.Menus.data.itemTypes, function( itemType ) {
  319. self.pages[ itemType.type + ':' + itemType.object ] = 0;
  320. } );
  321. self.loadItems( api.Menus.data.itemTypes );
  322. },
  323. /**
  324. * Load available nav menu items.
  325. *
  326. * @since 4.3.0
  327. * @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
  328. * @access private
  329. *
  330. * @param {Array.<object>} itemTypes List of objects containing type and key.
  331. * @param {string} deprecated Formerly the object parameter.
  332. * @returns {void}
  333. */
  334. loadItems: function( itemTypes, deprecated ) {
  335. var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {};
  336. itemTemplate = wp.template( 'available-menu-item' );
  337. if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
  338. _itemTypes = [ { type: itemTypes, object: deprecated } ];
  339. } else {
  340. _itemTypes = itemTypes;
  341. }
  342. _.each( _itemTypes, function( itemType ) {
  343. var container, name = itemType.type + ':' + itemType.object;
  344. if ( -1 === self.pages[ name ] ) {
  345. return; // Skip types for which there are no more results.
  346. }
  347. container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
  348. container.find( '.accordion-section-title' ).addClass( 'loading' );
  349. availableMenuItemContainers[ name ] = container;
  350. requestItemTypes.push( {
  351. object: itemType.object,
  352. type: itemType.type,
  353. page: self.pages[ name ]
  354. } );
  355. } );
  356. if ( 0 === requestItemTypes.length ) {
  357. return;
  358. }
  359. self.loading = true;
  360. params = api.previewer.query( { excludeCustomizedSaved: true } );
  361. _.extend( params, {
  362. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  363. 'wp_customize': 'on',
  364. 'item_types': requestItemTypes
  365. } );
  366. request = wp.ajax.post( 'load-available-menu-items-customizer', params );
  367. request.done(function( data ) {
  368. var typeInner;
  369. _.each( data.items, function( typeItems, name ) {
  370. if ( 0 === typeItems.length ) {
  371. if ( 0 === self.pages[ name ] ) {
  372. availableMenuItemContainers[ name ].find( '.accordion-section-title' )
  373. .addClass( 'cannot-expand' )
  374. .removeClass( 'loading' )
  375. .find( '.accordion-section-title > button' )
  376. .prop( 'tabIndex', -1 );
  377. }
  378. self.pages[ name ] = -1;
  379. return;
  380. } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
  381. availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).click();
  382. }
  383. typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
  384. self.collection.add( typeItems.models );
  385. typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
  386. typeItems.each( function( menuItem ) {
  387. typeInner.append( itemTemplate( menuItem.attributes ) );
  388. } );
  389. self.pages[ name ] += 1;
  390. });
  391. });
  392. request.fail(function( data ) {
  393. if ( typeof console !== 'undefined' && console.error ) {
  394. console.error( data );
  395. }
  396. });
  397. request.always(function() {
  398. _.each( availableMenuItemContainers, function( container ) {
  399. container.find( '.accordion-section-title' ).removeClass( 'loading' );
  400. } );
  401. self.loading = false;
  402. });
  403. },
  404. // Adjust the height of each section of items to fit the screen.
  405. itemSectionHeight: function() {
  406. var sections, lists, totalHeight, accordionHeight, diff;
  407. totalHeight = window.innerHeight;
  408. sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
  409. lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
  410. accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
  411. diff = totalHeight - accordionHeight;
  412. if ( 120 < diff && 290 > diff ) {
  413. sections.css( 'max-height', diff );
  414. lists.css( 'max-height', ( diff - 60 ) );
  415. }
  416. },
  417. // Highlights a menu item.
  418. select: function( menuitemTpl ) {
  419. this.selected = $( menuitemTpl );
  420. this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
  421. this.selected.addClass( 'selected' );
  422. },
  423. // Highlights a menu item on focus.
  424. focus: function( event ) {
  425. this.select( $( event.currentTarget ) );
  426. },
  427. // Submit handler for keypress and click on menu item.
  428. _submit: function( event ) {
  429. // Only proceed with keypress if it is Enter or Spacebar
  430. if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
  431. return;
  432. }
  433. this.submit( $( event.currentTarget ) );
  434. },
  435. // Adds a selected menu item to the menu.
  436. submit: function( menuitemTpl ) {
  437. var menuitemId, menu_item;
  438. if ( ! menuitemTpl ) {
  439. menuitemTpl = this.selected;
  440. }
  441. if ( ! menuitemTpl || ! this.currentMenuControl ) {
  442. return;
  443. }
  444. this.select( menuitemTpl );
  445. menuitemId = $( this.selected ).data( 'menu-item-id' );
  446. menu_item = this.collection.findWhere( { id: menuitemId } );
  447. if ( ! menu_item ) {
  448. return;
  449. }
  450. this.currentMenuControl.addItemToMenu( menu_item.attributes );
  451. $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
  452. },
  453. // Submit handler for keypress and click on custom menu item.
  454. _submitLink: function( event ) {
  455. // Only proceed with keypress if it is Enter.
  456. if ( 'keypress' === event.type && 13 !== event.which ) {
  457. return;
  458. }
  459. this.submitLink();
  460. },
  461. // Adds the custom menu item to the menu.
  462. submitLink: function() {
  463. var menuItem,
  464. itemName = $( '#custom-menu-item-name' ),
  465. itemUrl = $( '#custom-menu-item-url' ),
  466. urlRegex;
  467. if ( ! this.currentMenuControl ) {
  468. return;
  469. }
  470. /*
  471. * Allow URLs including:
  472. * - http://example.com/
  473. * - //example.com
  474. * - /directory/
  475. * - ?query-param
  476. * - #target
  477. * - mailto:foo@example.com
  478. *
  479. * Any further validation will be handled on the server when the setting is attempted to be saved,
  480. * so this pattern does not need to be complete.
  481. */
  482. urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
  483. if ( '' === itemName.val() ) {
  484. itemName.addClass( 'invalid' );
  485. return;
  486. } else if ( ! urlRegex.test( itemUrl.val() ) ) {
  487. itemUrl.addClass( 'invalid' );
  488. return;
  489. }
  490. menuItem = {
  491. 'title': itemName.val(),
  492. 'url': itemUrl.val(),
  493. 'type': 'custom',
  494. 'type_label': api.Menus.data.l10n.custom_label,
  495. 'object': 'custom'
  496. };
  497. this.currentMenuControl.addItemToMenu( menuItem );
  498. // Reset the custom link form.
  499. itemUrl.val( 'http://' );
  500. itemName.val( '' );
  501. },
  502. /**
  503. * Submit handler for keypress (enter) on field and click on button.
  504. *
  505. * @since 4.7.0
  506. * @private
  507. *
  508. * @param {jQuery.Event} event Event.
  509. * @returns {void}
  510. */
  511. _submitNew: function( event ) {
  512. var container;
  513. // Only proceed with keypress if it is Enter.
  514. if ( 'keypress' === event.type && 13 !== event.which ) {
  515. return;
  516. }
  517. if ( this.addingNew ) {
  518. return;
  519. }
  520. container = $( event.target ).closest( '.accordion-section' );
  521. this.submitNew( container );
  522. },
  523. /**
  524. * Creates a new object and adds an associated menu item to the menu.
  525. *
  526. * @since 4.7.0
  527. * @private
  528. *
  529. * @param {jQuery} container
  530. * @returns {void}
  531. */
  532. submitNew: function( container ) {
  533. var panel = this,
  534. itemName = container.find( '.create-item-input' ),
  535. title = itemName.val(),
  536. dataContainer = container.find( '.available-menu-items-list' ),
  537. itemType = dataContainer.data( 'type' ),
  538. itemObject = dataContainer.data( 'object' ),
  539. itemTypeLabel = dataContainer.data( 'type_label' ),
  540. promise;
  541. if ( ! this.currentMenuControl ) {
  542. return;
  543. }
  544. // Only posts are supported currently.
  545. if ( 'post_type' !== itemType ) {
  546. return;
  547. }
  548. if ( '' === $.trim( itemName.val() ) ) {
  549. itemName.addClass( 'invalid' );
  550. itemName.focus();
  551. return;
  552. } else {
  553. itemName.removeClass( 'invalid' );
  554. container.find( '.accordion-section-title' ).addClass( 'loading' );
  555. }
  556. panel.addingNew = true;
  557. itemName.attr( 'disabled', 'disabled' );
  558. promise = api.Menus.insertAutoDraftPost( {
  559. post_title: title,
  560. post_type: itemObject
  561. } );
  562. promise.done( function( data ) {
  563. var availableItem, $content, itemElement;
  564. availableItem = new api.Menus.AvailableItemModel( {
  565. 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
  566. 'title': itemName.val(),
  567. 'type': itemType,
  568. 'type_label': itemTypeLabel,
  569. 'object': itemObject,
  570. 'object_id': data.post_id,
  571. 'url': data.url
  572. } );
  573. // Add new item to menu.
  574. panel.currentMenuControl.addItemToMenu( availableItem.attributes );
  575. // Add the new item to the list of available items.
  576. api.Menus.availableMenuItemsPanel.collection.add( availableItem );
  577. $content = container.find( '.available-menu-items-list' );
  578. itemElement = $( wp.template( 'available-menu-item' )( availableItem.attributes ) );
  579. itemElement.find( '.menu-item-handle:first' ).addClass( 'item-added' );
  580. $content.prepend( itemElement );
  581. $content.scrollTop();
  582. // Reset the create content form.
  583. itemName.val( '' ).removeAttr( 'disabled' );
  584. panel.addingNew = false;
  585. container.find( '.accordion-section-title' ).removeClass( 'loading' );
  586. } );
  587. },
  588. // Opens the panel.
  589. open: function( menuControl ) {
  590. var panel = this, close;
  591. this.currentMenuControl = menuControl;
  592. this.itemSectionHeight();
  593. if ( api.section.has( 'publish_settings' ) ) {
  594. api.section( 'publish_settings' ).collapse();
  595. }
  596. $( 'body' ).addClass( 'adding-menu-items' );
  597. close = function() {
  598. panel.close();
  599. $( this ).off( 'click', close );
  600. };
  601. $( '#customize-preview' ).on( 'click', close );
  602. // Collapse all controls.
  603. _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
  604. control.collapseForm();
  605. } );
  606. this.$el.find( '.selected' ).removeClass( 'selected' );
  607. this.$search.focus();
  608. },
  609. // Closes the panel
  610. close: function( options ) {
  611. options = options || {};
  612. if ( options.returnFocus && this.currentMenuControl ) {
  613. this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
  614. }
  615. this.currentMenuControl = null;
  616. this.selected = null;
  617. $( 'body' ).removeClass( 'adding-menu-items' );
  618. $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
  619. this.$search.val( '' ).trigger( 'keyup' );
  620. },
  621. // Add a few keyboard enhancements to the panel.
  622. keyboardAccessible: function( event ) {
  623. var isEnter = ( 13 === event.which ),
  624. isEsc = ( 27 === event.which ),
  625. isBackTab = ( 9 === event.which && event.shiftKey ),
  626. isSearchFocused = $( event.target ).is( this.$search );
  627. // If enter pressed but nothing entered, don't do anything
  628. if ( isEnter && ! this.$search.val() ) {
  629. return;
  630. }
  631. if ( isSearchFocused && isBackTab ) {
  632. this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
  633. event.preventDefault(); // Avoid additional back-tab.
  634. } else if ( isEsc ) {
  635. this.close( { returnFocus: true } );
  636. }
  637. }
  638. });
  639. /**
  640. * wp.customize.Menus.MenusPanel
  641. *
  642. * Customizer panel for menus. This is used only for screen options management.
  643. * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
  644. *
  645. * @class wp.customize.Menus.MenusPanel
  646. * @augments wp.customize.Panel
  647. */
  648. api.Menus.MenusPanel = api.Panel.extend(/** @lends wp.customize.Menus.MenusPanel.prototype */{
  649. attachEvents: function() {
  650. api.Panel.prototype.attachEvents.call( this );
  651. var panel = this,
  652. panelMeta = panel.container.find( '.panel-meta' ),
  653. help = panelMeta.find( '.customize-help-toggle' ),
  654. content = panelMeta.find( '.customize-panel-description' ),
  655. options = $( '#screen-options-wrap' ),
  656. button = panelMeta.find( '.customize-screen-options-toggle' );
  657. button.on( 'click keydown', function( event ) {
  658. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  659. return;
  660. }
  661. event.preventDefault();
  662. // Hide description
  663. if ( content.not( ':hidden' ) ) {
  664. content.slideUp( 'fast' );
  665. help.attr( 'aria-expanded', 'false' );
  666. }
  667. if ( 'true' === button.attr( 'aria-expanded' ) ) {
  668. button.attr( 'aria-expanded', 'false' );
  669. panelMeta.removeClass( 'open' );
  670. panelMeta.removeClass( 'active-menu-screen-options' );
  671. options.slideUp( 'fast' );
  672. } else {
  673. button.attr( 'aria-expanded', 'true' );
  674. panelMeta.addClass( 'open' );
  675. panelMeta.addClass( 'active-menu-screen-options' );
  676. options.slideDown( 'fast' );
  677. }
  678. return false;
  679. } );
  680. // Help toggle
  681. help.on( 'click keydown', function( event ) {
  682. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  683. return;
  684. }
  685. event.preventDefault();
  686. if ( 'true' === button.attr( 'aria-expanded' ) ) {
  687. button.attr( 'aria-expanded', 'false' );
  688. help.attr( 'aria-expanded', 'true' );
  689. panelMeta.addClass( 'open' );
  690. panelMeta.removeClass( 'active-menu-screen-options' );
  691. options.slideUp( 'fast' );
  692. content.slideDown( 'fast' );
  693. }
  694. } );
  695. },
  696. /**
  697. * Update field visibility when clicking on the field toggles.
  698. */
  699. ready: function() {
  700. var panel = this;
  701. panel.container.find( '.hide-column-tog' ).click( function() {
  702. panel.saveManageColumnsState();
  703. });
  704. // Inject additional heading into the menu locations section's head container.
  705. api.section( 'menu_locations', function( section ) {
  706. section.headContainer.prepend(
  707. wp.template( 'nav-menu-locations-header' )( api.Menus.data )
  708. );
  709. } );
  710. },
  711. /**
  712. * Save hidden column states.
  713. *
  714. * @since 4.3.0
  715. * @private
  716. *
  717. * @returns {void}
  718. */
  719. saveManageColumnsState: _.debounce( function() {
  720. var panel = this;
  721. if ( panel._updateHiddenColumnsRequest ) {
  722. panel._updateHiddenColumnsRequest.abort();
  723. }
  724. panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
  725. hidden: panel.hidden(),
  726. screenoptionnonce: $( '#screenoptionnonce' ).val(),
  727. page: 'nav-menus'
  728. } );
  729. panel._updateHiddenColumnsRequest.always( function() {
  730. panel._updateHiddenColumnsRequest = null;
  731. } );
  732. }, 2000 ),
  733. /**
  734. * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
  735. */
  736. checked: function() {},
  737. /**
  738. * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
  739. */
  740. unchecked: function() {},
  741. /**
  742. * Get hidden fields.
  743. *
  744. * @since 4.3.0
  745. * @private
  746. *
  747. * @returns {Array} Fields (columns) that are hidden.
  748. */
  749. hidden: function() {
  750. return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
  751. var id = this.id;
  752. return id.substring( 0, id.length - 5 );
  753. }).get().join( ',' );
  754. }
  755. } );
  756. /**
  757. * wp.customize.Menus.MenuSection
  758. *
  759. * Customizer section for menus. This is used only for lazy-loading child controls.
  760. * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
  761. *
  762. * @class wp.customize.Menus.MenuSection
  763. * @augments wp.customize.Section
  764. */
  765. api.Menus.MenuSection = api.Section.extend(/** @lends wp.customize.Menus.MenuSection.prototype */{
  766. /**
  767. * Initialize.
  768. *
  769. * @since 4.3.0
  770. *
  771. * @param {String} id
  772. * @param {Object} options
  773. */
  774. initialize: function( id, options ) {
  775. var section = this;
  776. api.Section.prototype.initialize.call( section, id, options );
  777. section.deferred.initSortables = $.Deferred();
  778. },
  779. /**
  780. * Ready.
  781. */
  782. ready: function() {
  783. var section = this, fieldActiveToggles, handleFieldActiveToggle;
  784. if ( 'undefined' === typeof section.params.menu_id ) {
  785. throw new Error( 'params.menu_id was not defined' );
  786. }
  787. /*
  788. * Since newly created sections won't be registered in PHP, we need to prevent the
  789. * preview's sending of the activeSections to result in this control
  790. * being deactivated when the preview refreshes. So we can hook onto
  791. * the setting that has the same ID and its presence can dictate
  792. * whether the section is active.
  793. */
  794. section.active.validate = function() {
  795. if ( ! api.has( section.id ) ) {
  796. return false;
  797. }
  798. return !! api( section.id ).get();
  799. };
  800. section.populateControls();
  801. section.navMenuLocationSettings = {};
  802. section.assignedLocations = new api.Value( [] );
  803. api.each(function( setting, id ) {
  804. var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
  805. if ( matches ) {
  806. section.navMenuLocationSettings[ matches[1] ] = setting;
  807. setting.bind( function() {
  808. section.refreshAssignedLocations();
  809. });
  810. }
  811. });
  812. section.assignedLocations.bind(function( to ) {
  813. section.updateAssignedLocationsInSectionTitle( to );
  814. });
  815. section.refreshAssignedLocations();
  816. api.bind( 'pane-contents-reflowed', function() {
  817. // Skip menus that have been removed.
  818. if ( ! section.contentContainer.parent().length ) {
  819. return;
  820. }
  821. section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
  822. section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  823. section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  824. section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  825. section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  826. } );
  827. /**
  828. * Update the active field class for the content container for a given checkbox toggle.
  829. *
  830. * @this {jQuery}
  831. * @returns {void}
  832. */
  833. handleFieldActiveToggle = function() {
  834. var className = 'field-' + $( this ).val() + '-active';
  835. section.contentContainer.toggleClass( className, $( this ).prop( 'checked' ) );
  836. };
  837. fieldActiveToggles = api.panel( 'nav_menus' ).contentContainer.find( '.metabox-prefs:first' ).find( '.hide-column-tog' );
  838. fieldActiveToggles.each( handleFieldActiveToggle );
  839. fieldActiveToggles.on( 'click', handleFieldActiveToggle );
  840. },
  841. populateControls: function() {
  842. var section = this,
  843. menuNameControlId,
  844. menuLocationsControlId,
  845. menuAutoAddControlId,
  846. menuDeleteControlId,
  847. menuControl,
  848. menuNameControl,
  849. menuLocationsControl,
  850. menuAutoAddControl,
  851. menuDeleteControl;
  852. // Add the control for managing the menu name.
  853. menuNameControlId = section.id + '[name]';
  854. menuNameControl = api.control( menuNameControlId );
  855. if ( ! menuNameControl ) {
  856. menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
  857. type: 'nav_menu_name',
  858. label: api.Menus.data.l10n.menuNameLabel,
  859. section: section.id,
  860. priority: 0,
  861. settings: {
  862. 'default': section.id
  863. }
  864. } );
  865. api.control.add( menuNameControl );
  866. menuNameControl.active.set( true );
  867. }
  868. // Add the menu control.
  869. menuControl = api.control( section.id );
  870. if ( ! menuControl ) {
  871. menuControl = new api.controlConstructor.nav_menu( section.id, {
  872. type: 'nav_menu',
  873. section: section.id,
  874. priority: 998,
  875. settings: {
  876. 'default': section.id
  877. },
  878. menu_id: section.params.menu_id
  879. } );
  880. api.control.add( menuControl );
  881. menuControl.active.set( true );
  882. }
  883. // Add the menu locations control.
  884. menuLocationsControlId = section.id + '[locations]';
  885. menuLocationsControl = api.control( menuLocationsControlId );
  886. if ( ! menuLocationsControl ) {
  887. menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
  888. section: section.id,
  889. priority: 999,
  890. settings: {
  891. 'default': section.id
  892. },
  893. menu_id: section.params.menu_id
  894. } );
  895. api.control.add( menuLocationsControl.id, menuLocationsControl );
  896. menuControl.active.set( true );
  897. }
  898. // Add the control for managing the menu auto_add.
  899. menuAutoAddControlId = section.id + '[auto_add]';
  900. menuAutoAddControl = api.control( menuAutoAddControlId );
  901. if ( ! menuAutoAddControl ) {
  902. menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
  903. type: 'nav_menu_auto_add',
  904. label: '',
  905. section: section.id,
  906. priority: 1000,
  907. settings: {
  908. 'default': section.id
  909. }
  910. } );
  911. api.control.add( menuAutoAddControl );
  912. menuAutoAddControl.active.set( true );
  913. }
  914. // Add the control for deleting the menu
  915. menuDeleteControlId = section.id + '[delete]';
  916. menuDeleteControl = api.control( menuDeleteControlId );
  917. if ( ! menuDeleteControl ) {
  918. menuDeleteControl = new api.Control( menuDeleteControlId, {
  919. section: section.id,
  920. priority: 1001,
  921. templateId: 'nav-menu-delete-button'
  922. } );
  923. api.control.add( menuDeleteControl.id, menuDeleteControl );
  924. menuDeleteControl.active.set( true );
  925. menuDeleteControl.deferred.embedded.done( function () {
  926. menuDeleteControl.container.find( 'button' ).on( 'click', function() {
  927. var menuId = section.params.menu_id;
  928. var menuControl = api.Menus.getMenuControl( menuId );
  929. menuControl.setting.set( false );
  930. });
  931. } );
  932. }
  933. },
  934. /**
  935. *
  936. */
  937. refreshAssignedLocations: function() {
  938. var section = this,
  939. menuTermId = section.params.menu_id,
  940. currentAssignedLocations = [];
  941. _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
  942. if ( setting() === menuTermId ) {
  943. currentAssignedLocations.push( themeLocation );
  944. }
  945. });
  946. section.assignedLocations.set( currentAssignedLocations );
  947. },
  948. /**
  949. * @param {Array} themeLocationSlugs Theme location slugs.
  950. */
  951. updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
  952. var section = this,
  953. $title;
  954. $title = section.container.find( '.accordion-section-title:first' );
  955. $title.find( '.menu-in-location' ).remove();
  956. _.each( themeLocationSlugs, function( themeLocationSlug ) {
  957. var $label, locationName;
  958. $label = $( '<span class="menu-in-location"></span>' );
  959. locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ];
  960. $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) );
  961. $title.append( $label );
  962. });
  963. section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length );
  964. },
  965. onChangeExpanded: function( expanded, args ) {
  966. var section = this, completeCallback;
  967. if ( expanded ) {
  968. wpNavMenu.menuList = section.contentContainer;
  969. wpNavMenu.targetList = wpNavMenu.menuList;
  970. // Add attributes needed by wpNavMenu
  971. $( '#menu-to-edit' ).removeAttr( 'id' );
  972. wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
  973. _.each( api.section( section.id ).controls(), function( control ) {
  974. if ( 'nav_menu_item' === control.params.type ) {
  975. control.actuallyEmbed();
  976. }
  977. } );
  978. // Make sure Sortables is initialized after the section has been expanded to prevent `offset` issues.
  979. if ( args.completeCallback ) {
  980. completeCallback = args.completeCallback;
  981. }
  982. args.completeCallback = function() {
  983. if ( 'resolved' !== section.deferred.initSortables.state() ) {
  984. wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
  985. section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
  986. // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated.
  987. api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
  988. }
  989. if ( _.isFunction( completeCallback ) ) {
  990. completeCallback();
  991. }
  992. };
  993. }
  994. api.Section.prototype.onChangeExpanded.call( section, expanded, args );
  995. },
  996. /**
  997. * Highlight how a user may create new menu items.
  998. *
  999. * This method reminds the user to create new menu items and how.
  1000. * It's exposed this way because this class knows best which UI needs
  1001. * highlighted but those expanding this section know more about why and
  1002. * when the affordance should be highlighted.
  1003. *
  1004. * @since 4.9.0
  1005. *
  1006. * @returns {void}
  1007. */
  1008. highlightNewItemButton: function() {
  1009. api.utils.highlightButton( this.contentContainer.find( '.add-new-menu-item' ), { delay: 2000 } );
  1010. }
  1011. });
  1012. /**
  1013. * Create a nav menu setting and section.
  1014. *
  1015. * @since 4.9.0
  1016. *
  1017. * @param {string} [name=''] Nav menu name.
  1018. * @returns {wp.customize.Menus.MenuSection} Added nav menu.
  1019. */
  1020. api.Menus.createNavMenu = function createNavMenu( name ) {
  1021. var customizeId, placeholderId, setting;
  1022. placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
  1023. customizeId = 'nav_menu[' + String( placeholderId ) + ']';
  1024. // Register the menu control setting.
  1025. setting = api.create( customizeId, customizeId, {}, {
  1026. type: 'nav_menu',
  1027. transport: api.Menus.data.settingTransport,
  1028. previewer: api.previewer
  1029. } );
  1030. setting.set( $.extend(
  1031. {},
  1032. api.Menus.data.defaultSettingValues.nav_menu,
  1033. {
  1034. name: name || ''
  1035. }
  1036. ) );
  1037. /*
  1038. * Add the menu section (and its controls).
  1039. * Note that this will automatically create the required controls
  1040. * inside via the Section's ready method.
  1041. */
  1042. return api.section.add( new api.Menus.MenuSection( customizeId, {
  1043. panel: 'nav_menus',
  1044. title: displayNavMenuName( name ),
  1045. customizeAction: api.Menus.data.l10n.customizingMenus,
  1046. priority: 10,
  1047. menu_id: placeholderId
  1048. } ) );
  1049. };
  1050. /**
  1051. * wp.customize.Menus.NewMenuSection
  1052. *
  1053. * Customizer section for new menus.
  1054. *
  1055. * @class wp.customize.Menus.NewMenuSection
  1056. * @augments wp.customize.Section
  1057. */
  1058. api.Menus.NewMenuSection = api.Section.extend(/** @lends wp.customize.Menus.NewMenuSection.prototype */{
  1059. /**
  1060. * Add behaviors for the accordion section.
  1061. *
  1062. * @since 4.3.0
  1063. */
  1064. attachEvents: function() {
  1065. var section = this,
  1066. container = section.container,
  1067. contentContainer = section.contentContainer,
  1068. navMenuSettingPattern = /^nav_menu\[/;
  1069. section.headContainer.find( '.accordion-section-title' ).replaceWith(
  1070. wp.template( 'nav-menu-create-menu-section-title' )
  1071. );
  1072. /*
  1073. * We have to manually handle section expanded because we do not
  1074. * apply the `accordion-section-title` class to this button-driven section.
  1075. */
  1076. container.on( 'click', '.customize-add-menu-button', function() {
  1077. section.expand();
  1078. });
  1079. contentContainer.on( 'keydown', '.menu-name-field', function( event ) {
  1080. if ( 13 === event.which ) { // Enter.
  1081. section.submit();
  1082. }
  1083. } );
  1084. contentContainer.on( 'click', '#customize-new-menu-submit', function( event ) {
  1085. section.submit();
  1086. event.stopPropagation();
  1087. event.preventDefault();
  1088. } );
  1089. /**
  1090. * Get number of non-deleted nav menus.
  1091. *
  1092. * @since 4.9.0
  1093. * @returns {number} Count.
  1094. */
  1095. function getNavMenuCount() {
  1096. var count = 0;
  1097. api.each( function( setting ) {
  1098. if ( navMenuSettingPattern.test( setting.id ) && false !== setting.get() ) {
  1099. count += 1;
  1100. }
  1101. } );
  1102. return count;
  1103. }
  1104. /**
  1105. * Update visibility of notice to prompt users to create menus.
  1106. *
  1107. * @since 4.9.0
  1108. * @returns {void}
  1109. */
  1110. function updateNoticeVisibility() {
  1111. container.find( '.add-new-menu-notice' ).prop( 'hidden', getNavMenuCount() > 0 );
  1112. }
  1113. /**
  1114. * Handle setting addition.
  1115. *
  1116. * @since 4.9.0
  1117. * @param {wp.customize.Setting} setting - Added setting.
  1118. * @returns {void}
  1119. */
  1120. function addChangeEventListener( setting ) {
  1121. if ( navMenuSettingPattern.test( setting.id ) ) {
  1122. setting.bind( updateNoticeVisibility );
  1123. updateNoticeVisibility();
  1124. }
  1125. }
  1126. /**
  1127. * Handle setting removal.
  1128. *
  1129. * @since 4.9.0
  1130. * @param {wp.customize.Setting} setting - Removed setting.
  1131. * @returns {void}
  1132. */
  1133. function removeChangeEventListener( setting ) {
  1134. if ( navMenuSettingPattern.test( setting.id ) ) {
  1135. setting.unbind( updateNoticeVisibility );
  1136. updateNoticeVisibility();
  1137. }
  1138. }
  1139. api.each( addChangeEventListener );
  1140. api.bind( 'add', addChangeEventListener );
  1141. api.bind( 'removed', removeChangeEventListener );
  1142. updateNoticeVisibility();
  1143. api.Section.prototype.attachEvents.apply( section, arguments );
  1144. },
  1145. /**
  1146. * Set up the control.
  1147. *
  1148. * @since 4.9.0
  1149. */
  1150. ready: function() {
  1151. this.populateControls();
  1152. },
  1153. /**
  1154. * Create the controls for this section.
  1155. *
  1156. * @since 4.9.0
  1157. */
  1158. populateControls: function() {
  1159. var section = this,
  1160. menuNameControlId,
  1161. menuLocationsControlId,
  1162. newMenuSubmitControlId,
  1163. menuNameControl,
  1164. menuLocationsControl,
  1165. newMenuSubmitControl;
  1166. menuNameControlId = section.id + '[name]';
  1167. menuNameControl = api.control( menuNameControlId );
  1168. if ( ! menuNameControl ) {
  1169. menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
  1170. label: api.Menus.data.l10n.menuNameLabel,
  1171. description: api.Menus.data.l10n.newMenuNameDescription,
  1172. section: section.id,
  1173. priority: 0
  1174. } );
  1175. api.control.add( menuNameControl.id, menuNameControl );
  1176. menuNameControl.active.set( true );
  1177. }
  1178. menuLocationsControlId = section.id + '[locations]';
  1179. menuLocationsControl = api.control( menuLocationsControlId );
  1180. if ( ! menuLocationsControl ) {
  1181. menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
  1182. section: section.id,
  1183. priority: 1,
  1184. menu_id: '',
  1185. isCreating: true
  1186. } );
  1187. api.control.add( menuLocationsControlId, menuLocationsControl );
  1188. menuLocationsControl.active.set( true );
  1189. }
  1190. newMenuSubmitControlId = section.id + '[submit]';
  1191. newMenuSubmitControl = api.control( newMenuSubmitControlId );
  1192. if ( !newMenuSubmitControl ) {
  1193. newMenuSubmitControl = new api.Control( newMenuSubmitControlId, {
  1194. section: section.id,
  1195. priority: 1,
  1196. templateId: 'nav-menu-submit-new-button'
  1197. } );
  1198. api.control.add( newMenuSubmitControlId, newMenuSubmitControl );
  1199. newMenuSubmitControl.active.set( true );
  1200. }
  1201. },
  1202. /**
  1203. * Create the new menu with name and location supplied by the user.
  1204. *
  1205. * @since 4.9.0
  1206. */
  1207. submit: function() {
  1208. var section = this,
  1209. contentContainer = section.contentContainer,
  1210. nameInput = contentContainer.find( '.menu-name-field' ).first(),
  1211. name = nameInput.val(),
  1212. menuSection;
  1213. if ( ! name ) {
  1214. nameInput.addClass( 'invalid' );
  1215. nameInput.focus();
  1216. return;
  1217. }
  1218. menuSection = api.Menus.createNavMenu( name );
  1219. // Clear name field.
  1220. nameInput.val( '' );
  1221. nameInput.removeClass( 'invalid' );
  1222. contentContainer.find( '.assigned-menu-location input[type=checkbox]' ).each( function() {
  1223. var checkbox = $( this ),
  1224. navMenuLocationSetting;
  1225. if ( checkbox.prop( 'checked' ) ) {
  1226. navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
  1227. navMenuLocationSetting.set( menuSection.params.menu_id );
  1228. // Reset state for next new menu
  1229. checkbox.prop( 'checked', false );
  1230. }
  1231. } );
  1232. wp.a11y.speak( api.Menus.data.l10n.menuAdded );
  1233. // Focus on the new menu section.
  1234. menuSection.focus( {
  1235. completeCallback: function() {
  1236. menuSection.highlightNewItemButton();
  1237. }
  1238. } );
  1239. },
  1240. /**
  1241. * Select a default location.
  1242. *
  1243. * This method selects a single location by default so we can support
  1244. * creating a menu for a specific menu location.
  1245. *
  1246. * @since 4.9.0
  1247. *
  1248. * @param {string|null} locationId - The ID of the location to select. `null` clears all selections.
  1249. * @returns {void}
  1250. */
  1251. selectDefaultLocation: function( locationId ) {
  1252. var locationControl = api.control( this.id + '[locations]' ),
  1253. locationSelections = {};
  1254. if ( locationId !== null ) {
  1255. locationSelections[ locationId ] = true;
  1256. }
  1257. locationControl.setSelections( locationSelections );
  1258. }
  1259. });
  1260. /**
  1261. * wp.customize.Menus.MenuLocationControl
  1262. *
  1263. * Customizer control for menu locations (rendered as a <select>).
  1264. * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
  1265. *
  1266. * @class wp.customize.Menus.MenuLocationControl
  1267. * @augments wp.customize.Control
  1268. */
  1269. api.Menus.MenuLocationControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationControl.prototype */{
  1270. initialize: function( id, options ) {
  1271. var control = this,
  1272. matches = id.match( /^nav_menu_locations\[(.+?)]/ );
  1273. control.themeLocation = matches[1];
  1274. api.Control.prototype.initialize.call( control, id, options );
  1275. },
  1276. ready: function() {
  1277. var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
  1278. // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
  1279. control.setting.validate = function( value ) {
  1280. if ( '' === value ) {
  1281. return 0;
  1282. } else {
  1283. return parseInt( value, 10 );
  1284. }
  1285. };
  1286. // Create and Edit menu buttons.
  1287. control.container.find( '.create-menu' ).on( 'click', function() {
  1288. var addMenuSection = api.section( 'add_menu' );
  1289. addMenuSection.selectDefaultLocation( this.dataset.locationId );
  1290. addMenuSection.focus();
  1291. } );
  1292. control.container.find( '.edit-menu' ).on( 'click', function() {
  1293. var menuId = control.setting();
  1294. api.section( 'nav_menu[' + menuId + ']' ).focus();
  1295. });
  1296. control.setting.bind( 'change', function() {
  1297. var menuIsSelected = 0 !== control.setting();
  1298. control.container.find( '.create-menu' ).toggleClass( 'hidden', menuIsSelected );
  1299. control.container.find( '.edit-menu' ).toggleClass( 'hidden', ! menuIsSelected );
  1300. });
  1301. // Add/remove menus from the available options when they are added and removed.
  1302. api.bind( 'add', function( setting ) {
  1303. var option, menuId, matches = setting.id.match( navMenuIdRegex );
  1304. if ( ! matches || false === setting() ) {
  1305. return;
  1306. }
  1307. menuId = matches[1];
  1308. option = new Option( displayNavMenuName( setting().name ), menuId );
  1309. control.container.find( 'select' ).append( option );
  1310. });
  1311. api.bind( 'remove', function( setting ) {
  1312. var menuId, matches = setting.id.match( navMenuIdRegex );
  1313. if ( ! matches ) {
  1314. return;
  1315. }
  1316. menuId = parseInt( matches[1], 10 );
  1317. if ( control.setting() === menuId ) {
  1318. control.setting.set( '' );
  1319. }
  1320. control.container.find( 'option[value=' + menuId + ']' ).remove();
  1321. });
  1322. api.bind( 'change', function( setting ) {
  1323. var menuId, matches = setting.id.match( navMenuIdRegex );
  1324. if ( ! matches ) {
  1325. return;
  1326. }
  1327. menuId = parseInt( matches[1], 10 );
  1328. if ( false === setting() ) {
  1329. if ( control.setting() === menuId ) {
  1330. control.setting.set( '' );
  1331. }
  1332. control.container.find( 'option[value=' + menuId + ']' ).remove();
  1333. } else {
  1334. control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
  1335. }
  1336. });
  1337. }
  1338. });
  1339. api.Menus.MenuItemControl = api.Control.extend(/** @lends wp.customize.Menus.MenuItemControl.prototype */{
  1340. /**
  1341. * wp.customize.Menus.MenuItemControl
  1342. *
  1343. * Customizer control for menu items.
  1344. * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
  1345. *
  1346. * @constructs wp.customize.Menus.MenuItemControl
  1347. * @augments wp.customize.Control
  1348. *
  1349. * @inheritDoc
  1350. */
  1351. initialize: function( id, options ) {
  1352. var control = this;
  1353. control.expanded = new api.Value( false );
  1354. control.expandedArgumentsQueue = [];
  1355. control.expanded.bind( function( expanded ) {
  1356. var args = control.expandedArgumentsQueue.shift();
  1357. args = $.extend( {}, control.defaultExpandedArguments, args );
  1358. control.onChangeExpanded( expanded, args );
  1359. });
  1360. api.Control.prototype.initialize.call( control, id, options );
  1361. control.active.validate = function() {
  1362. var value, section = api.section( control.section() );
  1363. if ( section ) {
  1364. value = section.active();
  1365. } else {
  1366. value = false;
  1367. }
  1368. return value;
  1369. };
  1370. },
  1371. /**
  1372. * Override the embed() method to do nothing,
  1373. * so that the control isn't embedded on load,
  1374. * unless the containing section is already expanded.
  1375. *
  1376. * @since 4.3.0
  1377. */
  1378. embed: function() {
  1379. var control = this,
  1380. sectionId = control.section(),
  1381. section;
  1382. if ( ! sectionId ) {
  1383. return;
  1384. }
  1385. section = api.section( sectionId );
  1386. if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
  1387. control.actuallyEmbed();
  1388. }
  1389. },
  1390. /**
  1391. * This function is called in Section.onChangeExpanded() so the control
  1392. * will only get embedded when the Section is first expanded.
  1393. *
  1394. * @since 4.3.0
  1395. */
  1396. actuallyEmbed: function() {
  1397. var control = this;
  1398. if ( 'resolved' === control.deferred.embedded.state() ) {
  1399. return;
  1400. }
  1401. control.renderContent();
  1402. control.deferred.embedded.resolve(); // This triggers control.ready().
  1403. },
  1404. /**
  1405. * Set up the control.
  1406. */
  1407. ready: function() {
  1408. if ( 'undefined' === typeof this.params.menu_item_id ) {
  1409. throw new Error( 'params.menu_item_id was not defined' );
  1410. }
  1411. this._setupControlToggle();
  1412. this._setupReorderUI();
  1413. this._setupUpdateUI();
  1414. this._setupRemoveUI();
  1415. this._setupLinksUI();
  1416. this._setupTitleUI();
  1417. },
  1418. /**
  1419. * Show/hide the settings when clicking on the menu item handle.
  1420. */
  1421. _setupControlToggle: function() {
  1422. var control = this;
  1423. this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
  1424. e.preventDefault();
  1425. e.stopPropagation();
  1426. var menuControl = control.getMenuControl(),
  1427. isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
  1428. isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
  1429. if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
  1430. api.Menus.availableMenuItemsPanel.close();
  1431. }
  1432. if ( menuControl.isReordering || menuControl.isSorting ) {
  1433. return;
  1434. }
  1435. control.toggleForm();
  1436. } );
  1437. },
  1438. /**
  1439. * Set up the menu-item-reorder-nav
  1440. */
  1441. _setupReorderUI: function() {
  1442. var control = this, template, $reorderNav;
  1443. template = wp.template( 'menu-item-reorder-nav' );
  1444. // Add the menu item reordering elements to the menu item control.
  1445. control.container.find( '.item-controls' ).after( template );
  1446. // Handle clicks for up/down/left-right on the reorder nav.
  1447. $reorderNav = control.container.find( '.menu-item-reorder-nav' );