Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 

3121 řádky
96 KiB

  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. api.Menus = api.Menus || {};
  15. // Link settings.
  16. api.Menus.data = {
  17. itemTypes: [],
  18. l10n: {},
  19. settingTransport: 'refresh',
  20. phpIntMax: 0,
  21. defaultSettingValues: {
  22. nav_menu: {},
  23. nav_menu_item: {}
  24. },
  25. locationSlugMappedToName: {}
  26. };
  27. if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
  28. $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
  29. }
  30. /**
  31. * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
  32. * serve as placeholders until Save & Publish happens.
  33. *
  34. * @return {number}
  35. */
  36. api.Menus.generatePlaceholderAutoIncrementId = function() {
  37. return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
  38. };
  39. /**
  40. * wp.customize.Menus.AvailableItemModel
  41. *
  42. * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
  43. *
  44. * @constructor
  45. * @augments Backbone.Model
  46. */
  47. api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
  48. {
  49. id: null // This is only used by Backbone.
  50. },
  51. api.Menus.data.defaultSettingValues.nav_menu_item
  52. ) );
  53. /**
  54. * wp.customize.Menus.AvailableItemCollection
  55. *
  56. * Collection for available menu item models.
  57. *
  58. * @constructor
  59. * @augments Backbone.Model
  60. */
  61. api.Menus.AvailableItemCollection = Backbone.Collection.extend({
  62. model: api.Menus.AvailableItemModel,
  63. sort_key: 'order',
  64. comparator: function( item ) {
  65. return -item.get( this.sort_key );
  66. },
  67. sortByField: function( fieldName ) {
  68. this.sort_key = fieldName;
  69. this.sort();
  70. }
  71. });
  72. api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
  73. /**
  74. * Insert a new `auto-draft` post.
  75. *
  76. * @since 4.7.0
  77. * @access public
  78. *
  79. * @param {object} params - Parameters for the draft post to create.
  80. * @param {string} params.post_type - Post type to add.
  81. * @param {string} params.post_title - Post title to use.
  82. * @return {jQuery.promise} Promise resolved with the added post.
  83. */
  84. api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
  85. var request, deferred = $.Deferred();
  86. request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
  87. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  88. 'wp_customize': 'on',
  89. 'params': params
  90. } );
  91. request.done( function( response ) {
  92. if ( response.post_id ) {
  93. api( 'nav_menus_created_posts' ).set(
  94. api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] )
  95. );
  96. if ( 'page' === params.post_type ) {
  97. // Activate static front page controls as this could be the first page created.
  98. if ( api.section.has( 'static_front_page' ) ) {
  99. api.section( 'static_front_page' ).activate();
  100. }
  101. // Add new page to dropdown-pages controls.
  102. api.control.each( function( control ) {
  103. var select;
  104. if ( 'dropdown-pages' === control.params.type ) {
  105. select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' );
  106. select.append( new Option( params.post_title, response.post_id ) );
  107. }
  108. } );
  109. }
  110. deferred.resolve( response );
  111. }
  112. } );
  113. request.fail( function( response ) {
  114. var error = response || '';
  115. if ( 'undefined' !== typeof response.message ) {
  116. error = response.message;
  117. }
  118. console.error( error );
  119. deferred.rejectWith( error );
  120. } );
  121. return deferred.promise();
  122. };
  123. /**
  124. * wp.customize.Menus.AvailableMenuItemsPanelView
  125. *
  126. * View class for the available menu items panel.
  127. *
  128. * @constructor
  129. * @augments wp.Backbone.View
  130. * @augments Backbone.View
  131. */
  132. api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend({
  133. el: '#available-menu-items',
  134. events: {
  135. 'input #menu-items-search': 'debounceSearch',
  136. 'keyup #menu-items-search': 'debounceSearch',
  137. 'focus .menu-item-tpl': 'focus',
  138. 'click .menu-item-tpl': '_submit',
  139. 'click #custom-menu-item-submit': '_submitLink',
  140. 'keypress #custom-menu-item-name': '_submitLink',
  141. 'click .new-content-item .add-content': '_submitNew',
  142. 'keypress .create-item-input': '_submitNew',
  143. 'keydown': 'keyboardAccessible'
  144. },
  145. // Cache current selected menu item.
  146. selected: null,
  147. // Cache menu control that opened the panel.
  148. currentMenuControl: null,
  149. debounceSearch: null,
  150. $search: null,
  151. $clearResults: null,
  152. searchTerm: '',
  153. rendered: false,
  154. pages: {},
  155. sectionContent: '',
  156. loading: false,
  157. addingNew: false,
  158. initialize: function() {
  159. var self = this;
  160. if ( ! api.panel.has( 'nav_menus' ) ) {
  161. return;
  162. }
  163. this.$search = $( '#menu-items-search' );
  164. this.$clearResults = this.$el.find( '.clear-results' );
  165. this.sectionContent = this.$el.find( '.available-menu-items-list' );
  166. this.debounceSearch = _.debounce( self.search, 500 );
  167. _.bindAll( this, 'close' );
  168. // If the available menu items panel is open and the customize controls are
  169. // interacted with (other than an item being deleted), then close the
  170. // available menu items panel. Also close on back button click.
  171. $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
  172. var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
  173. isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
  174. if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
  175. self.close();
  176. }
  177. } );
  178. // Clear the search results and trigger a `keyup` event to fire a new search.
  179. this.$clearResults.on( 'click', function() {
  180. self.$search.val( '' ).focus().trigger( 'keyup' );
  181. } );
  182. this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
  183. $( this ).removeClass( 'invalid' );
  184. });
  185. // Load available items if it looks like we'll need them.
  186. api.panel( 'nav_menus' ).container.bind( 'expanded', function() {
  187. if ( ! self.rendered ) {
  188. self.initList();
  189. self.rendered = true;
  190. }
  191. });
  192. // Load more items.
  193. this.sectionContent.scroll( function() {
  194. var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
  195. visibleHeight = self.$el.find( '.accordion-section.open' ).height();
  196. if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
  197. var type = $( this ).data( 'type' ),
  198. object = $( this ).data( 'object' );
  199. if ( 'search' === type ) {
  200. if ( self.searchTerm ) {
  201. self.doSearch( self.pages.search );
  202. }
  203. } else {
  204. self.loadItems( [
  205. { type: type, object: object }
  206. ] );
  207. }
  208. }
  209. });
  210. // Close the panel if the URL in the preview changes
  211. api.previewer.bind( 'url', this.close );
  212. self.delegateEvents();
  213. },
  214. // Search input change handler.
  215. search: function( event ) {
  216. var $searchSection = $( '#available-menu-items-search' ),
  217. $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
  218. if ( ! event ) {
  219. return;
  220. }
  221. if ( this.searchTerm === event.target.value ) {
  222. return;
  223. }
  224. if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
  225. $otherSections.fadeOut( 100 );
  226. $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
  227. $searchSection.addClass( 'open' );
  228. this.$clearResults.addClass( 'is-visible' );
  229. } else if ( '' === event.target.value ) {
  230. $searchSection.removeClass( 'open' );
  231. $otherSections.show();
  232. this.$clearResults.removeClass( 'is-visible' );
  233. }
  234. this.searchTerm = event.target.value;
  235. this.pages.search = 1;
  236. this.doSearch( 1 );
  237. },
  238. // Get search results.
  239. doSearch: function( page ) {
  240. var self = this, params,
  241. $section = $( '#available-menu-items-search' ),
  242. $content = $section.find( '.accordion-section-content' ),
  243. itemTemplate = wp.template( 'available-menu-item' );
  244. if ( self.currentRequest ) {
  245. self.currentRequest.abort();
  246. }
  247. if ( page < 0 ) {
  248. return;
  249. } else if ( page > 1 ) {
  250. $section.addClass( 'loading-more' );
  251. $content.attr( 'aria-busy', 'true' );
  252. wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
  253. } else if ( '' === self.searchTerm ) {
  254. $content.html( '' );
  255. wp.a11y.speak( '' );
  256. return;
  257. }
  258. $section.addClass( 'loading' );
  259. self.loading = true;
  260. params = api.previewer.query( { excludeCustomizedSaved: true } );
  261. _.extend( params, {
  262. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  263. 'wp_customize': 'on',
  264. 'search': self.searchTerm,
  265. 'page': page
  266. } );
  267. self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
  268. self.currentRequest.done(function( data ) {
  269. var items;
  270. if ( 1 === page ) {
  271. // Clear previous results as it's a new search.
  272. $content.empty();
  273. }
  274. $section.removeClass( 'loading loading-more' );
  275. $content.attr( 'aria-busy', 'false' );
  276. $section.addClass( 'open' );
  277. self.loading = false;
  278. items = new api.Menus.AvailableItemCollection( data.items );
  279. self.collection.add( items.models );
  280. items.each( function( menuItem ) {
  281. $content.append( itemTemplate( menuItem.attributes ) );
  282. } );
  283. if ( 20 > items.length ) {
  284. self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
  285. } else {
  286. self.pages.search = self.pages.search + 1;
  287. }
  288. if ( items && page > 1 ) {
  289. wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
  290. } else if ( items && page === 1 ) {
  291. wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
  292. }
  293. });
  294. self.currentRequest.fail(function( data ) {
  295. // data.message may be undefined, for example when typing slow and the request is aborted.
  296. if ( data.message ) {
  297. $content.empty().append( $( '<li class="nothing-found"></li>' ).text( data.message ) );
  298. wp.a11y.speak( data.message );
  299. }
  300. self.pages.search = -1;
  301. });
  302. self.currentRequest.always(function() {
  303. $section.removeClass( 'loading loading-more' );
  304. $content.attr( 'aria-busy', 'false' );
  305. self.loading = false;
  306. self.currentRequest = null;
  307. });
  308. },
  309. // Render the individual items.
  310. initList: function() {
  311. var self = this;
  312. // Render the template for each item by type.
  313. _.each( api.Menus.data.itemTypes, function( itemType ) {
  314. self.pages[ itemType.type + ':' + itemType.object ] = 0;
  315. } );
  316. self.loadItems( api.Menus.data.itemTypes );
  317. },
  318. /**
  319. * Load available nav menu items.
  320. *
  321. * @since 4.3.0
  322. * @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
  323. * @access private
  324. *
  325. * @param {Array.<object>} itemTypes List of objects containing type and key.
  326. * @param {string} deprecated Formerly the object parameter.
  327. * @returns {void}
  328. */
  329. loadItems: function( itemTypes, deprecated ) {
  330. var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {};
  331. itemTemplate = wp.template( 'available-menu-item' );
  332. if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
  333. _itemTypes = [ { type: itemTypes, object: deprecated } ];
  334. } else {
  335. _itemTypes = itemTypes;
  336. }
  337. _.each( _itemTypes, function( itemType ) {
  338. var container, name = itemType.type + ':' + itemType.object;
  339. if ( -1 === self.pages[ name ] ) {
  340. return; // Skip types for which there are no more results.
  341. }
  342. container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
  343. container.find( '.accordion-section-title' ).addClass( 'loading' );
  344. availableMenuItemContainers[ name ] = container;
  345. requestItemTypes.push( {
  346. object: itemType.object,
  347. type: itemType.type,
  348. page: self.pages[ name ]
  349. } );
  350. } );
  351. if ( 0 === requestItemTypes.length ) {
  352. return;
  353. }
  354. self.loading = true;
  355. params = api.previewer.query( { excludeCustomizedSaved: true } );
  356. _.extend( params, {
  357. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  358. 'wp_customize': 'on',
  359. 'item_types': requestItemTypes
  360. } );
  361. request = wp.ajax.post( 'load-available-menu-items-customizer', params );
  362. request.done(function( data ) {
  363. var typeInner;
  364. _.each( data.items, function( typeItems, name ) {
  365. if ( 0 === typeItems.length ) {
  366. if ( 0 === self.pages[ name ] ) {
  367. availableMenuItemContainers[ name ].find( '.accordion-section-title' )
  368. .addClass( 'cannot-expand' )
  369. .removeClass( 'loading' )
  370. .find( '.accordion-section-title > button' )
  371. .prop( 'tabIndex', -1 );
  372. }
  373. self.pages[ name ] = -1;
  374. return;
  375. } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
  376. availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).click();
  377. }
  378. typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
  379. self.collection.add( typeItems.models );
  380. typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
  381. typeItems.each( function( menuItem ) {
  382. typeInner.append( itemTemplate( menuItem.attributes ) );
  383. } );
  384. self.pages[ name ] += 1;
  385. });
  386. });
  387. request.fail(function( data ) {
  388. if ( typeof console !== 'undefined' && console.error ) {
  389. console.error( data );
  390. }
  391. });
  392. request.always(function() {
  393. _.each( availableMenuItemContainers, function( container ) {
  394. container.find( '.accordion-section-title' ).removeClass( 'loading' );
  395. } );
  396. self.loading = false;
  397. });
  398. },
  399. // Adjust the height of each section of items to fit the screen.
  400. itemSectionHeight: function() {
  401. var sections, lists, totalHeight, accordionHeight, diff;
  402. totalHeight = window.innerHeight;
  403. sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
  404. lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
  405. accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
  406. diff = totalHeight - accordionHeight;
  407. if ( 120 < diff && 290 > diff ) {
  408. sections.css( 'max-height', diff );
  409. lists.css( 'max-height', ( diff - 60 ) );
  410. }
  411. },
  412. // Highlights a menu item.
  413. select: function( menuitemTpl ) {
  414. this.selected = $( menuitemTpl );
  415. this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
  416. this.selected.addClass( 'selected' );
  417. },
  418. // Highlights a menu item on focus.
  419. focus: function( event ) {
  420. this.select( $( event.currentTarget ) );
  421. },
  422. // Submit handler for keypress and click on menu item.
  423. _submit: function( event ) {
  424. // Only proceed with keypress if it is Enter or Spacebar
  425. if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
  426. return;
  427. }
  428. this.submit( $( event.currentTarget ) );
  429. },
  430. // Adds a selected menu item to the menu.
  431. submit: function( menuitemTpl ) {
  432. var menuitemId, menu_item;
  433. if ( ! menuitemTpl ) {
  434. menuitemTpl = this.selected;
  435. }
  436. if ( ! menuitemTpl || ! this.currentMenuControl ) {
  437. return;
  438. }
  439. this.select( menuitemTpl );
  440. menuitemId = $( this.selected ).data( 'menu-item-id' );
  441. menu_item = this.collection.findWhere( { id: menuitemId } );
  442. if ( ! menu_item ) {
  443. return;
  444. }
  445. this.currentMenuControl.addItemToMenu( menu_item.attributes );
  446. $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
  447. },
  448. // Submit handler for keypress and click on custom menu item.
  449. _submitLink: function( event ) {
  450. // Only proceed with keypress if it is Enter.
  451. if ( 'keypress' === event.type && 13 !== event.which ) {
  452. return;
  453. }
  454. this.submitLink();
  455. },
  456. // Adds the custom menu item to the menu.
  457. submitLink: function() {
  458. var menuItem,
  459. itemName = $( '#custom-menu-item-name' ),
  460. itemUrl = $( '#custom-menu-item-url' );
  461. if ( ! this.currentMenuControl ) {
  462. return;
  463. }
  464. if ( '' === itemName.val() ) {
  465. itemName.addClass( 'invalid' );
  466. return;
  467. } else if ( '' === itemUrl.val() || 'http://' === itemUrl.val() ) {
  468. itemUrl.addClass( 'invalid' );
  469. return;
  470. }
  471. menuItem = {
  472. 'title': itemName.val(),
  473. 'url': itemUrl.val(),
  474. 'type': 'custom',
  475. 'type_label': api.Menus.data.l10n.custom_label,
  476. 'object': 'custom'
  477. };
  478. this.currentMenuControl.addItemToMenu( menuItem );
  479. // Reset the custom link form.
  480. itemUrl.val( 'http://' );
  481. itemName.val( '' );
  482. },
  483. /**
  484. * Submit handler for keypress (enter) on field and click on button.
  485. *
  486. * @since 4.7.0
  487. * @private
  488. *
  489. * @param {jQuery.Event} event Event.
  490. * @returns {void}
  491. */
  492. _submitNew: function( event ) {
  493. var container;
  494. // Only proceed with keypress if it is Enter.
  495. if ( 'keypress' === event.type && 13 !== event.which ) {
  496. return;
  497. }
  498. if ( this.addingNew ) {
  499. return;
  500. }
  501. container = $( event.target ).closest( '.accordion-section' );
  502. this.submitNew( container );
  503. },
  504. /**
  505. * Creates a new object and adds an associated menu item to the menu.
  506. *
  507. * @since 4.7.0
  508. * @private
  509. *
  510. * @param {jQuery} container
  511. * @returns {void}
  512. */
  513. submitNew: function( container ) {
  514. var panel = this,
  515. itemName = container.find( '.create-item-input' ),
  516. title = itemName.val(),
  517. dataContainer = container.find( '.available-menu-items-list' ),
  518. itemType = dataContainer.data( 'type' ),
  519. itemObject = dataContainer.data( 'object' ),
  520. itemTypeLabel = dataContainer.data( 'type_label' ),
  521. promise;
  522. if ( ! this.currentMenuControl ) {
  523. return;
  524. }
  525. // Only posts are supported currently.
  526. if ( 'post_type' !== itemType ) {
  527. return;
  528. }
  529. if ( '' === $.trim( itemName.val() ) ) {
  530. itemName.addClass( 'invalid' );
  531. itemName.focus();
  532. return;
  533. } else {
  534. itemName.removeClass( 'invalid' );
  535. container.find( '.accordion-section-title' ).addClass( 'loading' );
  536. }
  537. panel.addingNew = true;
  538. itemName.attr( 'disabled', 'disabled' );
  539. promise = api.Menus.insertAutoDraftPost( {
  540. post_title: title,
  541. post_type: itemObject
  542. } );
  543. promise.done( function( data ) {
  544. var availableItem, $content, itemElement;
  545. availableItem = new api.Menus.AvailableItemModel( {
  546. 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
  547. 'title': itemName.val(),
  548. 'type': itemType,
  549. 'type_label': itemTypeLabel,
  550. 'object': itemObject,
  551. 'object_id': data.post_id,
  552. 'url': data.url
  553. } );
  554. // Add new item to menu.
  555. panel.currentMenuControl.addItemToMenu( availableItem.attributes );
  556. // Add the new item to the list of available items.
  557. api.Menus.availableMenuItemsPanel.collection.add( availableItem );
  558. $content = container.find( '.available-menu-items-list' );
  559. itemElement = $( wp.template( 'available-menu-item' )( availableItem.attributes ) );
  560. itemElement.find( '.menu-item-handle:first' ).addClass( 'item-added' );
  561. $content.prepend( itemElement );
  562. $content.scrollTop();
  563. // Reset the create content form.
  564. itemName.val( '' ).removeAttr( 'disabled' );
  565. panel.addingNew = false;
  566. container.find( '.accordion-section-title' ).removeClass( 'loading' );
  567. } );
  568. },
  569. // Opens the panel.
  570. open: function( menuControl ) {
  571. this.currentMenuControl = menuControl;
  572. this.itemSectionHeight();
  573. $( 'body' ).addClass( 'adding-menu-items' );
  574. // Collapse all controls.
  575. _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
  576. control.collapseForm();
  577. } );
  578. this.$el.find( '.selected' ).removeClass( 'selected' );
  579. this.$search.focus();
  580. },
  581. // Closes the panel
  582. close: function( options ) {
  583. options = options || {};
  584. if ( options.returnFocus && this.currentMenuControl ) {
  585. this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
  586. }
  587. this.currentMenuControl = null;
  588. this.selected = null;
  589. $( 'body' ).removeClass( 'adding-menu-items' );
  590. $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
  591. this.$search.val( '' );
  592. },
  593. // Add a few keyboard enhancements to the panel.
  594. keyboardAccessible: function( event ) {
  595. var isEnter = ( 13 === event.which ),
  596. isEsc = ( 27 === event.which ),
  597. isBackTab = ( 9 === event.which && event.shiftKey ),
  598. isSearchFocused = $( event.target ).is( this.$search );
  599. // If enter pressed but nothing entered, don't do anything
  600. if ( isEnter && ! this.$search.val() ) {
  601. return;
  602. }
  603. if ( isSearchFocused && isBackTab ) {
  604. this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
  605. event.preventDefault(); // Avoid additional back-tab.
  606. } else if ( isEsc ) {
  607. this.close( { returnFocus: true } );
  608. }
  609. }
  610. });
  611. /**
  612. * wp.customize.Menus.MenusPanel
  613. *
  614. * Customizer panel for menus. This is used only for screen options management.
  615. * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
  616. *
  617. * @constructor
  618. * @augments wp.customize.Panel
  619. */
  620. api.Menus.MenusPanel = api.Panel.extend({
  621. attachEvents: function() {
  622. api.Panel.prototype.attachEvents.call( this );
  623. var panel = this,
  624. panelMeta = panel.container.find( '.panel-meta' ),
  625. help = panelMeta.find( '.customize-help-toggle' ),
  626. content = panelMeta.find( '.customize-panel-description' ),
  627. options = $( '#screen-options-wrap' ),
  628. button = panelMeta.find( '.customize-screen-options-toggle' );
  629. button.on( 'click keydown', function( event ) {
  630. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  631. return;
  632. }
  633. event.preventDefault();
  634. // Hide description
  635. if ( content.not( ':hidden' ) ) {
  636. content.slideUp( 'fast' );
  637. help.attr( 'aria-expanded', 'false' );
  638. }
  639. if ( 'true' === button.attr( 'aria-expanded' ) ) {
  640. button.attr( 'aria-expanded', 'false' );
  641. panelMeta.removeClass( 'open' );
  642. panelMeta.removeClass( 'active-menu-screen-options' );
  643. options.slideUp( 'fast' );
  644. } else {
  645. button.attr( 'aria-expanded', 'true' );
  646. panelMeta.addClass( 'open' );
  647. panelMeta.addClass( 'active-menu-screen-options' );
  648. options.slideDown( 'fast' );
  649. }
  650. return false;
  651. } );
  652. // Help toggle
  653. help.on( 'click keydown', function( event ) {
  654. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  655. return;
  656. }
  657. event.preventDefault();
  658. if ( 'true' === button.attr( 'aria-expanded' ) ) {
  659. button.attr( 'aria-expanded', 'false' );
  660. help.attr( 'aria-expanded', 'true' );
  661. panelMeta.addClass( 'open' );
  662. panelMeta.removeClass( 'active-menu-screen-options' );
  663. options.slideUp( 'fast' );
  664. content.slideDown( 'fast' );
  665. }
  666. } );
  667. },
  668. /**
  669. * Update field visibility when clicking on the field toggles.
  670. */
  671. ready: function() {
  672. var panel = this;
  673. panel.container.find( '.hide-column-tog' ).click( function() {
  674. panel.saveManageColumnsState();
  675. });
  676. },
  677. /**
  678. * Save hidden column states.
  679. *
  680. * @since 4.3.0
  681. * @private
  682. *
  683. * @returns {void}
  684. */
  685. saveManageColumnsState: _.debounce( function() {
  686. var panel = this;
  687. if ( panel._updateHiddenColumnsRequest ) {
  688. panel._updateHiddenColumnsRequest.abort();
  689. }
  690. panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
  691. hidden: panel.hidden(),
  692. screenoptionnonce: $( '#screenoptionnonce' ).val(),
  693. page: 'nav-menus'
  694. } );
  695. panel._updateHiddenColumnsRequest.always( function() {
  696. panel._updateHiddenColumnsRequest = null;
  697. } );
  698. }, 2000 ),
  699. /**
  700. * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
  701. */
  702. checked: function() {},
  703. /**
  704. * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
  705. */
  706. unchecked: function() {},
  707. /**
  708. * Get hidden fields.
  709. *
  710. * @since 4.3.0
  711. * @private
  712. *
  713. * @returns {Array} Fields (columns) that are hidden.
  714. */
  715. hidden: function() {
  716. return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
  717. var id = this.id;
  718. return id.substring( 0, id.length - 5 );
  719. }).get().join( ',' );
  720. }
  721. } );
  722. /**
  723. * wp.customize.Menus.MenuSection
  724. *
  725. * Customizer section for menus. This is used only for lazy-loading child controls.
  726. * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
  727. *
  728. * @constructor
  729. * @augments wp.customize.Section
  730. */
  731. api.Menus.MenuSection = api.Section.extend({
  732. /**
  733. * Initialize.
  734. *
  735. * @since 4.3.0
  736. *
  737. * @param {String} id
  738. * @param {Object} options
  739. */
  740. initialize: function( id, options ) {
  741. var section = this;
  742. api.Section.prototype.initialize.call( section, id, options );
  743. section.deferred.initSortables = $.Deferred();
  744. },
  745. /**
  746. * Ready.
  747. */
  748. ready: function() {
  749. var section = this, fieldActiveToggles, handleFieldActiveToggle;
  750. if ( 'undefined' === typeof section.params.menu_id ) {
  751. throw new Error( 'params.menu_id was not defined' );
  752. }
  753. /*
  754. * Since newly created sections won't be registered in PHP, we need to prevent the
  755. * preview's sending of the activeSections to result in this control
  756. * being deactivated when the preview refreshes. So we can hook onto
  757. * the setting that has the same ID and its presence can dictate
  758. * whether the section is active.
  759. */
  760. section.active.validate = function() {
  761. if ( ! api.has( section.id ) ) {
  762. return false;
  763. }
  764. return !! api( section.id ).get();
  765. };
  766. section.populateControls();
  767. section.navMenuLocationSettings = {};
  768. section.assignedLocations = new api.Value( [] );
  769. api.each(function( setting, id ) {
  770. var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
  771. if ( matches ) {
  772. section.navMenuLocationSettings[ matches[1] ] = setting;
  773. setting.bind( function() {
  774. section.refreshAssignedLocations();
  775. });
  776. }
  777. });
  778. section.assignedLocations.bind(function( to ) {
  779. section.updateAssignedLocationsInSectionTitle( to );
  780. });
  781. section.refreshAssignedLocations();
  782. api.bind( 'pane-contents-reflowed', function() {
  783. // Skip menus that have been removed.
  784. if ( ! section.contentContainer.parent().length ) {
  785. return;
  786. }
  787. section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
  788. section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  789. section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  790. section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  791. section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  792. } );
  793. /**
  794. * Update the active field class for the content container for a given checkbox toggle.
  795. *
  796. * @this {jQuery}
  797. * @returns {void}
  798. */
  799. handleFieldActiveToggle = function() {
  800. var className = 'field-' + $( this ).val() + '-active';
  801. section.contentContainer.toggleClass( className, $( this ).prop( 'checked' ) );
  802. };
  803. fieldActiveToggles = api.panel( 'nav_menus' ).contentContainer.find( '.metabox-prefs:first' ).find( '.hide-column-tog' );
  804. fieldActiveToggles.each( handleFieldActiveToggle );
  805. fieldActiveToggles.on( 'click', handleFieldActiveToggle );
  806. },
  807. populateControls: function() {
  808. var section = this, menuNameControlId, menuAutoAddControlId, menuControl, menuNameControl, menuAutoAddControl;
  809. // Add the control for managing the menu name.
  810. menuNameControlId = section.id + '[name]';
  811. menuNameControl = api.control( menuNameControlId );
  812. if ( ! menuNameControl ) {
  813. menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
  814. params: {
  815. type: 'nav_menu_name',
  816. content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '-name" class="customize-control customize-control-nav_menu_name"></li>', // @todo core should do this for us; see #30741
  817. label: api.Menus.data.l10n.menuNameLabel,
  818. active: true,
  819. section: section.id,
  820. priority: 0,
  821. settings: {
  822. 'default': section.id
  823. }
  824. }
  825. } );
  826. api.control.add( menuNameControl.id, menuNameControl );
  827. menuNameControl.active.set( true );
  828. }
  829. // Add the menu control.
  830. menuControl = api.control( section.id );
  831. if ( ! menuControl ) {
  832. menuControl = new api.controlConstructor.nav_menu( section.id, {
  833. params: {
  834. type: 'nav_menu',
  835. content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '" class="customize-control customize-control-nav_menu"></li>', // @todo core should do this for us; see #30741
  836. section: section.id,
  837. priority: 998,
  838. active: true,
  839. settings: {
  840. 'default': section.id
  841. },
  842. menu_id: section.params.menu_id
  843. }
  844. } );
  845. api.control.add( menuControl.id, menuControl );
  846. menuControl.active.set( true );
  847. }
  848. // Add the control for managing the menu auto_add.
  849. menuAutoAddControlId = section.id + '[auto_add]';
  850. menuAutoAddControl = api.control( menuAutoAddControlId );
  851. if ( ! menuAutoAddControl ) {
  852. menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
  853. params: {
  854. type: 'nav_menu_auto_add',
  855. content: '<li id="customize-control-' + section.id.replace( '[', '-' ).replace( ']', '' ) + '-auto-add" class="customize-control customize-control-nav_menu_auto_add"></li>', // @todo core should do this for us
  856. label: '',
  857. active: true,
  858. section: section.id,
  859. priority: 999,
  860. settings: {
  861. 'default': section.id
  862. }
  863. }
  864. } );
  865. api.control.add( menuAutoAddControl.id, menuAutoAddControl );
  866. menuAutoAddControl.active.set( true );
  867. }
  868. },
  869. /**
  870. *
  871. */
  872. refreshAssignedLocations: function() {
  873. var section = this,
  874. menuTermId = section.params.menu_id,
  875. currentAssignedLocations = [];
  876. _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
  877. if ( setting() === menuTermId ) {
  878. currentAssignedLocations.push( themeLocation );
  879. }
  880. });
  881. section.assignedLocations.set( currentAssignedLocations );
  882. },
  883. /**
  884. * @param {Array} themeLocationSlugs Theme location slugs.
  885. */
  886. updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
  887. var section = this,
  888. $title;
  889. $title = section.container.find( '.accordion-section-title:first' );
  890. $title.find( '.menu-in-location' ).remove();
  891. _.each( themeLocationSlugs, function( themeLocationSlug ) {
  892. var $label, locationName;
  893. $label = $( '<span class="menu-in-location"></span>' );
  894. locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ];
  895. $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) );
  896. $title.append( $label );
  897. });
  898. section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length );
  899. },
  900. onChangeExpanded: function( expanded, args ) {
  901. var section = this, completeCallback;
  902. if ( expanded ) {
  903. wpNavMenu.menuList = section.contentContainer;
  904. wpNavMenu.targetList = wpNavMenu.menuList;
  905. // Add attributes needed by wpNavMenu
  906. $( '#menu-to-edit' ).removeAttr( 'id' );
  907. wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
  908. _.each( api.section( section.id ).controls(), function( control ) {
  909. if ( 'nav_menu_item' === control.params.type ) {
  910. control.actuallyEmbed();
  911. }
  912. } );
  913. // Make sure Sortables is initialized after the section has been expanded to prevent `offset` issues.
  914. if ( args.completeCallback ) {
  915. completeCallback = args.completeCallback;
  916. }
  917. args.completeCallback = function() {
  918. if ( 'resolved' !== section.deferred.initSortables.state() ) {
  919. wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
  920. section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
  921. // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated.
  922. api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
  923. }
  924. if ( _.isFunction( completeCallback ) ) {
  925. completeCallback();
  926. }
  927. };
  928. }
  929. api.Section.prototype.onChangeExpanded.call( section, expanded, args );
  930. }
  931. });
  932. /**
  933. * wp.customize.Menus.NewMenuSection
  934. *
  935. * Customizer section for new menus.
  936. * Note that 'new_menu' must match the WP_Customize_New_Menu_Section::$type.
  937. *
  938. * @constructor
  939. * @augments wp.customize.Section
  940. */
  941. api.Menus.NewMenuSection = api.Section.extend({
  942. /**
  943. * Add behaviors for the accordion section.
  944. *
  945. * @since 4.3.0
  946. */
  947. attachEvents: function() {
  948. var section = this;
  949. this.container.on( 'click', '.add-menu-toggle', function() {
  950. if ( section.expanded() ) {
  951. section.collapse();
  952. } else {
  953. section.expand();
  954. }
  955. });
  956. },
  957. /**
  958. * Update UI to reflect expanded state.
  959. *
  960. * @since 4.1.0
  961. *
  962. * @param {Boolean} expanded
  963. */
  964. onChangeExpanded: function( expanded ) {
  965. var section = this,
  966. button = section.container.find( '.add-menu-toggle' ),
  967. content = section.contentContainer,
  968. customizer = section.headContainer.closest( '.wp-full-overlay-sidebar-content' );
  969. if ( expanded ) {
  970. button.addClass( 'open' );
  971. button.attr( 'aria-expanded', 'true' );
  972. content.slideDown( 'fast', function() {
  973. customizer.scrollTop( customizer.height() );
  974. });
  975. } else {
  976. button.removeClass( 'open' );
  977. button.attr( 'aria-expanded', 'false' );
  978. content.slideUp( 'fast' );
  979. content.find( '.menu-name-field' ).removeClass( 'invalid' );
  980. }
  981. },
  982. /**
  983. * Find the content element.
  984. *
  985. * @since 4.7.0
  986. *
  987. * @returns {jQuery} Content UL element.
  988. */
  989. getContent: function() {
  990. return this.container.find( 'ul:first' );
  991. }
  992. });
  993. /**
  994. * wp.customize.Menus.MenuLocationControl
  995. *
  996. * Customizer control for menu locations (rendered as a <select>).
  997. * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
  998. *
  999. * @constructor
  1000. * @augments wp.customize.Control
  1001. */
  1002. api.Menus.MenuLocationControl = api.Control.extend({
  1003. initialize: function( id, options ) {
  1004. var control = this,
  1005. matches = id.match( /^nav_menu_locations\[(.+?)]/ );
  1006. control.themeLocation = matches[1];
  1007. api.Control.prototype.initialize.call( control, id, options );
  1008. },
  1009. ready: function() {
  1010. var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
  1011. // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
  1012. control.setting.validate = function( value ) {
  1013. if ( '' === value ) {
  1014. return 0;
  1015. } else {
  1016. return parseInt( value, 10 );
  1017. }
  1018. };
  1019. // Edit menu button.
  1020. control.container.find( '.edit-menu' ).on( 'click', function() {
  1021. var menuId = control.setting();
  1022. api.section( 'nav_menu[' + menuId + ']' ).focus();
  1023. });
  1024. control.setting.bind( 'change', function() {
  1025. if ( 0 === control.setting() ) {
  1026. control.container.find( '.edit-menu' ).addClass( 'hidden' );
  1027. } else {
  1028. control.container.find( '.edit-menu' ).removeClass( 'hidden' );
  1029. }
  1030. });
  1031. // Add/remove menus from the available options when they are added and removed.
  1032. api.bind( 'add', function( setting ) {
  1033. var option, menuId, matches = setting.id.match( navMenuIdRegex );
  1034. if ( ! matches || false === setting() ) {
  1035. return;
  1036. }
  1037. menuId = matches[1];
  1038. option = new Option( displayNavMenuName( setting().name ), menuId );
  1039. control.container.find( 'select' ).append( option );
  1040. });
  1041. api.bind( 'remove', function( setting ) {
  1042. var menuId, matches = setting.id.match( navMenuIdRegex );
  1043. if ( ! matches ) {
  1044. return;
  1045. }
  1046. menuId = parseInt( matches[1], 10 );
  1047. if ( control.setting() === menuId ) {
  1048. control.setting.set( '' );
  1049. }
  1050. control.container.find( 'option[value=' + menuId + ']' ).remove();
  1051. });
  1052. api.bind( 'change', function( setting ) {
  1053. var menuId, matches = setting.id.match( navMenuIdRegex );
  1054. if ( ! matches ) {
  1055. return;
  1056. }
  1057. menuId = parseInt( matches[1], 10 );
  1058. if ( false === setting() ) {
  1059. if ( control.setting() === menuId ) {
  1060. control.setting.set( '' );
  1061. }
  1062. control.container.find( 'option[value=' + menuId + ']' ).remove();
  1063. } else {
  1064. control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
  1065. }
  1066. });
  1067. }
  1068. });
  1069. /**
  1070. * wp.customize.Menus.MenuItemControl
  1071. *
  1072. * Customizer control for menu items.
  1073. * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
  1074. *
  1075. * @constructor
  1076. * @augments wp.customize.Control
  1077. */
  1078. api.Menus.MenuItemControl = api.Control.extend({
  1079. /**
  1080. * @inheritdoc
  1081. */
  1082. initialize: function( id, options ) {
  1083. var control = this;
  1084. control.expanded = new api.Value( false );
  1085. control.expandedArgumentsQueue = [];
  1086. control.expanded.bind( function( expanded ) {
  1087. var args = control.expandedArgumentsQueue.shift();
  1088. args = $.extend( {}, control.defaultExpandedArguments, args );
  1089. control.onChangeExpanded( expanded, args );
  1090. });
  1091. api.Control.prototype.initialize.call( control, id, options );
  1092. control.active.validate = function() {
  1093. var value, section = api.section( control.section() );
  1094. if ( section ) {
  1095. value = section.active();
  1096. } else {
  1097. value = false;
  1098. }
  1099. return value;
  1100. };
  1101. },
  1102. /**
  1103. * Override the embed() method to do nothing,
  1104. * so that the control isn't embedded on load,
  1105. * unless the containing section is already expanded.
  1106. *
  1107. * @since 4.3.0
  1108. */
  1109. embed: function() {
  1110. var control = this,
  1111. sectionId = control.section(),
  1112. section;
  1113. if ( ! sectionId ) {
  1114. return;
  1115. }
  1116. section = api.section( sectionId );
  1117. if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
  1118. control.actuallyEmbed();
  1119. }
  1120. },
  1121. /**
  1122. * This function is called in Section.onChangeExpanded() so the control
  1123. * will only get embedded when the Section is first expanded.
  1124. *
  1125. * @since 4.3.0
  1126. */
  1127. actuallyEmbed: function() {
  1128. var control = this;
  1129. if ( 'resolved' === control.deferred.embedded.state() ) {
  1130. return;
  1131. }
  1132. control.renderContent();
  1133. control.deferred.embedded.resolve(); // This triggers control.ready().
  1134. },
  1135. /**
  1136. * Set up the control.
  1137. */
  1138. ready: function() {
  1139. if ( 'undefined' === typeof this.params.menu_item_id ) {
  1140. throw new Error( 'params.menu_item_id was not defined' );
  1141. }
  1142. this._setupControlToggle();
  1143. this._setupReorderUI();
  1144. this._setupUpdateUI();
  1145. this._setupRemoveUI();
  1146. this._setupLinksUI();
  1147. this._setupTitleUI();
  1148. },
  1149. /**
  1150. * Show/hide the settings when clicking on the menu item handle.
  1151. */
  1152. _setupControlToggle: function() {
  1153. var control = this;
  1154. this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
  1155. e.preventDefault();
  1156. e.stopPropagation();
  1157. var menuControl = control.getMenuControl();
  1158. if ( menuControl.isReordering || menuControl.isSorting ) {
  1159. return;
  1160. }
  1161. control.toggleForm();
  1162. } );
  1163. },
  1164. /**
  1165. * Set up the menu-item-reorder-nav
  1166. */
  1167. _setupReorderUI: function() {
  1168. var control = this, template, $reorderNav;
  1169. template = wp.template( 'menu-item-reorder-nav' );
  1170. // Add the menu item reordering elements to the menu item control.
  1171. control.container.find( '.item-controls' ).after( template );
  1172. // Handle clicks for up/down/left-right on the reorder nav.
  1173. $reorderNav = control.container.find( '.menu-item-reorder-nav' );
  1174. $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
  1175. var moveBtn = $( this );
  1176. moveBtn.focus();
  1177. var isMoveUp = moveBtn.is( '.menus-move-up' ),
  1178. isMoveDown = moveBtn.is( '.menus-move-down' ),
  1179. isMoveLeft = moveBtn.is( '.menus-move-left' ),
  1180. isMoveRight = moveBtn.is( '.menus-move-right' );
  1181. if ( isMoveUp ) {
  1182. control.moveUp();
  1183. } else if ( isMoveDown ) {
  1184. control.moveDown();
  1185. } else if ( isMoveLeft ) {
  1186. control.moveLeft();
  1187. } else if ( isMoveRight ) {
  1188. control.moveRight();
  1189. }
  1190. moveBtn.focus(); // Re-focus after the container was moved.
  1191. } );
  1192. },
  1193. /**
  1194. * Set up event handlers for menu item updating.
  1195. */
  1196. _setupUpdateUI: function() {
  1197. var control = this,
  1198. settingValue = control.setting();
  1199. control.elements = {};
  1200. control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
  1201. control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
  1202. control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
  1203. control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
  1204. control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
  1205. control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
  1206. control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
  1207. // @todo allow other elements, added by plugins, to be automatically picked up here; allow additional values to be added to setting array.
  1208. _.each( control.elements, function( element, property ) {
  1209. element.bind(function( value ) {
  1210. if ( element.element.is( 'input[type=checkbox]' ) ) {
  1211. value = ( value ) ? element.element.val() : '';
  1212. }
  1213. var settingValue = control.setting();
  1214. if ( settingValue && settingValue[ property ] !== value ) {
  1215. settingValue = _.clone( settingValue );
  1216. settingValue[ property ] = value;
  1217. control.setting.set( settingValue );
  1218. }
  1219. });
  1220. if ( settingValue ) {
  1221. if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) {
  1222. element.set( settingValue[ property ].join( ' ' ) );
  1223. } else {
  1224. element.set( settingValue[ property ] );
  1225. }
  1226. }
  1227. });
  1228. control.setting.bind(function( to, from ) {
  1229. var itemId = control.params.menu_item_id,
  1230. followingSiblingItemControls = [],
  1231. childrenItemControls = [],
  1232. menuControl;
  1233. if ( false === to ) {
  1234. menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
  1235. control.container.remove();
  1236. _.each( menuControl.getMenuItemControls(), function( otherControl ) {
  1237. if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
  1238. followingSiblingItemControls.push( otherControl );
  1239. } else if ( otherControl.setting().menu_item_parent === itemId ) {
  1240. childrenItemControls.push( otherControl );
  1241. }
  1242. });
  1243. // Shift all following siblings by the number of children this item has.
  1244. _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
  1245. var value = _.clone( followingSiblingItemControl.setting() );
  1246. value.position += childrenItemControls.length;
  1247. followingSiblingItemControl.setting.set( value );
  1248. });
  1249. // Now move the children up to be the new subsequent siblings.
  1250. _.each( childrenItemControls, function( childrenItemControl, i ) {
  1251. var value = _.clone( childrenItemControl.setting() );
  1252. value.position = from.position + i;
  1253. value.menu_item_parent = from.menu_item_parent;
  1254. childrenItemControl.setting.set( value );
  1255. });
  1256. menuControl.debouncedReflowMenuItems();
  1257. } else {
  1258. // Update the elements' values to match the new setting properties.
  1259. _.each( to, function( value, key ) {
  1260. if ( control.elements[ key] ) {
  1261. control.elements[ key ].set( to[ key ] );
  1262. }
  1263. } );
  1264. control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
  1265. // Handle UI updates when the position or depth (parent) change.
  1266. if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
  1267. control.getMenuControl().debouncedReflowMenuItems();
  1268. }
  1269. }
  1270. });
  1271. },
  1272. /**
  1273. * Set up event handlers for menu item deletion.
  1274. */
  1275. _setupRemoveUI: function() {
  1276. var control = this, $removeBtn;
  1277. // Configure delete button.
  1278. $removeBtn = control.container.find( '.item-delete' );
  1279. $removeBtn.on( 'click', function() {
  1280. // Find an adjacent element to add focus to when this menu item goes away
  1281. var addingItems = true, $adjacentFocusTarget, $next, $prev;
  1282. if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
  1283. addingItems = false;
  1284. }
  1285. $next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first();
  1286. $prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first();
  1287. if ( $next.length ) {
  1288. $adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
  1289. } else if ( $prev.length ) {
  1290. $adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
  1291. } else {
  1292. $adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first();
  1293. }
  1294. control.container.slideUp( function() {
  1295. control.setting.set( false );
  1296. wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
  1297. $adjacentFocusTarget.focus(); // keyboard accessibility
  1298. } );
  1299. } );
  1300. },
  1301. _setupLinksUI: function() {
  1302. var $origBtn;
  1303. // Configure original link.
  1304. $origBtn = this.container.find( 'a.original-link' );
  1305. $origBtn.on( 'click', function( e ) {
  1306. e.preventDefault();
  1307. api.previewer.previewUrl( e.target.toString() );
  1308. } );
  1309. },
  1310. /**
  1311. * Update item handle title when changed.
  1312. */
  1313. _setupTitleUI: function() {
  1314. var control = this, titleEl;
  1315. // Ensure that whitespace is trimmed on blur so placeholder can be shown.
  1316. control.container.find( '.edit-menu-item-title' ).on( 'blur', function() {
  1317. $( this ).val( $.trim( $( this ).val() ) );
  1318. } );
  1319. titleEl = control.container.find( '.menu-item-title' );
  1320. control.setting.bind( function( item ) {
  1321. var trimmedTitle, titleText;
  1322. if ( ! item ) {
  1323. return;
  1324. }
  1325. trimmedTitle = $.trim( item.title );
  1326. titleText = trimmedTitle || item.original_title || api.Menus.data.l10n.untitled;
  1327. if ( item._invalid ) {
  1328. titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText );
  1329. }
  1330. // Don't update to an empty title.
  1331. if ( trimmedTitle || item.original_title ) {
  1332. titleEl
  1333. .text( titleText )
  1334. .removeClass( 'no-title' );
  1335. } else {
  1336. titleEl
  1337. .text( titleText )
  1338. .addClass( 'no-title' );
  1339. }
  1340. } );
  1341. },
  1342. /**
  1343. *
  1344. * @returns {number}
  1345. */
  1346. getDepth: function() {
  1347. var control = this, setting = control.setting(), depth = 0;
  1348. if ( ! setting ) {
  1349. return 0;
  1350. }
  1351. while ( setting && setting.menu_item_parent ) {
  1352. depth += 1;
  1353. control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
  1354. if ( ! control ) {
  1355. break;
  1356. }
  1357. setting = control.setting();
  1358. }
  1359. return depth;
  1360. },
  1361. /**
  1362. * Amend the control's params with the data necessary for the JS template just in time.
  1363. */
  1364. renderContent: function() {
  1365. var control = this,
  1366. settingValue = control.setting(),
  1367. containerClasses;
  1368. control.params.title = settingValue.title || '';
  1369. control.params.depth = control.getDepth();
  1370. control.container.data( 'item-depth', control.params.depth );
  1371. containerClasses = [
  1372. 'menu-item',
  1373. 'menu-item-depth-' + String( control.params.depth ),
  1374. 'menu-item-' + settingValue.object,
  1375. 'menu-item-edit-inactive'
  1376. ];
  1377. if ( settingValue._invalid ) {
  1378. containerClasses.push( 'menu-item-invalid' );
  1379. control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title );
  1380. } else if ( 'draft' === settingValue.status ) {
  1381. containerClasses.push( 'pending' );
  1382. control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
  1383. }
  1384. control.params.el_classes = containerClasses.join( ' ' );
  1385. control.params.item_type_label = settingValue.type_label;
  1386. control.params.item_type = settingValue.type;
  1387. control.params.url = settingValue.url;
  1388. control.params.target = settingValue.target;
  1389. control.params.attr_title = settingValue.attr_title;
  1390. control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
  1391. control.params.attr_title = settingValue.attr_title;
  1392. control.params.xfn = settingValue.xfn;
  1393. control.params.description = settingValue.description;
  1394. control.params.parent = settingValue.menu_item_parent;
  1395. control.params.original_title = settingValue.original_title || '';
  1396. control.container.addClass( control.params.el_classes );
  1397. api.Control.prototype.renderContent.call( control );
  1398. },
  1399. /***********************************************************************
  1400. * Begin public API methods
  1401. **********************************************************************/
  1402. /**
  1403. * @return {wp.customize.controlConstructor.nav_menu|null}
  1404. */
  1405. getMenuControl: function() {
  1406. var control = this, settingValue = control.setting();
  1407. if ( settingValue && settingValue.nav_menu_term_id ) {
  1408. return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
  1409. } else {
  1410. return null;
  1411. }
  1412. },
  1413. /**
  1414. * Expand the accordion section containing a control
  1415. */
  1416. expandControlSection: function() {
  1417. var $section = this.container.closest( '.accordion-section' );
  1418. if ( ! $section.hasClass( 'open' ) ) {
  1419. $section.find( '.accordion-section-title:first' ).trigger( 'click' );
  1420. }
  1421. },
  1422. /**
  1423. * @since 4.6.0
  1424. *
  1425. * @param {Boolean} expanded
  1426. * @param {Object} [params]
  1427. * @returns {Boolean} false if state already applied
  1428. */
  1429. _toggleExpanded: api.Section.prototype._toggleExpanded,
  1430. /**
  1431. * @since 4.6.0
  1432. *
  1433. * @param {Object} [params]
  1434. * @returns {Boolean} false if already expanded
  1435. */
  1436. expand: api.Section.prototype.expand,
  1437. /**
  1438. * Expand the menu item form control.
  1439. *
  1440. * @since 4.5.0 Added params.completeCallback.
  1441. *
  1442. * @param {Object} [params] - Optional params.
  1443. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1444. */
  1445. expandForm: function( params ) {
  1446. this.expand( params );
  1447. },
  1448. /**
  1449. * @since 4.6.0
  1450. *
  1451. * @param {Object} [params]
  1452. * @returns {Boolean} false if already collapsed
  1453. */
  1454. collapse: api.Section.prototype.collapse,
  1455. /**
  1456. * Collapse the menu item form control.
  1457. *
  1458. * @since 4.5.0 Added params.completeCallback.
  1459. *
  1460. * @param {Object} [params] - Optional params.
  1461. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1462. */
  1463. collapseForm: function( params ) {
  1464. this.collapse( params );
  1465. },
  1466. /**
  1467. * Expand or collapse the menu item control.
  1468. *
  1469. * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
  1470. * @since 4.5.0 Added params.completeCallback.
  1471. *
  1472. * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
  1473. * @param {Object} [params] - Optional params.
  1474. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1475. */
  1476. toggleForm: function( showOrHide, params ) {
  1477. if ( typeof showOrHide === 'undefined' ) {
  1478. showOrHide = ! this.expanded();
  1479. }
  1480. if ( showOrHide ) {
  1481. this.expand( params );
  1482. } else {
  1483. this.collapse( params );
  1484. }
  1485. },
  1486. /**
  1487. * Expand or collapse the menu item control.
  1488. *
  1489. * @since 4.6.0
  1490. * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
  1491. * @param {Object} [params] - Optional params.
  1492. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1493. */
  1494. onChangeExpanded: function( showOrHide, params ) {
  1495. var self = this, $menuitem, $inside, complete;
  1496. $menuitem = this.container;
  1497. $inside = $menuitem.find( '.menu-item-settings:first' );
  1498. if ( 'undefined' === typeof showOrHide ) {
  1499. showOrHide = ! $inside.is( ':visible' );
  1500. }
  1501. // Already expanded or collapsed.
  1502. if ( $inside.is( ':visible' ) === showOrHide ) {
  1503. if ( params && params.completeCallback ) {
  1504. params.completeCallback();
  1505. }
  1506. return;
  1507. }
  1508. if ( showOrHide ) {
  1509. // Close all other menu item controls before expanding this one.
  1510. api.control.each( function( otherControl ) {
  1511. if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1512. otherControl.collapseForm();
  1513. }
  1514. } );
  1515. complete = function() {
  1516. $menuitem
  1517. .removeClass( 'menu-item-edit-inactive' )
  1518. .addClass( 'menu-item-edit-active' );
  1519. self.container.trigger( 'expanded' );
  1520. if ( params && params.completeCallback ) {
  1521. params.completeCallback();
  1522. }
  1523. };
  1524. $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' );
  1525. $inside.slideDown( 'fast', complete );
  1526. self.container.trigger( 'expand' );
  1527. } else {
  1528. complete = function() {
  1529. $menuitem
  1530. .addClass( 'menu-item-edit-inactive' )
  1531. .removeClass( 'menu-item-edit-active' );
  1532. self.container.trigger( 'collapsed' );
  1533. if ( params && params.completeCallback ) {
  1534. params.completeCallback();
  1535. }
  1536. };
  1537. self.container.trigger( 'collapse' );
  1538. $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' );
  1539. $inside.slideUp( 'fast', complete );
  1540. }
  1541. },
  1542. /**
  1543. * Expand the containing menu section, expand the form, and focus on
  1544. * the first input in the control.
  1545. *
  1546. * @since 4.5.0 Added params.completeCallback.
  1547. *
  1548. * @param {Object} [params] - Params object.
  1549. * @param {Function} [params.completeCallback] - Optional callback function when focus has completed.
  1550. */
  1551. focus: function( params ) {
  1552. params = params || {};
  1553. var control = this, originalCompleteCallback = params.completeCallback, focusControl;
  1554. focusControl = function() {
  1555. control.expandControlSection();
  1556. params.completeCallback = function() {
  1557. var focusable;
  1558. // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
  1559. focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' );
  1560. focusable.first().focus();
  1561. if ( originalCompleteCallback ) {
  1562. originalCompleteCallback();
  1563. }
  1564. };
  1565. control.expandForm( params );
  1566. };
  1567. if ( api.section.has( control.section() ) ) {
  1568. api.section( control.section() ).expand( {
  1569. completeCallback: focusControl
  1570. } );
  1571. } else {
  1572. focusControl();
  1573. }
  1574. },
  1575. /**
  1576. * Move menu item up one in the menu.
  1577. */
  1578. moveUp: function() {
  1579. this._changePosition( -1 );
  1580. wp.a11y.speak( api.Menus.data.l10n.movedUp );
  1581. },
  1582. /**
  1583. * Move menu item up one in the menu.
  1584. */
  1585. moveDown: function() {
  1586. this._changePosition( 1 );
  1587. wp.a11y.speak( api.Menus.data.l10n.movedDown );
  1588. },
  1589. /**
  1590. * Move menu item and all children up one level of depth.
  1591. */
  1592. moveLeft: function() {
  1593. this._changeDepth( -1 );
  1594. wp.a11y.speak( api.Menus.data.l10n.movedLeft );
  1595. },
  1596. /**
  1597. * Move menu item and children one level deeper, as a submenu of the previous item.
  1598. */
  1599. moveRight: function() {
  1600. this._changeDepth( 1 );
  1601. wp.a11y.speak( api.Menus.data.l10n.movedRight );
  1602. },
  1603. /**
  1604. * Note that this will trigger a UI update, causing child items to
  1605. * move as well and cardinal order class names to be updated.
  1606. *
  1607. * @private
  1608. *
  1609. * @param {Number} offset 1|-1
  1610. */
  1611. _changePosition: function( offset ) {
  1612. var control = this,
  1613. adjacentSetting,
  1614. settingValue = _.clone( control.setting() ),
  1615. siblingSettings = [],
  1616. realPosition;
  1617. if ( 1 !== offset && -1 !== offset ) {
  1618. throw new Error( 'Offset changes by 1 are only supported.' );
  1619. }
  1620. // Skip moving deleted items.
  1621. if ( ! control.setting() ) {
  1622. return;
  1623. }
  1624. // Locate the other items under the same parent (siblings).
  1625. _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
  1626. if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
  1627. siblingSettings.push( otherControl.setting );
  1628. }
  1629. });
  1630. siblingSettings.sort(function( a, b ) {
  1631. return a().position - b().position;
  1632. });
  1633. realPosition = _.indexOf( siblingSettings, control.setting );
  1634. if ( -1 === realPosition ) {
  1635. throw new Error( 'Expected setting to be among siblings.' );
  1636. }
  1637. // Skip doing anything if the item is already at the edge in the desired direction.
  1638. if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
  1639. // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
  1640. return;
  1641. }
  1642. // Update any adjacent menu item setting to take on this item's position.
  1643. adjacentSetting = siblingSettings[ realPosition + offset ];
  1644. if ( adjacentSetting ) {
  1645. adjacentSetting.set( $.extend(
  1646. _.clone( adjacentSetting() ),
  1647. {
  1648. position: settingValue.position
  1649. }
  1650. ) );
  1651. }
  1652. settingValue.position += offset;
  1653. control.setting.set( settingValue );
  1654. },
  1655. /**
  1656. * Note that this will trigger a UI update, causing child items to
  1657. * move as well and cardinal order class names to be updated.
  1658. *
  1659. * @private
  1660. *
  1661. * @param {Number} offset 1|-1
  1662. */
  1663. _changeDepth: function( offset ) {
  1664. if ( 1 !== offset && -1 !== offset ) {
  1665. throw new Error( 'Offset changes by 1 are only supported.' );
  1666. }
  1667. var control = this,
  1668. settingValue = _.clone( control.setting() ),
  1669. siblingControls = [],
  1670. realPosition,
  1671. siblingControl,
  1672. parentControl;
  1673. // Locate the other items under the same parent (siblings).
  1674. _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
  1675. if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
  1676. siblingControls.push( otherControl );
  1677. }
  1678. });
  1679. siblingControls.sort(function( a, b ) {
  1680. return a.setting().position - b.setting().position;
  1681. });
  1682. realPosition = _.indexOf( siblingControls, control );
  1683. if ( -1 === realPosition ) {
  1684. throw new Error( 'Expected control to be among siblings.' );
  1685. }
  1686. if ( -1 === offset ) {
  1687. // Skip moving left an item that is already at the top level.
  1688. if ( ! settingValue.menu_item_parent ) {
  1689. return;
  1690. }
  1691. parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' );
  1692. // Make this control the parent of all the following siblings.
  1693. _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) {
  1694. siblingControl.setting.set(
  1695. $.extend(
  1696. {},
  1697. siblingControl.setting(),
  1698. {
  1699. menu_item_parent: control.params.menu_item_id,
  1700. position: i
  1701. }
  1702. )
  1703. );
  1704. });
  1705. // Increase the positions of the parent item's subsequent children to make room for this one.
  1706. _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
  1707. var otherControlSettingValue, isControlToBeShifted;
  1708. isControlToBeShifted = (
  1709. otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent &&
  1710. otherControl.setting().position > parentControl.setting().position
  1711. );
  1712. if ( isControlToBeShifted ) {
  1713. otherControlSettingValue = _.clone( otherControl.setting() );
  1714. otherControl.setting.set(
  1715. $.extend(
  1716. otherControlSettingValue,
  1717. { position: otherControlSettingValue.position + 1 }
  1718. )
  1719. );
  1720. }
  1721. });
  1722. // Make this control the following sibling of its parent item.
  1723. settingValue.position = parentControl.setting().position + 1;
  1724. settingValue.menu_item_parent = parentControl.setting().menu_item_parent;
  1725. control.setting.set( settingValue );
  1726. } else if ( 1 === offset ) {
  1727. // Skip moving right an item that doesn't have a previous sibling.
  1728. if ( realPosition === 0 ) {
  1729. return;
  1730. }
  1731. // Make the control the last child of the previous sibling.
  1732. siblingControl = siblingControls[ realPosition - 1 ];
  1733. settingValue.menu_item_parent = siblingControl.params.menu_item_id;
  1734. settingValue.position = 0;
  1735. _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
  1736. if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
  1737. settingValue.position = Math.max( settingValue.position, otherControl.setting().position );
  1738. }
  1739. });
  1740. settingValue.position += 1;
  1741. control.setting.set( settingValue );
  1742. }
  1743. }
  1744. } );
  1745. /**
  1746. * wp.customize.Menus.MenuNameControl
  1747. *
  1748. * Customizer control for a nav menu's name.
  1749. *
  1750. * @constructor
  1751. * @augments wp.customize.Control
  1752. */
  1753. api.Menus.MenuNameControl = api.Control.extend({
  1754. ready: function() {
  1755. var control = this,
  1756. settingValue = control.setting();
  1757. /*
  1758. * Since the control is not registered in PHP, we need to prevent the
  1759. * preview's sending of the activeControls to result in this control
  1760. * being deactivated.
  1761. */
  1762. control.active.validate = function() {
  1763. var value, section = api.section( control.section() );
  1764. if ( section ) {
  1765. value = section.active();
  1766. } else {
  1767. value = false;
  1768. }
  1769. return value;
  1770. };
  1771. control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) );
  1772. control.nameElement.bind(function( value ) {
  1773. var settingValue = control.setting();
  1774. if ( settingValue && settingValue.name !== value ) {
  1775. settingValue = _.clone( settingValue );
  1776. settingValue.name = value;
  1777. control.setting.set( settingValue );
  1778. }
  1779. });
  1780. if ( settingValue ) {
  1781. control.nameElement.set( settingValue.name );
  1782. }
  1783. control.setting.bind(function( object ) {
  1784. if ( object ) {
  1785. control.nameElement.set( object.name );
  1786. }
  1787. });
  1788. }
  1789. });
  1790. /**
  1791. * wp.customize.Menus.MenuAutoAddControl
  1792. *
  1793. * Customizer control for a nav menu's auto add.
  1794. *
  1795. * @constructor
  1796. * @augments wp.customize.Control
  1797. */
  1798. api.Menus.MenuAutoAddControl = api.Control.extend({
  1799. ready: function() {
  1800. var control = this,
  1801. settingValue = control.setting();
  1802. /*
  1803. * Since the control is not registered in PHP, we need to prevent the
  1804. * preview's sending of the activeControls to result in this control
  1805. * being deactivated.
  1806. */
  1807. control.active.validate = function() {
  1808. var value, section = api.section( control.section() );
  1809. if ( section ) {
  1810. value = section.active();
  1811. } else {
  1812. value = false;
  1813. }
  1814. return value;
  1815. };
  1816. control.autoAddElement = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) );
  1817. control.autoAddElement.bind(function( value ) {
  1818. var settingValue = control.setting();
  1819. if ( settingValue && settingValue.name !== value ) {
  1820. settingValue = _.clone( settingValue );
  1821. settingValue.auto_add = value;
  1822. control.setting.set( settingValue );
  1823. }
  1824. });
  1825. if ( settingValue ) {
  1826. control.autoAddElement.set( settingValue.auto_add );
  1827. }
  1828. control.setting.bind(function( object ) {
  1829. if ( object ) {
  1830. control.autoAddElement.set( object.auto_add );
  1831. }
  1832. });
  1833. }
  1834. });
  1835. /**
  1836. * wp.customize.Menus.MenuControl
  1837. *
  1838. * Customizer control for menus.
  1839. * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type
  1840. *
  1841. * @constructor
  1842. * @augments wp.customize.Control
  1843. */
  1844. api.Menus.MenuControl = api.Control.extend({
  1845. /**
  1846. * Set up the control.
  1847. */
  1848. ready: function() {
  1849. var control = this,
  1850. section = api.section( control.section() ),
  1851. menuId = control.params.menu_id,
  1852. menu = control.setting(),
  1853. name,
  1854. widgetTemplate,
  1855. select;
  1856. if ( 'undefined' === typeof this.params.menu_id ) {
  1857. throw new Error( 'params.menu_id was not defined' );
  1858. }
  1859. /*
  1860. * Since the control is not registered in PHP, we need to prevent the
  1861. * preview's sending of the activeControls to result in this control
  1862. * being deactivated.
  1863. */
  1864. control.active.validate = function() {
  1865. var value;
  1866. if ( section ) {
  1867. value = section.active();
  1868. } else {
  1869. value = false;
  1870. }
  1871. return value;
  1872. };
  1873. control.$controlSection = section.headContainer;
  1874. control.$sectionContent = control.container.closest( '.accordion-section-content' );
  1875. this._setupModel();
  1876. api.section( control.section(), function( section ) {
  1877. section.deferred.initSortables.done(function( menuList ) {
  1878. control._setupSortable( menuList );
  1879. });
  1880. } );
  1881. this._setupAddition();
  1882. this._setupLocations();
  1883. this._setupTitle();
  1884. // Add menu to Custom Menu widgets.
  1885. if ( menu ) {
  1886. name = displayNavMenuName( menu.name );
  1887. // Add the menu to the existing controls.
  1888. api.control.each( function( widgetControl ) {
  1889. if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
  1890. return;
  1891. }
  1892. widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).show();
  1893. widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).hide();
  1894. select = widgetControl.container.find( 'select' );
  1895. if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
  1896. select.append( new Option( name, menuId ) );
  1897. }
  1898. } );
  1899. // Add the menu to the widget template.
  1900. widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
  1901. widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).show();
  1902. widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).hide();
  1903. select = widgetTemplate.find( '.widget-inside select:first' );
  1904. if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
  1905. select.append( new Option( name, menuId ) );
  1906. }
  1907. }
  1908. },
  1909. /**
  1910. * Update ordering of menu item controls when the setting is updated.
  1911. */
  1912. _setupModel: function() {
  1913. var control = this,
  1914. menuId = control.params.menu_id;
  1915. control.setting.bind( function( to ) {
  1916. var name;
  1917. if ( false === to ) {
  1918. control._handleDeletion();
  1919. } else {
  1920. // Update names in the Custom Menu widgets.
  1921. name = displayNavMenuName( to.name );
  1922. api.control.each( function( widgetControl ) {
  1923. if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
  1924. return;
  1925. }
  1926. var select = widgetControl.container.find( 'select' );
  1927. select.find( 'option[value=' + String( menuId ) + ']' ).text( name );
  1928. });
  1929. }
  1930. } );
  1931. control.container.find( '.menu-delete' ).on( 'click', function( event ) {
  1932. event.stopPropagation();
  1933. event.preventDefault();
  1934. control.setting.set( false );
  1935. });
  1936. },
  1937. /**
  1938. * Allow items in each menu to be re-ordered, and for the order to be previewed.
  1939. *
  1940. * Notice that the UI aspects here are handled by wpNavMenu.initSortables()
  1941. * which is called in MenuSection.onChangeExpanded()
  1942. *
  1943. * @param {object} menuList - The element that has sortable().
  1944. */
  1945. _setupSortable: function( menuList ) {
  1946. var control = this;
  1947. if ( ! menuList.is( control.$sectionContent ) ) {
  1948. throw new Error( 'Unexpected menuList.' );
  1949. }
  1950. menuList.on( 'sortstart', function() {
  1951. control.isSorting = true;
  1952. });
  1953. menuList.on( 'sortstop', function() {
  1954. setTimeout( function() { // Next tick.
  1955. var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ),
  1956. menuItemControls = [],
  1957. position = 0,
  1958. priority = 10;
  1959. control.isSorting = false;
  1960. // Reset horizontal scroll position when done dragging.
  1961. control.$sectionContent.scrollLeft( 0 );
  1962. _.each( menuItemContainerIds, function( menuItemContainerId ) {
  1963. var menuItemId, menuItemControl, matches;
  1964. matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
  1965. if ( ! matches ) {
  1966. return;
  1967. }
  1968. menuItemId = parseInt( matches[1], 10 );
  1969. menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
  1970. if ( menuItemControl ) {
  1971. menuItemControls.push( menuItemControl );
  1972. }
  1973. } );
  1974. _.each( menuItemControls, function( menuItemControl ) {
  1975. if ( false === menuItemControl.setting() ) {
  1976. // Skip deleted items.
  1977. return;
  1978. }
  1979. var setting = _.clone( menuItemControl.setting() );
  1980. position += 1;
  1981. priority += 1;
  1982. setting.position = position;
  1983. menuItemControl.priority( priority );
  1984. // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value.
  1985. setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 );
  1986. if ( ! setting.menu_item_parent ) {
  1987. setting.menu_item_parent = 0;
  1988. }
  1989. menuItemControl.setting.set( setting );
  1990. });
  1991. });
  1992. });
  1993. control.isReordering = false;
  1994. /**
  1995. * Keyboard-accessible reordering.
  1996. */
  1997. this.container.find( '.reorder-toggle' ).on( 'click', function() {
  1998. control.toggleReordering( ! control.isReordering );
  1999. } );
  2000. },
  2001. /**
  2002. * Set up UI for adding a new menu item.
  2003. */
  2004. _setupAddition: function() {
  2005. var self = this;
  2006. this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) {
  2007. if ( self.$sectionContent.hasClass( 'reordering' ) ) {
  2008. return;
  2009. }
  2010. if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
  2011. $( this ).attr( 'aria-expanded', 'true' );
  2012. api.Menus.availableMenuItemsPanel.open( self );
  2013. } else {
  2014. $( this ).attr( 'aria-expanded', 'false' );
  2015. api.Menus.availableMenuItemsPanel.close();
  2016. event.stopPropagation();
  2017. }
  2018. } );
  2019. },
  2020. _handleDeletion: function() {
  2021. var control = this,
  2022. section,
  2023. menuId = control.params.menu_id,
  2024. removeSection,
  2025. widgetTemplate,
  2026. navMenuCount = 0;
  2027. section = api.section( control.section() );
  2028. removeSection = function() {
  2029. section.container.remove();
  2030. api.section.remove( section.id );
  2031. };
  2032. if ( section && section.expanded() ) {
  2033. section.collapse({
  2034. completeCallback: function() {
  2035. removeSection();
  2036. wp.a11y.speak( api.Menus.data.l10n.menuDeleted );
  2037. api.panel( 'nav_menus' ).focus();
  2038. }
  2039. });
  2040. } else {
  2041. removeSection();
  2042. }
  2043. api.each(function( setting ) {
  2044. if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
  2045. navMenuCount += 1;
  2046. }
  2047. });
  2048. // Remove the menu from any Custom Menu widgets.
  2049. api.control.each(function( widgetControl ) {
  2050. if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
  2051. return;
  2052. }
  2053. var select = widgetControl.container.find( 'select' );
  2054. if ( select.val() === String( menuId ) ) {
  2055. select.prop( 'selectedIndex', 0 ).trigger( 'change' );
  2056. }
  2057. widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
  2058. widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
  2059. widgetControl.container.find( 'option[value=' + String( menuId ) + ']' ).remove();
  2060. });
  2061. // Remove the menu to the nav menu widget template.
  2062. widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
  2063. widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
  2064. widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
  2065. widgetTemplate.find( 'option[value=' + String( menuId ) + ']' ).remove();
  2066. },
  2067. // Setup theme location checkboxes.
  2068. _setupLocations: function() {
  2069. var control = this;
  2070. control.container.find( '.assigned-menu-location' ).each(function() {
  2071. var container = $( this ),
  2072. checkbox = container.find( 'input[type=checkbox]' ),
  2073. element,
  2074. updateSelectedMenuLabel,
  2075. navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
  2076. updateSelectedMenuLabel = function( selectedMenuId ) {
  2077. var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' );
  2078. if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
  2079. container.find( '.theme-location-set' ).hide();
  2080. } else {
  2081. container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) );
  2082. }
  2083. };
  2084. element = new api.Element( checkbox );
  2085. element.set( navMenuLocationSetting.get() === control.params.menu_id );
  2086. checkbox.on( 'change', function() {
  2087. // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well.
  2088. navMenuLocationSetting.set( this.checked ? control.params.menu_id : 0 );
  2089. } );
  2090. navMenuLocationSetting.bind(function( selectedMenuId ) {
  2091. element.set( selectedMenuId === control.params.menu_id );
  2092. updateSelectedMenuLabel( selectedMenuId );
  2093. });
  2094. updateSelectedMenuLabel( navMenuLocationSetting.get() );
  2095. });
  2096. },
  2097. /**
  2098. * Update Section Title as menu name is changed.
  2099. */
  2100. _setupTitle: function() {
  2101. var control = this;
  2102. control.setting.bind( function( menu ) {
  2103. if ( ! menu ) {
  2104. return;
  2105. }
  2106. var section = api.section( control.section() ),
  2107. menuId = control.params.menu_id,
  2108. controlTitle = section.headContainer.find( '.accordion-section-title' ),
  2109. sectionTitle = section.contentContainer.find( '.customize-section-title h3' ),
  2110. location = section.headContainer.find( '.menu-in-location' ),
  2111. action = sectionTitle.find( '.customize-action' ),
  2112. name = displayNavMenuName( menu.name );
  2113. // Update the control title
  2114. controlTitle.text( name );
  2115. if ( location.length ) {
  2116. location.appendTo( controlTitle );
  2117. }
  2118. // Update the section title
  2119. sectionTitle.text( name );
  2120. if ( action.length ) {
  2121. action.prependTo( sectionTitle );
  2122. }
  2123. // Update the nav menu name in location selects.
  2124. api.control.each( function( control ) {
  2125. if ( /^nav_menu_locations\[/.test( control.id ) ) {
  2126. control.container.find( 'option[value=' + menuId + ']' ).text( name );
  2127. }
  2128. } );
  2129. // Update the nav menu name in all location checkboxes.
  2130. section.contentContainer.find( '.customize-control-checkbox input' ).each( function() {
  2131. if ( $( this ).prop( 'checked' ) ) {
  2132. $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name );
  2133. }
  2134. } );
  2135. } );
  2136. },
  2137. /***********************************************************************
  2138. * Begin public API methods
  2139. **********************************************************************/
  2140. /**
  2141. * Enable/disable the reordering UI
  2142. *
  2143. * @param {Boolean} showOrHide to enable/disable reordering
  2144. */
  2145. toggleReordering: function( showOrHide ) {
  2146. var addNewItemBtn = this.container.find( '.add-new-menu-item' ),
  2147. reorderBtn = this.container.find( '.reorder-toggle' ),
  2148. itemsTitle = this.$sectionContent.find( '.item-title' );
  2149. showOrHide = Boolean( showOrHide );
  2150. if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
  2151. return;
  2152. }
  2153. this.isReordering = showOrHide;
  2154. this.$sectionContent.toggleClass( 'reordering', showOrHide );
  2155. this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' );
  2156. if ( this.isReordering ) {
  2157. addNewItemBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  2158. reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOff );
  2159. wp.a11y.speak( api.Menus.data.l10n.reorderModeOn );
  2160. itemsTitle.attr( 'aria-hidden', 'false' );
  2161. } else {
  2162. addNewItemBtn.removeAttr( 'tabindex aria-hidden' );
  2163. reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOn );
  2164. wp.a11y.speak( api.Menus.data.l10n.reorderModeOff );
  2165. itemsTitle.attr( 'aria-hidden', 'true' );
  2166. }
  2167. if ( showOrHide ) {
  2168. _( this.getMenuItemControls() ).each( function( formControl ) {
  2169. formControl.collapseForm();
  2170. } );
  2171. }
  2172. },
  2173. /**
  2174. * @return {wp.customize.controlConstructor.nav_menu_item[]}
  2175. */
  2176. getMenuItemControls: function() {
  2177. var menuControl = this,
  2178. menuItemControls = [],
  2179. menuTermId = menuControl.params.menu_id;
  2180. api.control.each(function( control ) {
  2181. if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) {
  2182. menuItemControls.push( control );
  2183. }
  2184. });
  2185. return menuItemControls;
  2186. },
  2187. /**
  2188. * Make sure that each menu item control has the proper depth.
  2189. */
  2190. reflowMenuItems: function() {
  2191. var menuControl = this,
  2192. menuItemControls = menuControl.getMenuItemControls(),
  2193. reflowRecursively;
  2194. reflowRecursively = function( context ) {
  2195. var currentMenuItemControls = [],
  2196. thisParent = context.currentParent;
  2197. _.each( context.menuItemControls, function( menuItemControl ) {
  2198. if ( thisParent === menuItemControl.setting().menu_item_parent ) {
  2199. currentMenuItemControls.push( menuItemControl );
  2200. // @todo We could remove this item from menuItemControls now, for efficiency.
  2201. }
  2202. });
  2203. currentMenuItemControls.sort( function( a, b ) {
  2204. return a.setting().position - b.setting().position;
  2205. });
  2206. _.each( currentMenuItemControls, function( menuItemControl ) {
  2207. // Update position.
  2208. context.currentAbsolutePosition += 1;
  2209. menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order.
  2210. // Update depth.
  2211. if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) {
  2212. _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) {
  2213. menuItemControl.container.removeClass( className );
  2214. });
  2215. menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) );
  2216. }
  2217. menuItemControl.container.data( 'item-depth', context.currentDepth );
  2218. // Process any children items.
  2219. context.currentDepth += 1;
  2220. context.currentParent = menuItemControl.params.menu_item_id;
  2221. reflowRecursively( context );
  2222. context.currentDepth -= 1;
  2223. context.currentParent = thisParent;
  2224. });
  2225. // Update class names for reordering controls.
  2226. if ( currentMenuItemControls.length ) {
  2227. _( currentMenuItemControls ).each(function( menuItemControl ) {
  2228. menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' );
  2229. if ( 0 === context.currentDepth ) {
  2230. menuItemControl.container.addClass( 'move-left-disabled' );
  2231. } else if ( 10 === context.currentDepth ) {
  2232. menuItemControl.container.addClass( 'move-right-disabled' );
  2233. }
  2234. });
  2235. currentMenuItemControls[0].container
  2236. .addClass( 'move-up-disabled' )
  2237. .addClass( 'move-right-disabled' )
  2238. .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length );
  2239. currentMenuItemControls[ currentMenuItemControls.length - 1 ].container
  2240. .addClass( 'move-down-disabled' )
  2241. .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length );
  2242. }
  2243. };
  2244. reflowRecursively( {
  2245. menuItemControls: menuItemControls,
  2246. currentParent: 0,
  2247. currentDepth: 0,
  2248. currentAbsolutePosition: 0
  2249. } );
  2250. menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 );
  2251. },
  2252. /**
  2253. * Note that this function gets debounced so that when a lot of setting
  2254. * changes are made at once, for instance when moving a menu item that
  2255. * has child items, this function will only be called once all of the
  2256. * settings have been updated.
  2257. */
  2258. debouncedReflowMenuItems: _.debounce( function() {
  2259. this.reflowMenuItems.apply( this, arguments );
  2260. }, 0 ),
  2261. /**
  2262. * Add a new item to this menu.
  2263. *
  2264. * @param {object} item - Value for the nav_menu_item setting to be created.
  2265. * @returns {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance.
  2266. */
  2267. addItemToMenu: function( item ) {
  2268. var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10;
  2269. _.each( menuControl.getMenuItemControls(), function( control ) {
  2270. if ( false === control.setting() ) {
  2271. return;
  2272. }
  2273. priority = Math.max( priority, control.priority() );
  2274. if ( 0 === control.setting().menu_item_parent ) {
  2275. position = Math.max( position, control.setting().position );
  2276. }
  2277. });
  2278. position += 1;
  2279. priority += 1;
  2280. item = $.extend(
  2281. {},
  2282. api.Menus.data.defaultSettingValues.nav_menu_item,
  2283. item,
  2284. {
  2285. nav_menu_term_id: menuControl.params.menu_id,
  2286. original_title: item.title,
  2287. position: position
  2288. }
  2289. );
  2290. delete item.id; // only used by Backbone
  2291. placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
  2292. customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
  2293. settingArgs = {
  2294. type: 'nav_menu_item',
  2295. transport: api.Menus.data.settingTransport,
  2296. previewer: api.previewer
  2297. };
  2298. setting = api.create( customizeId, customizeId, {}, settingArgs );
  2299. setting.set( item ); // Change from initial empty object to actual item to mark as dirty.
  2300. // Add the menu item control.
  2301. menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, {
  2302. params: {
  2303. type: 'nav_menu_item',
  2304. content: '<li id="customize-control-nav_menu_item-' + String( placeholderId ) + '" class="customize-control customize-control-nav_menu_item"></li>',
  2305. section: menuControl.id,
  2306. priority: priority,
  2307. active: true,
  2308. settings: {
  2309. 'default': customizeId
  2310. },
  2311. menu_item_id: placeholderId
  2312. },
  2313. previewer: api.previewer
  2314. } );
  2315. api.control.add( customizeId, menuItemControl );
  2316. setting.preview();
  2317. menuControl.debouncedReflowMenuItems();
  2318. wp.a11y.speak( api.Menus.data.l10n.itemAdded );
  2319. return menuItemControl;
  2320. }
  2321. } );
  2322. /**
  2323. * wp.customize.Menus.NewMenuControl
  2324. *
  2325. * Customizer control for creating new menus and handling deletion of existing menus.
  2326. * Note that 'new_menu' must match the WP_Customize_New_Menu_Control::$type.
  2327. *
  2328. * @constructor
  2329. * @augments wp.customize.Control
  2330. */
  2331. api.Menus.NewMenuControl = api.Control.extend({
  2332. /**
  2333. * Set up the control.
  2334. */
  2335. ready: function() {
  2336. this._bindHandlers();
  2337. },
  2338. _bindHandlers: function() {
  2339. var self = this,
  2340. name = $( '#customize-control-new_menu_name input' ),
  2341. submit = $( '#create-new-menu-submit' );
  2342. name.on( 'keydown', function( event ) {
  2343. if ( 13 === event.which ) { // Enter.
  2344. self.submit();
  2345. }
  2346. } );
  2347. submit.on( 'click', function( event ) {
  2348. self.submit();
  2349. event.stopPropagation();
  2350. event.preventDefault();
  2351. } );
  2352. },
  2353. /**
  2354. * Create the new menu with the name supplied.
  2355. */
  2356. submit: function() {
  2357. var control = this,
  2358. container = control.container.closest( '.accordion-section-new-menu' ),
  2359. nameInput = container.find( '.menu-name-field' ).first(),
  2360. name = nameInput.val(),
  2361. menuSection,
  2362. customizeId,
  2363. placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
  2364. if ( ! name ) {
  2365. nameInput.addClass( 'invalid' );
  2366. nameInput.focus();
  2367. return;
  2368. }
  2369. customizeId = 'nav_menu[' + String( placeholderId ) + ']';
  2370. // Register the menu control setting.
  2371. api.create( customizeId, customizeId, {}, {
  2372. type: 'nav_menu',
  2373. transport: api.Menus.data.settingTransport,
  2374. previewer: api.previewer
  2375. } );
  2376. api( customizeId ).set( $.extend(
  2377. {},
  2378. api.Menus.data.defaultSettingValues.nav_menu,
  2379. {
  2380. name: name
  2381. }
  2382. ) );
  2383. /*
  2384. * Add the menu section (and its controls).
  2385. * Note that this will automatically create the required controls
  2386. * inside via the Section's ready method.
  2387. */
  2388. menuSection = new api.Menus.MenuSection( customizeId, {
  2389. params: {
  2390. id: customizeId,
  2391. panel: 'nav_menus',
  2392. title: displayNavMenuName( name ),
  2393. customizeAction: api.Menus.data.l10n.customizingMenus,
  2394. type: 'nav_menu',
  2395. priority: 10,
  2396. menu_id: placeholderId
  2397. }
  2398. } );
  2399. api.section.add( customizeId, menuSection );
  2400. // Clear name field.
  2401. nameInput.val( '' );
  2402. nameInput.removeClass( 'invalid' );
  2403. wp.a11y.speak( api.Menus.data.l10n.menuAdded );
  2404. // Focus on the new menu section.
  2405. api.section( customizeId ).focus(); // @todo should we focus on the new menu's control and open the add-items panel? Thinking user flow...
  2406. }
  2407. });
  2408. /**
  2409. * Extends wp.customize.controlConstructor with control constructor for
  2410. * menu_location, menu_item, nav_menu, and new_menu.
  2411. */
  2412. $.extend( api.controlConstructor, {
  2413. nav_menu_location: api.Menus.MenuLocationControl,
  2414. nav_menu_item: api.Menus.MenuItemControl,
  2415. nav_menu: api.Menus.MenuControl,
  2416. nav_menu_name: api.Menus.MenuNameControl,
  2417. nav_menu_auto_add: api.Menus.MenuAutoAddControl,
  2418. new_menu: api.Menus.NewMenuControl
  2419. });
  2420. /**
  2421. * Extends wp.customize.panelConstructor with section constructor for menus.
  2422. */
  2423. $.extend( api.panelConstructor, {
  2424. nav_menus: api.Menus.MenusPanel
  2425. });
  2426. /**
  2427. * Extends wp.customize.sectionConstructor with section constructor for menu.
  2428. */
  2429. $.extend( api.sectionConstructor, {
  2430. nav_menu: api.Menus.MenuSection,
  2431. new_menu: api.Menus.NewMenuSection
  2432. });
  2433. /**
  2434. * Init Customizer for menus.
  2435. */
  2436. api.bind( 'ready', function() {
  2437. // Set up the menu items panel.
  2438. api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({
  2439. collection: api.Menus.availableMenuItems
  2440. });
  2441. api.bind( 'saved', function( data ) {
  2442. if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
  2443. api.Menus.applySavedData( data );
  2444. }
  2445. } );
  2446. /*
  2447. * Reset the list of posts created in the customizer once published.
  2448. * The setting is updated quietly (bypassing events being triggered)
  2449. * so that the customized state doesn't become immediately dirty.
  2450. */
  2451. api.state( 'changesetStatus' ).bind( function( status ) {
  2452. if ( 'publish' === status ) {
  2453. api( 'nav_menus_created_posts' )._value = [];
  2454. }
  2455. } );
  2456. // Open and focus menu control.
  2457. api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl );
  2458. } );
  2459. /**
  2460. * When customize_save comes back with a success, make sure any inserted
  2461. * nav menus and items are properly re-added with their newly-assigned IDs.
  2462. *
  2463. * @param {object} data
  2464. * @param {array} data.nav_menu_updates
  2465. * @param {array} data.nav_menu_item_updates
  2466. */
  2467. api.Menus.applySavedData = function( data ) {
  2468. var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {};
  2469. _( data.nav_menu_updates ).each(function( update ) {
  2470. var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount;
  2471. if ( 'inserted' === update.status ) {
  2472. if ( ! update.previous_term_id ) {
  2473. throw new Error( 'Expected previous_term_id' );
  2474. }
  2475. if ( ! update.term_id ) {
  2476. throw new Error( 'Expected term_id' );
  2477. }
  2478. oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']';
  2479. if ( ! api.has( oldCustomizeId ) ) {
  2480. throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
  2481. }
  2482. oldSetting = api( oldCustomizeId );
  2483. if ( ! api.section.has( oldCustomizeId ) ) {
  2484. throw new Error( 'Expected control to exist: ' + oldCustomizeId );
  2485. }
  2486. oldSection = api.section( oldCustomizeId );
  2487. settingValue = oldSetting.get();
  2488. if ( ! settingValue ) {
  2489. throw new Error( 'Did not expect setting to be empty (deleted).' );
  2490. }
  2491. settingValue = $.extend( _.clone( settingValue ), update.saved_value );
  2492. insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
  2493. newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
  2494. newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
  2495. type: 'nav_menu',
  2496. transport: api.Menus.data.settingTransport,
  2497. previewer: api.previewer
  2498. } );
  2499. if ( oldSection.expanded() ) {
  2500. oldSection.collapse();
  2501. }
  2502. // Add the menu section.
  2503. newSection = new api.Menus.MenuSection( newCustomizeId, {
  2504. params: {
  2505. id: newCustomizeId,
  2506. panel: 'nav_menus',
  2507. title: settingValue.name,
  2508. customizeAction: api.Menus.data.l10n.customizingMenus,
  2509. type: 'nav_menu',
  2510. priority: oldSection.priority.get(),
  2511. active: true,
  2512. menu_id: update.term_id
  2513. }
  2514. } );
  2515. // Add new control for the new menu.
  2516. api.section.add( newCustomizeId, newSection );
  2517. // Update the values for nav menus in Custom Menu controls.
  2518. api.control.each( function( setting ) {
  2519. if ( ! setting.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== setting.params.widget_id_base ) {
  2520. return;
  2521. }
  2522. var select, oldMenuOption, newMenuOption;
  2523. select = setting.container.find( 'select' );
  2524. oldMenuOption = select.find( 'option[value=' + String( update.previous_term_id ) + ']' );
  2525. newMenuOption = select.find( 'option[value=' + String( update.term_id ) + ']' );
  2526. newMenuOption.prop( 'selected', oldMenuOption.prop( 'selected' ) );
  2527. oldMenuOption.remove();
  2528. } );
  2529. // Delete the old placeholder nav_menu.
  2530. oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
  2531. oldSetting.set( false );
  2532. oldSetting.preview();
  2533. newSetting.preview();
  2534. oldSetting._dirty = false;
  2535. // Remove nav_menu section.
  2536. oldSection.container.remove();
  2537. api.section.remove( oldCustomizeId );
  2538. // Update the nav_menu widget to reflect removed placeholder menu.
  2539. navMenuCount = 0;
  2540. api.each(function( setting ) {
  2541. if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
  2542. navMenuCount += 1;
  2543. }
  2544. });
  2545. widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
  2546. widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
  2547. widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
  2548. widgetTemplate.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
  2549. // Update the nav_menu_locations[...] controls to remove the placeholder menus from the dropdown options.
  2550. wp.customize.control.each(function( control ){
  2551. if ( /^nav_menu_locations\[/.test( control.id ) ) {
  2552. control.container.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
  2553. }
  2554. });
  2555. // Update nav_menu_locations to reference the new ID.
  2556. api.each( function( setting ) {
  2557. var wasSaved = api.state( 'saved' ).get();
  2558. if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) {
  2559. setting.set( update.term_id );
  2560. setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update().
  2561. api.state( 'saved' ).set( wasSaved );
  2562. setting.preview();
  2563. }
  2564. } );
  2565. if ( oldSection.expanded.get() ) {
  2566. // @todo This doesn't seem to be working.
  2567. newSection.expand();
  2568. }
  2569. } else if ( 'updated' === update.status ) {
  2570. customizeId = 'nav_menu[' + String( update.term_id ) + ']';
  2571. if ( ! api.has( customizeId ) ) {
  2572. throw new Error( 'Expected setting to exist: ' + customizeId );
  2573. }
  2574. // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name).
  2575. setting = api( customizeId );
  2576. if ( ! _.isEqual( update.saved_value, setting.get() ) ) {
  2577. wasSaved = api.state( 'saved' ).get();
  2578. setting.set( update.saved_value );
  2579. setting._dirty = false;
  2580. api.state( 'saved' ).set( wasSaved );
  2581. }
  2582. }
  2583. } );
  2584. // Build up mapping of nav_menu_item placeholder IDs to inserted IDs.
  2585. _( data.nav_menu_item_updates ).each(function( update ) {
  2586. if ( update.previous_post_id ) {
  2587. insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id;
  2588. }
  2589. });
  2590. _( data.nav_menu_item_updates ).each(function( update ) {
  2591. var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl;
  2592. if ( 'inserted' === update.status ) {
  2593. if ( ! update.previous_post_id ) {
  2594. throw new Error( 'Expected previous_post_id' );
  2595. }
  2596. if ( ! update.post_id ) {
  2597. throw new Error( 'Expected post_id' );
  2598. }
  2599. oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']';
  2600. if ( ! api.has( oldCustomizeId ) ) {
  2601. throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
  2602. }
  2603. oldSetting = api( oldCustomizeId );
  2604. if ( ! api.control.has( oldCustomizeId ) ) {
  2605. throw new Error( 'Expected control to exist: ' + oldCustomizeId );
  2606. }
  2607. oldControl = api.control( oldCustomizeId );
  2608. settingValue = oldSetting.get();
  2609. if ( ! settingValue ) {
  2610. throw new Error( 'Did not expect setting to be empty (deleted).' );
  2611. }
  2612. settingValue = _.clone( settingValue );
  2613. // If the parent menu item was also inserted, update the menu_item_parent to the new ID.
  2614. if ( settingValue.menu_item_parent < 0 ) {
  2615. if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) {
  2616. throw new Error( 'inserted ID for menu_item_parent not available' );
  2617. }
  2618. settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ];
  2619. }
  2620. // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id.
  2621. if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) {
  2622. settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ];
  2623. }
  2624. newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
  2625. newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
  2626. type: 'nav_menu_item',
  2627. transport: api.Menus.data.settingTransport,
  2628. previewer: api.previewer
  2629. } );
  2630. // Add the menu control.
  2631. newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, {
  2632. params: {
  2633. type: 'nav_menu_item',
  2634. content: '<li id="customize-control-nav_menu_item-' + String( update.post_id ) + '" class="customize-control customize-control-nav_menu_item"></li>',
  2635. menu_id: update.post_id,
  2636. section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']',
  2637. priority: oldControl.priority.get(),
  2638. active: true,
  2639. settings: {
  2640. 'default': newCustomizeId
  2641. },
  2642. menu_item_id: update.post_id
  2643. },
  2644. previewer: api.previewer
  2645. } );
  2646. // Remove old control.
  2647. oldControl.container.remove();
  2648. api.control.remove( oldCustomizeId );
  2649. // Add new control to take its place.
  2650. api.control.add( newCustomizeId, newControl );
  2651. // Delete the placeholder and preview the new setting.
  2652. oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
  2653. oldSetting.set( false );
  2654. oldSetting.preview();
  2655. newSetting.preview();
  2656. oldSetting._dirty = false;
  2657. newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) );
  2658. }
  2659. });
  2660. /*
  2661. * Update the settings for any nav_menu widgets that had selected a placeholder ID.
  2662. */
  2663. _.each( data.widget_nav_menu_updates, function( widgetSettingValue, widgetSettingId ) {
  2664. var setting = api( widgetSettingId );
  2665. if ( setting ) {
  2666. setting._value = widgetSettingValue;
  2667. setting.preview(); // Send to the preview now so that menu refresh will use the inserted menu.
  2668. }
  2669. });
  2670. };
  2671. /**
  2672. * Focus a menu item control.
  2673. *
  2674. * @param {string} menuItemId
  2675. */
  2676. api.Menus.focusMenuItemControl = function( menuItemId ) {
  2677. var control = api.Menus.getMenuItemControl( menuItemId );
  2678. if ( control ) {
  2679. control.focus();
  2680. }
  2681. };
  2682. /**
  2683. * Get the control for a given menu.
  2684. *
  2685. * @param menuId
  2686. * @return {wp.customize.controlConstructor.menus[]}
  2687. */
  2688. api.Menus.getMenuControl = function( menuId ) {
  2689. return api.control( 'nav_menu[' + menuId + ']' );
  2690. };
  2691. /**
  2692. * Given a menu item ID, get the control associated with it.
  2693. *
  2694. * @param {string} menuItemId
  2695. * @return {object|null}
  2696. */
  2697. api.Menus.getMenuItemControl = function( menuItemId ) {
  2698. return api.control( menuItemIdToSettingId( menuItemId ) );
  2699. };
  2700. /**
  2701. * @param {String} menuItemId
  2702. */
  2703. function menuItemIdToSettingId( menuItemId ) {
  2704. return 'nav_menu_item[' + menuItemId + ']';
  2705. }
  2706. /**
  2707. * Apply sanitize_text_field()-like logic to the supplied name, returning a
  2708. * "unnammed" fallback string if the name is then empty.
  2709. *
  2710. * @param {string} name
  2711. * @returns {string}
  2712. */
  2713. function displayNavMenuName( name ) {
  2714. name = name || '';
  2715. name = $( '<div>' ).text( name ).html(); // Emulate esc_html() which is used in wp-admin/nav-menus.php.
  2716. name = $.trim( name );
  2717. return name || api.Menus.data.l10n.unnamed;
  2718. }
  2719. })( wp.customize, wp, jQuery );