PageRenderTime 66ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-admin/js/customize-nav-menus.js

https://gitlab.com/webkod3r/tripolis
JavaScript | 1625 lines | 1115 code | 218 blank | 292 comment | 188 complexity | 074e06ecc789adcc5c08459a7252c714 MD5 | raw file
  1. /* global _wpCustomizeNavMenusSettings, wpNavMenu, console */
  2. ( function( api, wp, $ ) {
  3. 'use strict';
  4. /**
  5. * Set up wpNavMenu for drag and drop.
  6. */
  7. wpNavMenu.originalInit = wpNavMenu.init;
  8. wpNavMenu.options.menuItemDepthPerLevel = 20;
  9. wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
  10. wpNavMenu.options.targetTolerance = 10;
  11. wpNavMenu.init = function() {
  12. this.jQueryExtensions();
  13. };
  14. 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. * wp.customize.Menus.AvailableMenuItemsPanelView
  75. *
  76. * View class for the available menu items panel.
  77. *
  78. * @constructor
  79. * @augments wp.Backbone.View
  80. * @augments Backbone.View
  81. */
  82. api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend({
  83. el: '#available-menu-items',
  84. events: {
  85. 'input #menu-items-search': 'debounceSearch',
  86. 'keyup #menu-items-search': 'debounceSearch',
  87. 'focus .menu-item-tpl': 'focus',
  88. 'click .menu-item-tpl': '_submit',
  89. 'click #custom-menu-item-submit': '_submitLink',
  90. 'keypress #custom-menu-item-name': '_submitLink',
  91. 'keydown': 'keyboardAccessible'
  92. },
  93. // Cache current selected menu item.
  94. selected: null,
  95. // Cache menu control that opened the panel.
  96. currentMenuControl: null,
  97. debounceSearch: null,
  98. $search: null,
  99. searchTerm: '',
  100. rendered: false,
  101. pages: {},
  102. sectionContent: '',
  103. loading: false,
  104. initialize: function() {
  105. var self = this;
  106. if ( ! api.panel.has( 'nav_menus' ) ) {
  107. return;
  108. }
  109. this.$search = $( '#menu-items-search' );
  110. this.sectionContent = this.$el.find( '.accordion-section-content' );
  111. this.debounceSearch = _.debounce( self.search, 500 );
  112. _.bindAll( this, 'close' );
  113. // If the available menu items panel is open and the customize controls are
  114. // interacted with (other than an item being deleted), then close the
  115. // available menu items panel. Also close on back button click.
  116. $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
  117. var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
  118. isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
  119. if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
  120. self.close();
  121. }
  122. } );
  123. // Clear the search results.
  124. $( '.clear-results' ).on( 'click keydown', function( event ) {
  125. if ( event.type === 'keydown' && ( 13 !== event.which && 32 !== event.which ) ) { // "return" or "space" keys only
  126. return;
  127. }
  128. event.preventDefault();
  129. $( '#menu-items-search' ).val( '' ).focus();
  130. event.target.value = '';
  131. self.search( event );
  132. } );
  133. this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
  134. $( this ).removeClass( 'invalid' );
  135. });
  136. // Load available items if it looks like we'll need them.
  137. api.panel( 'nav_menus' ).container.bind( 'expanded', function() {
  138. if ( ! self.rendered ) {
  139. self.initList();
  140. self.rendered = true;
  141. }
  142. });
  143. // Load more items.
  144. this.sectionContent.scroll( function() {
  145. var totalHeight = self.$el.find( '.accordion-section.open .accordion-section-content' ).prop( 'scrollHeight' ),
  146. visibleHeight = self.$el.find( '.accordion-section.open' ).height();
  147. if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
  148. var type = $( this ).data( 'type' ),
  149. object = $( this ).data( 'object' );
  150. if ( 'search' === type ) {
  151. if ( self.searchTerm ) {
  152. self.doSearch( self.pages.search );
  153. }
  154. } else {
  155. self.loadItems( type, object );
  156. }
  157. }
  158. });
  159. // Close the panel if the URL in the preview changes
  160. api.previewer.bind( 'url', this.close );
  161. self.delegateEvents();
  162. },
  163. // Search input change handler.
  164. search: function( event ) {
  165. var $searchSection = $( '#available-menu-items-search' ),
  166. $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
  167. if ( ! event ) {
  168. return;
  169. }
  170. if ( this.searchTerm === event.target.value ) {
  171. return;
  172. }
  173. if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
  174. $otherSections.fadeOut( 100 );
  175. $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
  176. $searchSection.addClass( 'open' );
  177. $searchSection.find( '.clear-results' )
  178. .prop( 'tabIndex', 0 )
  179. .addClass( 'is-visible' );
  180. } else if ( '' === event.target.value ) {
  181. $searchSection.removeClass( 'open' );
  182. $otherSections.show();
  183. $searchSection.find( '.clear-results' )
  184. .prop( 'tabIndex', -1 )
  185. .removeClass( 'is-visible' );
  186. }
  187. this.searchTerm = event.target.value;
  188. this.pages.search = 1;
  189. this.doSearch( 1 );
  190. },
  191. // Get search results.
  192. doSearch: function( page ) {
  193. var self = this, params,
  194. $section = $( '#available-menu-items-search' ),
  195. $content = $section.find( '.accordion-section-content' ),
  196. itemTemplate = wp.template( 'available-menu-item' );
  197. if ( self.currentRequest ) {
  198. self.currentRequest.abort();
  199. }
  200. if ( page < 0 ) {
  201. return;
  202. } else if ( page > 1 ) {
  203. $section.addClass( 'loading-more' );
  204. $content.attr( 'aria-busy', 'true' );
  205. wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
  206. } else if ( '' === self.searchTerm ) {
  207. $content.html( '' );
  208. wp.a11y.speak( '' );
  209. return;
  210. }
  211. $section.addClass( 'loading' );
  212. self.loading = true;
  213. params = {
  214. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  215. 'wp_customize': 'on',
  216. 'search': self.searchTerm,
  217. 'page': page
  218. };
  219. self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
  220. self.currentRequest.done(function( data ) {
  221. var items;
  222. if ( 1 === page ) {
  223. // Clear previous results as it's a new search.
  224. $content.empty();
  225. }
  226. $section.removeClass( 'loading loading-more' );
  227. $content.attr( 'aria-busy', 'false' );
  228. $section.addClass( 'open' );
  229. self.loading = false;
  230. items = new api.Menus.AvailableItemCollection( data.items );
  231. self.collection.add( items.models );
  232. items.each( function( menuItem ) {
  233. $content.append( itemTemplate( menuItem.attributes ) );
  234. } );
  235. if ( 20 > items.length ) {
  236. self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
  237. } else {
  238. self.pages.search = self.pages.search + 1;
  239. }
  240. if ( items && page > 1 ) {
  241. wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
  242. } else if ( items && page === 1 ) {
  243. wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
  244. }
  245. });
  246. self.currentRequest.fail(function( data ) {
  247. // data.message may be undefined, for example when typing slow and the request is aborted.
  248. if ( data.message ) {
  249. $content.empty().append( $( '<p class="nothing-found"></p>' ).text( data.message ) );
  250. wp.a11y.speak( data.message );
  251. }
  252. self.pages.search = -1;
  253. });
  254. self.currentRequest.always(function() {
  255. $section.removeClass( 'loading loading-more' );
  256. $content.attr( 'aria-busy', 'false' );
  257. self.loading = false;
  258. self.currentRequest = null;
  259. });
  260. },
  261. // Render the individual items.
  262. initList: function() {
  263. var self = this;
  264. // Render the template for each item by type.
  265. _.each( api.Menus.data.itemTypes, function( itemType ) {
  266. self.pages[ itemType.type + ':' + itemType.object ] = 0;
  267. self.loadItems( itemType.type, itemType.object ); // @todo we need to combine these Ajax requests.
  268. } );
  269. },
  270. // Load available menu items.
  271. loadItems: function( type, object ) {
  272. var self = this, params, request, itemTemplate, availableMenuItemContainer;
  273. itemTemplate = wp.template( 'available-menu-item' );
  274. if ( -1 === self.pages[ type + ':' + object ] ) {
  275. return;
  276. }
  277. availableMenuItemContainer = $( '#available-menu-items-' + type + '-' + object );
  278. availableMenuItemContainer.find( '.accordion-section-title' ).addClass( 'loading' );
  279. self.loading = true;
  280. params = {
  281. 'customize-menus-nonce': api.settings.nonce['customize-menus'],
  282. 'wp_customize': 'on',
  283. 'type': type,
  284. 'object': object,
  285. 'page': self.pages[ type + ':' + object ]
  286. };
  287. request = wp.ajax.post( 'load-available-menu-items-customizer', params );
  288. request.done(function( data ) {
  289. var items, typeInner;
  290. items = data.items;
  291. if ( 0 === items.length ) {
  292. if ( 0 === self.pages[ type + ':' + object ] ) {
  293. availableMenuItemContainer
  294. .addClass( 'cannot-expand' )
  295. .removeClass( 'loading' )
  296. .find( '.accordion-section-title > button' )
  297. .prop( 'tabIndex', -1 );
  298. }
  299. self.pages[ type + ':' + object ] = -1;
  300. return;
  301. }
  302. items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
  303. self.collection.add( items.models );
  304. typeInner = availableMenuItemContainer.find( '.accordion-section-content' );
  305. items.each(function( menuItem ) {
  306. typeInner.append( itemTemplate( menuItem.attributes ) );
  307. });
  308. self.pages[ type + ':' + object ] += 1;
  309. });
  310. request.fail(function( data ) {
  311. if ( typeof console !== 'undefined' && console.error ) {
  312. console.error( data );
  313. }
  314. });
  315. request.always(function() {
  316. availableMenuItemContainer.find( '.accordion-section-title' ).removeClass( 'loading' );
  317. self.loading = false;
  318. });
  319. },
  320. // Adjust the height of each section of items to fit the screen.
  321. itemSectionHeight: function() {
  322. var sections, totalHeight, accordionHeight, diff;
  323. totalHeight = window.innerHeight;
  324. sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
  325. accordionHeight = 46 * ( 2 + sections.length ) - 13; // Magic numbers.
  326. diff = totalHeight - accordionHeight;
  327. if ( 120 < diff && 290 > diff ) {
  328. sections.css( 'max-height', diff );
  329. }
  330. },
  331. // Highlights a menu item.
  332. select: function( menuitemTpl ) {
  333. this.selected = $( menuitemTpl );
  334. this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
  335. this.selected.addClass( 'selected' );
  336. },
  337. // Highlights a menu item on focus.
  338. focus: function( event ) {
  339. this.select( $( event.currentTarget ) );
  340. },
  341. // Submit handler for keypress and click on menu item.
  342. _submit: function( event ) {
  343. // Only proceed with keypress if it is Enter or Spacebar
  344. if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
  345. return;
  346. }
  347. this.submit( $( event.currentTarget ) );
  348. },
  349. // Adds a selected menu item to the menu.
  350. submit: function( menuitemTpl ) {
  351. var menuitemId, menu_item;
  352. if ( ! menuitemTpl ) {
  353. menuitemTpl = this.selected;
  354. }
  355. if ( ! menuitemTpl || ! this.currentMenuControl ) {
  356. return;
  357. }
  358. this.select( menuitemTpl );
  359. menuitemId = $( this.selected ).data( 'menu-item-id' );
  360. menu_item = this.collection.findWhere( { id: menuitemId } );
  361. if ( ! menu_item ) {
  362. return;
  363. }
  364. this.currentMenuControl.addItemToMenu( menu_item.attributes );
  365. $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
  366. },
  367. // Submit handler for keypress and click on custom menu item.
  368. _submitLink: function( event ) {
  369. // Only proceed with keypress if it is Enter.
  370. if ( 'keypress' === event.type && 13 !== event.which ) {
  371. return;
  372. }
  373. this.submitLink();
  374. },
  375. // Adds the custom menu item to the menu.
  376. submitLink: function() {
  377. var menuItem,
  378. itemName = $( '#custom-menu-item-name' ),
  379. itemUrl = $( '#custom-menu-item-url' );
  380. if ( ! this.currentMenuControl ) {
  381. return;
  382. }
  383. if ( '' === itemName.val() ) {
  384. itemName.addClass( 'invalid' );
  385. return;
  386. } else if ( '' === itemUrl.val() || 'http://' === itemUrl.val() ) {
  387. itemUrl.addClass( 'invalid' );
  388. return;
  389. }
  390. menuItem = {
  391. 'title': itemName.val(),
  392. 'url': itemUrl.val(),
  393. 'type': 'custom',
  394. 'type_label': api.Menus.data.l10n.custom_label,
  395. 'object': ''
  396. };
  397. this.currentMenuControl.addItemToMenu( menuItem );
  398. // Reset the custom link form.
  399. itemUrl.val( 'http://' );
  400. itemName.val( '' );
  401. },
  402. // Opens the panel.
  403. open: function( menuControl ) {
  404. this.currentMenuControl = menuControl;
  405. this.itemSectionHeight();
  406. $( 'body' ).addClass( 'adding-menu-items' );
  407. // Collapse all controls.
  408. _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
  409. control.collapseForm();
  410. } );
  411. this.$el.find( '.selected' ).removeClass( 'selected' );
  412. this.$search.focus();
  413. },
  414. // Closes the panel
  415. close: function( options ) {
  416. options = options || {};
  417. if ( options.returnFocus && this.currentMenuControl ) {
  418. this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
  419. }
  420. this.currentMenuControl = null;
  421. this.selected = null;
  422. $( 'body' ).removeClass( 'adding-menu-items' );
  423. $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
  424. this.$search.val( '' );
  425. },
  426. // Add a few keyboard enhancements to the panel.
  427. keyboardAccessible: function( event ) {
  428. var isEnter = ( 13 === event.which ),
  429. isEsc = ( 27 === event.which ),
  430. isBackTab = ( 9 === event.which && event.shiftKey ),
  431. isSearchFocused = $( event.target ).is( this.$search );
  432. // If enter pressed but nothing entered, don't do anything
  433. if ( isEnter && ! this.$search.val() ) {
  434. return;
  435. }
  436. if ( isSearchFocused && isBackTab ) {
  437. this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
  438. event.preventDefault(); // Avoid additional back-tab.
  439. } else if ( isEsc ) {
  440. this.close( { returnFocus: true } );
  441. }
  442. }
  443. });
  444. /**
  445. * wp.customize.Menus.MenusPanel
  446. *
  447. * Customizer panel for menus. This is used only for screen options management.
  448. * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
  449. *
  450. * @constructor
  451. * @augments wp.customize.Panel
  452. */
  453. api.Menus.MenusPanel = api.Panel.extend({
  454. attachEvents: function() {
  455. api.Panel.prototype.attachEvents.call( this );
  456. var panel = this,
  457. panelMeta = panel.container.find( '.panel-meta' ),
  458. help = panelMeta.find( '.customize-help-toggle' ),
  459. content = panelMeta.find( '.customize-panel-description' ),
  460. options = $( '#screen-options-wrap' ),
  461. button = panelMeta.find( '.customize-screen-options-toggle' );
  462. button.on( 'click keydown', function( event ) {
  463. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  464. return;
  465. }
  466. event.preventDefault();
  467. // Hide description
  468. if ( content.not( ':hidden' ) ) {
  469. content.slideUp( 'fast' );
  470. help.attr( 'aria-expanded', 'false' );
  471. }
  472. if ( 'true' === button.attr( 'aria-expanded' ) ) {
  473. button.attr( 'aria-expanded', 'false' );
  474. panelMeta.removeClass( 'open' );
  475. panelMeta.removeClass( 'active-menu-screen-options' );
  476. options.slideUp( 'fast' );
  477. } else {
  478. button.attr( 'aria-expanded', 'true' );
  479. panelMeta.addClass( 'open' );
  480. panelMeta.addClass( 'active-menu-screen-options' );
  481. options.slideDown( 'fast' );
  482. }
  483. return false;
  484. } );
  485. // Help toggle
  486. help.on( 'click keydown', function( event ) {
  487. if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
  488. return;
  489. }
  490. event.preventDefault();
  491. if ( 'true' === button.attr( 'aria-expanded' ) ) {
  492. button.attr( 'aria-expanded', 'false' );
  493. help.attr( 'aria-expanded', 'true' );
  494. panelMeta.addClass( 'open' );
  495. panelMeta.removeClass( 'active-menu-screen-options' );
  496. options.slideUp( 'fast' );
  497. content.slideDown( 'fast' );
  498. }
  499. } );
  500. },
  501. /**
  502. * Show/hide/save screen options (columns). From common.js.
  503. */
  504. ready: function() {
  505. var panel = this;
  506. this.container.find( '.hide-column-tog' ).click( function() {
  507. var $t = $( this ), column = $t.val();
  508. if ( $t.prop( 'checked' ) ) {
  509. panel.checked( column );
  510. } else {
  511. panel.unchecked( column );
  512. }
  513. panel.saveManageColumnsState();
  514. });
  515. this.container.find( '.hide-column-tog' ).each( function() {
  516. var $t = $( this ), column = $t.val();
  517. if ( $t.prop( 'checked' ) ) {
  518. panel.checked( column );
  519. } else {
  520. panel.unchecked( column );
  521. }
  522. });
  523. },
  524. saveManageColumnsState: _.debounce( function() {
  525. var panel = this;
  526. if ( panel._updateHiddenColumnsRequest ) {
  527. panel._updateHiddenColumnsRequest.abort();
  528. }
  529. panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
  530. hidden: panel.hidden(),
  531. screenoptionnonce: $( '#screenoptionnonce' ).val(),
  532. page: 'nav-menus'
  533. } );
  534. panel._updateHiddenColumnsRequest.always( function() {
  535. panel._updateHiddenColumnsRequest = null;
  536. } );
  537. }, 2000 ),
  538. checked: function( column ) {
  539. this.container.addClass( 'field-' + column + '-active' );
  540. },
  541. unchecked: function( column ) {
  542. this.container.removeClass( 'field-' + column + '-active' );
  543. },
  544. hidden: function() {
  545. return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
  546. var id = this.id;
  547. return id.substring( 0, id.length - 5 );
  548. }).get().join( ',' );
  549. }
  550. } );
  551. /**
  552. * wp.customize.Menus.MenuSection
  553. *
  554. * Customizer section for menus. This is used only for lazy-loading child controls.
  555. * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
  556. *
  557. * @constructor
  558. * @augments wp.customize.Section
  559. */
  560. api.Menus.MenuSection = api.Section.extend({
  561. /**
  562. * @since Menu Customizer 0.3
  563. *
  564. * @param {String} id
  565. * @param {Object} options
  566. */
  567. initialize: function( id, options ) {
  568. var section = this;
  569. api.Section.prototype.initialize.call( section, id, options );
  570. section.deferred.initSortables = $.Deferred();
  571. },
  572. /**
  573. *
  574. */
  575. ready: function() {
  576. var section = this;
  577. if ( 'undefined' === typeof section.params.menu_id ) {
  578. throw new Error( 'params.menu_id was not defined' );
  579. }
  580. /*
  581. * Since newly created sections won't be registered in PHP, we need to prevent the
  582. * preview's sending of the activeSections to result in this control
  583. * being deactivated when the preview refreshes. So we can hook onto
  584. * the setting that has the same ID and its presence can dictate
  585. * whether the section is active.
  586. */
  587. section.active.validate = function() {
  588. if ( ! api.has( section.id ) ) {
  589. return false;
  590. }
  591. return !! api( section.id ).get();
  592. };
  593. section.populateControls();
  594. section.navMenuLocationSettings = {};
  595. section.assignedLocations = new api.Value( [] );
  596. api.each(function( setting, id ) {
  597. var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
  598. if ( matches ) {
  599. section.navMenuLocationSettings[ matches[1] ] = setting;
  600. setting.bind( function() {
  601. section.refreshAssignedLocations();
  602. });
  603. }
  604. });
  605. section.assignedLocations.bind(function( to ) {
  606. section.updateAssignedLocationsInSectionTitle( to );
  607. });
  608. section.refreshAssignedLocations();
  609. api.bind( 'pane-contents-reflowed', function() {
  610. // Skip menus that have been removed.
  611. if ( ! section.container.parent().length ) {
  612. return;
  613. }
  614. section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
  615. section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  616. section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  617. section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  618. section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  619. } );
  620. },
  621. populateControls: function() {
  622. var section = this, menuNameControlId, menuAutoAddControlId, menuControl, menuNameControl, menuAutoAddControl;
  623. // Add the control for managing the menu name.
  624. menuNameControlId = section.id + '[name]';
  625. menuNameControl = api.control( menuNameControlId );
  626. if ( ! menuNameControl ) {
  627. menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
  628. params: {
  629. type: 'nav_menu_name',
  630. 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
  631. label: api.Menus.data.l10n.menuNameLabel,
  632. active: true,
  633. section: section.id,
  634. priority: 0,
  635. settings: {
  636. 'default': section.id
  637. }
  638. }
  639. } );
  640. api.control.add( menuNameControl.id, menuNameControl );
  641. menuNameControl.active.set( true );
  642. }
  643. // Add the menu control.
  644. menuControl = api.control( section.id );
  645. if ( ! menuControl ) {
  646. menuControl = new api.controlConstructor.nav_menu( section.id, {
  647. params: {
  648. type: 'nav_menu',
  649. 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
  650. section: section.id,
  651. priority: 998,
  652. active: true,
  653. settings: {
  654. 'default': section.id
  655. },
  656. menu_id: section.params.menu_id
  657. }
  658. } );
  659. api.control.add( menuControl.id, menuControl );
  660. menuControl.active.set( true );
  661. }
  662. // Add the control for managing the menu auto_add.
  663. menuAutoAddControlId = section.id + '[auto_add]';
  664. menuAutoAddControl = api.control( menuAutoAddControlId );
  665. if ( ! menuAutoAddControl ) {
  666. menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
  667. params: {
  668. type: 'nav_menu_auto_add',
  669. 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
  670. label: '',
  671. active: true,
  672. section: section.id,
  673. priority: 999,
  674. settings: {
  675. 'default': section.id
  676. }
  677. }
  678. } );
  679. api.control.add( menuAutoAddControl.id, menuAutoAddControl );
  680. menuAutoAddControl.active.set( true );
  681. }
  682. },
  683. /**
  684. *
  685. */
  686. refreshAssignedLocations: function() {
  687. var section = this,
  688. menuTermId = section.params.menu_id,
  689. currentAssignedLocations = [];
  690. _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
  691. if ( setting() === menuTermId ) {
  692. currentAssignedLocations.push( themeLocation );
  693. }
  694. });
  695. section.assignedLocations.set( currentAssignedLocations );
  696. },
  697. /**
  698. * @param {array} themeLocations
  699. */
  700. updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
  701. var section = this,
  702. $title;
  703. $title = section.container.find( '.accordion-section-title:first' );
  704. $title.find( '.menu-in-location' ).remove();
  705. _.each( themeLocationSlugs, function( themeLocationSlug ) {
  706. var $label, locationName;
  707. $label = $( '<span class="menu-in-location"></span>' );
  708. locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ];
  709. $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) );
  710. $title.append( $label );
  711. });
  712. section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length );
  713. },
  714. onChangeExpanded: function( expanded, args ) {
  715. var section = this;
  716. if ( expanded ) {
  717. wpNavMenu.menuList = section.container.find( '.accordion-section-content:first' );
  718. wpNavMenu.targetList = wpNavMenu.menuList;
  719. // Add attributes needed by wpNavMenu
  720. $( '#menu-to-edit' ).removeAttr( 'id' );
  721. wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
  722. _.each( api.section( section.id ).controls(), function( control ) {
  723. if ( 'nav_menu_item' === control.params.type ) {
  724. control.actuallyEmbed();
  725. }
  726. } );
  727. if ( 'resolved' !== section.deferred.initSortables.state() ) {
  728. wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
  729. section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
  730. // @todo Note that wp.customize.reflowPaneContents() is debounced, so this immediate change will show a slight flicker while priorities get updated.
  731. api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
  732. }
  733. }
  734. api.Section.prototype.onChangeExpanded.call( section, expanded, args );
  735. }
  736. });
  737. /**
  738. * wp.customize.Menus.NewMenuSection
  739. *
  740. * Customizer section for new menus.
  741. * Note that 'new_menu' must match the WP_Customize_New_Menu_Section::$type.
  742. *
  743. * @constructor
  744. * @augments wp.customize.Section
  745. */
  746. api.Menus.NewMenuSection = api.Section.extend({
  747. /**
  748. * Add behaviors for the accordion section.
  749. *
  750. * @since Menu Customizer 0.3
  751. */
  752. attachEvents: function() {
  753. var section = this;
  754. this.container.on( 'click', '.add-menu-toggle', function() {
  755. if ( section.expanded() ) {
  756. section.collapse();
  757. } else {
  758. section.expand();
  759. }
  760. });
  761. },
  762. /**
  763. * Update UI to reflect expanded state.
  764. *
  765. * @since 4.1.0
  766. *
  767. * @param {Boolean} expanded
  768. */
  769. onChangeExpanded: function( expanded ) {
  770. var section = this,
  771. button = section.container.find( '.add-menu-toggle' ),
  772. content = section.container.find( '.new-menu-section-content' ),
  773. customizer = section.container.closest( '.wp-full-overlay-sidebar-content' );
  774. if ( expanded ) {
  775. button.addClass( 'open' );
  776. button.attr( 'aria-expanded', 'true' );
  777. content.slideDown( 'fast', function() {
  778. customizer.scrollTop( customizer.height() );
  779. });
  780. } else {
  781. button.removeClass( 'open' );
  782. button.attr( 'aria-expanded', 'false' );
  783. content.slideUp( 'fast' );
  784. content.find( '.menu-name-field' ).removeClass( 'invalid' );
  785. }
  786. }
  787. });
  788. /**
  789. * wp.customize.Menus.MenuLocationControl
  790. *
  791. * Customizer control for menu locations (rendered as a <select>).
  792. * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
  793. *
  794. * @constructor
  795. * @augments wp.customize.Control
  796. */
  797. api.Menus.MenuLocationControl = api.Control.extend({
  798. initialize: function( id, options ) {
  799. var control = this,
  800. matches = id.match( /^nav_menu_locations\[(.+?)]/ );
  801. control.themeLocation = matches[1];
  802. api.Control.prototype.initialize.call( control, id, options );
  803. },
  804. ready: function() {
  805. var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
  806. // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
  807. control.setting.validate = function( value ) {
  808. return parseInt( value, 10 );
  809. };
  810. // Add/remove menus from the available options when they are added and removed.
  811. api.bind( 'add', function( setting ) {
  812. var option, menuId, matches = setting.id.match( navMenuIdRegex );
  813. if ( ! matches || false === setting() ) {
  814. return;
  815. }
  816. menuId = matches[1];
  817. option = new Option( displayNavMenuName( setting().name ), menuId );
  818. control.container.find( 'select' ).append( option );
  819. });
  820. api.bind( 'remove', function( setting ) {
  821. var menuId, matches = setting.id.match( navMenuIdRegex );
  822. if ( ! matches ) {
  823. return;
  824. }
  825. menuId = parseInt( matches[1], 10 );
  826. if ( control.setting() === menuId ) {
  827. control.setting.set( '' );
  828. }
  829. control.container.find( 'option[value=' + menuId + ']' ).remove();
  830. });
  831. api.bind( 'change', function( setting ) {
  832. var menuId, matches = setting.id.match( navMenuIdRegex );
  833. if ( ! matches ) {
  834. return;
  835. }
  836. menuId = parseInt( matches[1], 10 );
  837. if ( false === setting() ) {
  838. if ( control.setting() === menuId ) {
  839. control.setting.set( '' );
  840. }
  841. control.container.find( 'option[value=' + menuId + ']' ).remove();
  842. } else {
  843. control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
  844. }
  845. });
  846. }
  847. });
  848. /**
  849. * wp.customize.Menus.MenuItemControl
  850. *
  851. * Customizer control for menu items.
  852. * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
  853. *
  854. * @constructor
  855. * @augments wp.customize.Control
  856. */
  857. api.Menus.MenuItemControl = api.Control.extend({
  858. /**
  859. * @inheritdoc
  860. */
  861. initialize: function( id, options ) {
  862. var control = this;
  863. api.Control.prototype.initialize.call( control, id, options );
  864. control.active.validate = function() {
  865. var value, section = api.section( control.section() );
  866. if ( section ) {
  867. value = section.active();
  868. } else {
  869. value = false;
  870. }
  871. return value;
  872. };
  873. },
  874. /**
  875. * @since Menu Customizer 0.3
  876. *
  877. * Override the embed() method to do nothing,
  878. * so that the control isn't embedded on load,
  879. * unless the containing section is already expanded.
  880. */
  881. embed: function() {
  882. var control = this,
  883. sectionId = control.section(),
  884. section;
  885. if ( ! sectionId ) {
  886. return;
  887. }
  888. section = api.section( sectionId );
  889. if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
  890. control.actuallyEmbed();
  891. }
  892. },
  893. /**
  894. * This function is called in Section.onChangeExpanded() so the control
  895. * will only get embedded when the Section is first expanded.
  896. *
  897. * @since Menu Customizer 0.3
  898. */
  899. actuallyEmbed: function() {
  900. var control = this;
  901. if ( 'resolved' === control.deferred.embedded.state() ) {
  902. return;
  903. }
  904. control.renderContent();
  905. control.deferred.embedded.resolve(); // This triggers control.ready().
  906. },
  907. /**
  908. * Set up the control.
  909. */
  910. ready: function() {
  911. if ( 'undefined' === typeof this.params.menu_item_id ) {
  912. throw new Error( 'params.menu_item_id was not defined' );
  913. }
  914. this._setupControlToggle();
  915. this._setupReorderUI();
  916. this._setupUpdateUI();
  917. this._setupRemoveUI();
  918. this._setupLinksUI();
  919. this._setupTitleUI();
  920. },
  921. /**
  922. * Show/hide the settings when clicking on the menu item handle.
  923. */
  924. _setupControlToggle: function() {
  925. var control = this;
  926. this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
  927. e.preventDefault();
  928. e.stopPropagation();
  929. var menuControl = control.getMenuControl();
  930. if ( menuControl.isReordering || menuControl.isSorting ) {
  931. return;
  932. }
  933. control.toggleForm();
  934. } );
  935. },
  936. /**
  937. * Set up the menu-item-reorder-nav
  938. */
  939. _setupReorderUI: function() {
  940. var control = this, template, $reorderNav;
  941. template = wp.template( 'menu-item-reorder-nav' );
  942. // Add the menu item reordering elements to the menu item control.
  943. control.container.find( '.item-controls' ).after( template );
  944. // Handle clicks for up/down/left-right on the reorder nav.
  945. $reorderNav = control.container.find( '.menu-item-reorder-nav' );
  946. $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
  947. var moveBtn = $( this );
  948. moveBtn.focus();
  949. var isMoveUp = moveBtn.is( '.menus-move-up' ),
  950. isMoveDown = moveBtn.is( '.menus-move-down' ),
  951. isMoveLeft = moveBtn.is( '.menus-move-left' ),
  952. isMoveRight = moveBtn.is( '.menus-move-right' );
  953. if ( isMoveUp ) {
  954. control.moveUp();
  955. } else if ( isMoveDown ) {
  956. control.moveDown();
  957. } else if ( isMoveLeft ) {
  958. control.moveLeft();
  959. } else if ( isMoveRight ) {
  960. control.moveRight();
  961. }
  962. moveBtn.focus(); // Re-focus after the container was moved.
  963. } );
  964. },
  965. /**
  966. * Set up event handlers for menu item updating.
  967. */
  968. _setupUpdateUI: function() {
  969. var control = this,
  970. settingValue = control.setting();
  971. control.elements = {};
  972. control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
  973. control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
  974. control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
  975. control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
  976. control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
  977. control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
  978. control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
  979. // @todo allow other elements, added by plugins, to be automatically picked up here; allow additional values to be added to setting array.
  980. _.each( control.elements, function( element, property ) {
  981. element.bind(function( value ) {
  982. if ( element.element.is( 'input[type=checkbox]' ) ) {
  983. value = ( value ) ? element.element.val() : '';
  984. }
  985. var settingValue = control.setting();
  986. if ( settingValue && settingValue[ property ] !== value ) {
  987. settingValue = _.clone( settingValue );
  988. settingValue[ property ] = value;
  989. control.setting.set( settingValue );
  990. }
  991. });
  992. if ( settingValue ) {
  993. if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) {
  994. element.set( settingValue[ property ].join( ' ' ) );
  995. } else {
  996. element.set( settingValue[ property ] );
  997. }
  998. }
  999. });
  1000. control.setting.bind(function( to, from ) {
  1001. var itemId = control.params.menu_item_id,
  1002. followingSiblingItemControls = [],
  1003. childrenItemControls = [],
  1004. menuControl;
  1005. if ( false === to ) {
  1006. menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
  1007. control.container.remove();
  1008. _.each( menuControl.getMenuItemControls(), function( otherControl ) {
  1009. if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
  1010. followingSiblingItemControls.push( otherControl );
  1011. } else if ( otherControl.setting().menu_item_parent === itemId ) {
  1012. childrenItemControls.push( otherControl );
  1013. }
  1014. });
  1015. // Shift all following siblings by the number of children this item has.
  1016. _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
  1017. var value = _.clone( followingSiblingItemControl.setting() );
  1018. value.position += childrenItemControls.length;
  1019. followingSiblingItemControl.setting.set( value );
  1020. });
  1021. // Now move the children up to be the new subsequent siblings.
  1022. _.each( childrenItemControls, function( childrenItemControl, i ) {
  1023. var value = _.clone( childrenItemControl.setting() );
  1024. value.position = from.position + i;
  1025. value.menu_item_parent = from.menu_item_parent;
  1026. childrenItemControl.setting.set( value );
  1027. });
  1028. menuControl.debouncedReflowMenuItems();
  1029. } else {
  1030. // Update the elements' values to match the new setting properties.
  1031. _.each( to, function( value, key ) {
  1032. if ( control.elements[ key] ) {
  1033. control.elements[ key ].set( to[ key ] );
  1034. }
  1035. } );
  1036. control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
  1037. // Handle UI updates when the position or depth (parent) change.
  1038. if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
  1039. control.getMenuControl().debouncedReflowMenuItems();
  1040. }
  1041. }
  1042. });
  1043. },
  1044. /**
  1045. * Set up event handlers for menu item deletion.
  1046. */
  1047. _setupRemoveUI: function() {
  1048. var control = this, $removeBtn;
  1049. // Configure delete button.
  1050. $removeBtn = control.container.find( '.item-delete' );
  1051. $removeBtn.on( 'click', function() {
  1052. // Find an adjacent element to add focus to when this menu item goes away
  1053. var addingItems = true, $adjacentFocusTarget, $next, $prev;
  1054. if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
  1055. addingItems = false;
  1056. }
  1057. $next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first();
  1058. $prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first();
  1059. if ( $next.length ) {
  1060. $adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
  1061. } else if ( $prev.length ) {
  1062. $adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
  1063. } else {
  1064. $adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first();
  1065. }
  1066. control.container.slideUp( function() {
  1067. control.setting.set( false );
  1068. wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
  1069. $adjacentFocusTarget.focus(); // keyboard accessibility
  1070. } );
  1071. } );
  1072. },
  1073. _setupLinksUI: function() {
  1074. var $origBtn;
  1075. // Configure original link.
  1076. $origBtn = this.container.find( 'a.original-link' );
  1077. $origBtn.on( 'click', function( e ) {
  1078. e.preventDefault();
  1079. api.previewer.previewUrl( e.target.toString() );
  1080. } );
  1081. },
  1082. /**
  1083. * Update item handle title when changed.
  1084. */
  1085. _setupTitleUI: function() {
  1086. var control = this;
  1087. control.setting.bind( function( item ) {
  1088. if ( ! item ) {
  1089. return;
  1090. }
  1091. var titleEl = control.container.find( '.menu-item-title' ),
  1092. titleText = item.title || api.Menus.data.l10n.untitled;
  1093. if ( item._invalid ) {
  1094. titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText );
  1095. }
  1096. // Don't update to an empty title.
  1097. if ( item.title ) {
  1098. titleEl
  1099. .text( titleText )
  1100. .removeClass( 'no-title' );
  1101. } else {
  1102. titleEl
  1103. .text( titleText )
  1104. .addClass( 'no-title' );
  1105. }
  1106. } );
  1107. },
  1108. /**
  1109. *
  1110. * @returns {number}
  1111. */
  1112. getDepth: function() {
  1113. var control = this, setting = control.setting(), depth = 0;
  1114. if ( ! setting ) {
  1115. return 0;
  1116. }
  1117. while ( setting && setting.menu_item_parent ) {
  1118. depth += 1;
  1119. control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
  1120. if ( ! control ) {
  1121. break;
  1122. }
  1123. setting = control.setting();
  1124. }
  1125. return depth;
  1126. },
  1127. /**
  1128. * Amend the control's params with the data necessary for the JS template just in time.
  1129. */
  1130. renderContent: function() {
  1131. var control = this,
  1132. settingValue = control.setting(),
  1133. containerClasses;
  1134. control.params.title = settingValue.title || '';
  1135. control.params.depth = control.getDepth();
  1136. control.container.data( 'item-depth', control.params.depth );
  1137. containerClasses = [
  1138. 'menu-item',
  1139. 'menu-item-depth-' + String( control.params.depth ),
  1140. 'menu-item-' + settingValue.object,
  1141. 'menu-item-edit-inactive'
  1142. ];
  1143. if ( settingValue._invalid ) {
  1144. containerClasses.push( 'menu-item-invalid' );
  1145. control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title );
  1146. } else if ( 'draft' === settingValue.status ) {
  1147. containerClasses.push( 'pending' );
  1148. control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
  1149. }
  1150. control.params.el_classes = containerClasses.join( ' ' );
  1151. control.params.item_type_label = settingValue.type_label;
  1152. control.params.item_type = settingValue.type;
  1153. control.params.url = settingValue.url;
  1154. control.params.target = settingValue.target;
  1155. control.params.attr_title = settingValue.attr_title;
  1156. control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
  1157. control.params.attr_title = settingValue.attr_title;
  1158. control.params.xfn = settingValue.xfn;
  1159. control.params.description = settingValue.description;
  1160. control.params.parent = settingValue.menu_item_parent;
  1161. control.params.original_title = settingValue.original_title || '';
  1162. control.container.addClass( control.params.el_classes );
  1163. api.Control.prototype.renderContent.call( control );
  1164. },
  1165. /***********************************************************************
  1166. * Begin public API methods
  1167. **********************************************************************/
  1168. /**
  1169. * @return {wp.customize.controlConstructor.nav_menu|null}
  1170. */
  1171. getMenuControl: function() {
  1172. var control = this, settingValue = control.setting();
  1173. if ( settingValue && settingValue.nav_menu_term_id ) {
  1174. return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
  1175. } else {
  1176. return null;
  1177. }
  1178. },
  1179. /**
  1180. * Expand the accordion section containing a control
  1181. */
  1182. expandControlSection: function() {
  1183. var $section = this.container.closest( '.accordion-section' );
  1184. if ( ! $section.hasClass( 'open' ) ) {
  1185. $section.find( '.accordion-section-title:first' ).trigger( 'click' );
  1186. }
  1187. },
  1188. /**
  1189. * Expand the menu item form control.
  1190. *
  1191. * @since 4.5.0 Added params.completeCallback.
  1192. *
  1193. * @param {Object} [params] - Optional params.
  1194. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1195. */
  1196. expandForm: function( params ) {
  1197. this.toggleForm( true, params );
  1198. },
  1199. /**
  1200. * Collapse the menu item form control.
  1201. *
  1202. * @since 4.5.0 Added params.completeCallback.
  1203. *
  1204. * @param {Object} [params] - Optional params.
  1205. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1206. */
  1207. collapseForm: function( params ) {
  1208. this.toggleForm( false, params );
  1209. },
  1210. /**
  1211. * Expand or collapse the menu item control.
  1212. *
  1213. * @since 4.5.0 Added params.completeCallback.
  1214. *
  1215. * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
  1216. * @param {Object} [params] - Optional params.
  1217. * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
  1218. */
  1219. toggleForm: function( showOrHide, params ) {
  1220. var self = this, $menuitem, $inside, complete;
  1221. $menuitem = this.container;
  1222. $inside = $menuitem.find( '.menu-item-settings:first' );
  1223. if ( 'undefined' === typeof showOrHide ) {
  1224. showOrHide = ! $inside.is( ':visible' );
  1225. }
  1226. // Already expanded or collapsed.
  1227. if ( $inside.is( ':visible' ) === showOrHide ) {
  1228. if ( params && params.completeCallback ) {
  1229. params.completeCallback();
  1230. }
  1231. return;
  1232. }
  1233. if ( showOrHide ) {
  1234. // Close all other menu item controls before expanding this one.
  1235. api.control.each( function( otherControl ) {
  1236. if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1237. otherControl.collapseForm();
  1238. }
  1239. } );
  1240. complete = function() {
  1241. $menuitem
  1242. .removeClass( 'menu-item-edit-inactive' )
  1243. .addClass( 'menu-item-edit-active' );
  1244. self.container.trigger( 'expanded' );
  1245. if ( params && params.completeCallback ) {
  1246. params.completeCallback();
  1247. }
  1248. };
  1249. $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' );
  1250. $inside.slideDown( 'fast', complete );
  1251. self.container.trigger( 'expand' );
  1252. } else {
  1253. complete = function() {
  1254. $menuitem
  1255. .addClass( 'menu-item-edit-inactive' )
  1256. .removeClass( 'menu-item-edit-active' );
  1257. self.container.trigger( 'collapsed' );
  1258. if ( params && params.completeCallback ) {
  1259. params.completeCallback();
  1260. }
  1261. };
  1262. self.container.trigger( 'collapse' );
  1263. $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' );
  1264. $inside.slideUp( 'fast', complete );
  1265. }
  1266. },
  1267. /**
  1268. * Expand the containing menu section, expand the form, and focus on
  1269. * the first input in the control.
  1270. *
  1271. * @since 4.5.0 Added params.completeCallback.
  1272. *
  1273. * @param {Object} [params] - Params object.
  1274. * @param {Function} [params.completeCallback] - Optional callback function when focus has completed.
  1275. */
  1276. focus: function( params ) {
  1277. params = params || {};
  1278. var control = this, originalCompleteCallback = params.completeCallback;
  1279. control.expandControlSection();
  1280. params.completeCallback = function() {
  1281. var focusable;
  1282. // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
  1283. focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' );
  1284. focusable.first().focus();
  1285. if ( originalCompleteCallback ) {
  1286. originalCompleteCallback();
  1287. }
  1288. };
  1289. control.expandForm( params );
  1290. },
  1291. /**
  1292. * Move menu item up one in the menu.
  1293. */
  1294. moveUp: function() {
  1295. this._changePosition( -1 );
  1296. wp.a11y.speak( api.Menus.data.l10n.movedUp );
  1297. },
  1298. /**
  1299. * Move menu item up one in the menu.
  1300. */
  1301. moveDown: function() {
  1302. this._changePosition( 1 );
  1303. wp.a11y.speak( api.Menus.data.l10n.movedDown );
  1304. },
  1305. /**
  1306. * Move menu item and all children up one level of depth.
  1307. */
  1308. moveLeft: function() {
  1309. this._changeDepth( -1 );
  1310. wp.a11y.speak( api.Menus.data.l10n.movedLeft );
  1311. },
  1312. /**
  1313. * Move menu item and children one level deeper, as a submenu of the previous item.
  1314. */
  1315. moveRight: function() {
  1316. this._changeDepth( 1 );
  1317. wp.a11y.speak( api.Menus.data.l10n.movedRight );
  1318. },
  1319. /**
  1320. * Note that this will trigger a UI update, causing child items to
  1321. * move as well and cardinal order class names to be updated.
  1322. *
  1323. * @private
  1324. *
  1325. * @param {Number} offset 1|-1
  1326. */
  1327. _changePosition: function( offset ) {
  1328. var control = this,
  1329. adjacentSetting,
  1330. settingValue = _.clone( control.setting() ),
  1331. siblingSettings = [],
  1332. realPosition;
  1333. if ( 1 !== offset && -1 !== offset ) {
  1334. throw new Error( 'Offset changes by 1 are only supported.' );
  1335. }
  1336. // Skip moving deleted items.
  1337. if ( ! control.setting() ) {
  1338. return;
  1339. }
  1340. // Locate the other items under the same parent (siblings).
  1341. _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
  1342. if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
  1343. siblingSettings.push( otherControl.setting );
  1344. }
  1345. });
  1346. siblingSettings.sort(function( a, b ) {
  1347. return a().position - b().position;
  1348. });
  1349. realPosition = _.indexOf( siblingSettings, control.setting );
  1350. if ( -1 === realPosition ) {
  1351. throw new Error( 'Expected setting to be among siblings.' );
  1352. }
  1353. // Skip doing anything if the item is already at the edge in the desired direction.
  1354. if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
  1355. // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
  1356. return;
  1357. }
  1358. // Update any adjacent menu item setting to take on this item's position.
  1359. adjacentSetting = siblingSettings[ realPosition + offset ];
  1360. if ( adjacentSetting ) {
  1361. adjacentSetting.set( $.extend(
  1362. _.clone( adjacentSetting() ),
  1363. {
  1364. position: settingValue.position
  1365. }
  1366. ) );
  1367. }
  1368. settingValue.position += offset;
  1369. control.setting.set( settingValue );
  1370. },
  1371. /**
  1372. * Note that this will trigger a UI update, causing child items to
  1373. * move as well and cardinal order class names to be updated.
  1374. *
  1375. * @private
  1376. *
  1377. * @param {Number} offset 1|-1
  1378. */
  1379. _changeDepth: function( offset ) {
  1380. if ( 1 !== offset && -1 !== offset ) {
  1381. throw new Error( 'Offset changes by 1 are only supported.' );
  1382. }
  1383. var control = this,
  1384. settingValue = _.clone( control.setting() ),
  1385. siblingControls = [],
  1386. realPosition,
  1387. siblingControl,
  1388. parentControl;
  1389. // Locate the other items under the same parent (siblings).
  1390. _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
  1391. if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
  1392. siblingControls.push( otherControl );
  1393. }
  1394. });
  1395. siblingControls.sort(function( a, b ) {
  1396. return a.setting().position - b.setting().position;
  1397. });
  1398. realPosition = _.indexOf( siblingControls, control );
  1399. if ( -1 === realPosition ) {
  1400. throw new Error( 'Expected control to be among siblings.' );
  1401. }
  1402. if ( -1 === offset ) {
  1403. // Skip moving left an item that is already at the top level.
  1404. if ( ! settingValue.menu_item_parent ) {
  1405. return;
  1406. }
  1407. parentControl = api.control( 'nav_menu_item[' + set