vertical-tabs.es6.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /**
  2. * @file
  3. * Define vertical tabs functionality.
  4. */
  5. /**
  6. * Triggers when form values inside a vertical tab changes.
  7. *
  8. * This is used to update the summary in vertical tabs in order to know what
  9. * are the important fields' values.
  10. *
  11. * @event summaryUpdated
  12. */
  13. (function($, Drupal, drupalSettings) {
  14. /**
  15. * Show the parent vertical tab pane of a targeted page fragment.
  16. *
  17. * In order to make sure a targeted element inside a vertical tab pane is
  18. * visible on a hash change or fragment link click, show all parent panes.
  19. *
  20. * @param {jQuery.Event} e
  21. * The event triggered.
  22. * @param {jQuery} $target
  23. * The targeted node as a jQuery object.
  24. */
  25. const handleFragmentLinkClickOrHashChange = (e, $target) => {
  26. $target.parents('.vertical-tabs__pane').each((index, pane) => {
  27. $(pane)
  28. .data('verticalTab')
  29. .focus();
  30. });
  31. };
  32. /**
  33. * This script transforms a set of details into a stack of vertical tabs.
  34. *
  35. * Each tab may have a summary which can be updated by another
  36. * script. For that to work, each details element has an associated
  37. * 'verticalTabCallback' (with jQuery.data() attached to the details),
  38. * which is called every time the user performs an update to a form
  39. * element inside the tab pane.
  40. *
  41. * @type {Drupal~behavior}
  42. *
  43. * @prop {Drupal~behaviorAttach} attach
  44. * Attaches behaviors for vertical tabs.
  45. */
  46. Drupal.behaviors.verticalTabs = {
  47. attach(context) {
  48. const width = drupalSettings.widthBreakpoint || 640;
  49. const mq = `(max-width: ${width}px)`;
  50. if (window.matchMedia(mq).matches) {
  51. return;
  52. }
  53. /**
  54. * Binds a listener to handle fragment link clicks and URL hash changes.
  55. */
  56. $('body')
  57. .once('vertical-tabs-fragments')
  58. .on(
  59. 'formFragmentLinkClickOrHashChange.verticalTabs',
  60. handleFragmentLinkClickOrHashChange,
  61. );
  62. $(context)
  63. .find('[data-vertical-tabs-panes]')
  64. .once('vertical-tabs')
  65. .each(function() {
  66. const $this = $(this).addClass('vertical-tabs__panes');
  67. const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
  68. let tabFocus;
  69. // Check if there are some details that can be converted to
  70. // vertical-tabs.
  71. const $details = $this.find('> details');
  72. if ($details.length === 0) {
  73. return;
  74. }
  75. // Create the tab column.
  76. const tabList = $('<ul class="vertical-tabs__menu"></ul>');
  77. $this
  78. .wrap('<div class="vertical-tabs clearfix"></div>')
  79. .before(tabList);
  80. // Transform each details into a tab.
  81. $details.each(function() {
  82. const $that = $(this);
  83. const verticalTab = new Drupal.verticalTab({
  84. title: $that.find('> summary').text(),
  85. details: $that,
  86. });
  87. tabList.append(verticalTab.item);
  88. $that
  89. .removeClass('collapsed')
  90. // prop() can't be used on browsers not supporting details element,
  91. // the style won't apply to them if prop() is used.
  92. .attr('open', true)
  93. .addClass('vertical-tabs__pane')
  94. .data('verticalTab', verticalTab);
  95. if (this.id === focusID) {
  96. tabFocus = $that;
  97. }
  98. });
  99. $(tabList)
  100. .find('> li')
  101. .eq(0)
  102. .addClass('first');
  103. $(tabList)
  104. .find('> li')
  105. .eq(-1)
  106. .addClass('last');
  107. if (!tabFocus) {
  108. // If the current URL has a fragment and one of the tabs contains an
  109. // element that matches the URL fragment, activate that tab.
  110. const $locationHash = $this.find(window.location.hash);
  111. if (window.location.hash && $locationHash.length) {
  112. tabFocus = $locationHash.closest('.vertical-tabs__pane');
  113. } else {
  114. tabFocus = $this.find('> .vertical-tabs__pane').eq(0);
  115. }
  116. }
  117. if (tabFocus.length) {
  118. tabFocus.data('verticalTab').focus();
  119. }
  120. });
  121. },
  122. };
  123. /**
  124. * The vertical tab object represents a single tab within a tab group.
  125. *
  126. * @constructor
  127. *
  128. * @param {object} settings
  129. * Settings object.
  130. * @param {string} settings.title
  131. * The name of the tab.
  132. * @param {jQuery} settings.details
  133. * The jQuery object of the details element that is the tab pane.
  134. *
  135. * @fires event:summaryUpdated
  136. *
  137. * @listens event:summaryUpdated
  138. */
  139. Drupal.verticalTab = function(settings) {
  140. const self = this;
  141. $.extend(this, settings, Drupal.theme('verticalTab', settings));
  142. this.link.attr('href', `#${settings.details.attr('id')}`);
  143. this.link.on('click', e => {
  144. e.preventDefault();
  145. self.focus();
  146. });
  147. // Keyboard events added:
  148. // Pressing the Enter key will open the tab pane.
  149. this.link.on('keydown', event => {
  150. if (event.keyCode === 13) {
  151. event.preventDefault();
  152. self.focus();
  153. // Set focus on the first input field of the visible details/tab pane.
  154. $('.vertical-tabs__pane :input:visible:enabled')
  155. .eq(0)
  156. .trigger('focus');
  157. }
  158. });
  159. this.details
  160. .on('summaryUpdated', () => {
  161. self.updateSummary();
  162. })
  163. .trigger('summaryUpdated');
  164. };
  165. Drupal.verticalTab.prototype = {
  166. /**
  167. * Displays the tab's content pane.
  168. */
  169. focus() {
  170. this.details
  171. .siblings('.vertical-tabs__pane')
  172. .each(function() {
  173. const tab = $(this).data('verticalTab');
  174. tab.details.hide();
  175. tab.item.removeClass('is-selected');
  176. })
  177. .end()
  178. .show()
  179. .siblings(':hidden.vertical-tabs__active-tab')
  180. .val(this.details.attr('id'));
  181. this.item.addClass('is-selected');
  182. // Mark the active tab for screen readers.
  183. $('#active-vertical-tab').remove();
  184. this.link.append(
  185. `<span id="active-vertical-tab" class="visually-hidden">${Drupal.t(
  186. '(active tab)',
  187. )}</span>`,
  188. );
  189. },
  190. /**
  191. * Updates the tab's summary.
  192. */
  193. updateSummary() {
  194. this.summary.html(this.details.drupalGetSummary());
  195. },
  196. /**
  197. * Shows a vertical tab pane.
  198. *
  199. * @return {Drupal.verticalTab}
  200. * The verticalTab instance.
  201. */
  202. tabShow() {
  203. // Display the tab.
  204. this.item.show();
  205. // Show the vertical tabs.
  206. this.item.closest('.js-form-type-vertical-tabs').show();
  207. // Update .first marker for items. We need recurse from parent to retain
  208. // the actual DOM element order as jQuery implements sortOrder, but not
  209. // as public method.
  210. this.item
  211. .parent()
  212. .children('.vertical-tabs__menu-item')
  213. .removeClass('first')
  214. .filter(':visible')
  215. .eq(0)
  216. .addClass('first');
  217. // Display the details element.
  218. this.details.removeClass('vertical-tab--hidden').show();
  219. // Focus this tab.
  220. this.focus();
  221. return this;
  222. },
  223. /**
  224. * Hides a vertical tab pane.
  225. *
  226. * @return {Drupal.verticalTab}
  227. * The verticalTab instance.
  228. */
  229. tabHide() {
  230. // Hide this tab.
  231. this.item.hide();
  232. // Update .first marker for items. We need recurse from parent to retain
  233. // the actual DOM element order as jQuery implements sortOrder, but not
  234. // as public method.
  235. this.item
  236. .parent()
  237. .children('.vertical-tabs__menu-item')
  238. .removeClass('first')
  239. .filter(':visible')
  240. .eq(0)
  241. .addClass('first');
  242. // Hide the details element.
  243. this.details.addClass('vertical-tab--hidden').hide();
  244. // Focus the first visible tab (if there is one).
  245. const $firstTab = this.details
  246. .siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)')
  247. .eq(0);
  248. if ($firstTab.length) {
  249. $firstTab.data('verticalTab').focus();
  250. }
  251. // Hide the vertical tabs (if no tabs remain).
  252. else {
  253. this.item.closest('.js-form-type-vertical-tabs').hide();
  254. }
  255. return this;
  256. },
  257. };
  258. /**
  259. * Theme function for a vertical tab.
  260. *
  261. * @param {object} settings
  262. * An object with the following keys:
  263. * @param {string} settings.title
  264. * The name of the tab.
  265. *
  266. * @return {object}
  267. * This function has to return an object with at least these keys:
  268. * - item: The root tab jQuery element
  269. * - link: The anchor tag that acts as the clickable area of the tab
  270. * (jQuery version)
  271. * - summary: The jQuery element that contains the tab summary
  272. */
  273. Drupal.theme.verticalTab = function(settings) {
  274. const tab = {};
  275. tab.item = $(
  276. '<li class="vertical-tabs__menu-item" tabindex="-1"></li>',
  277. ).append(
  278. (tab.link = $('<a href="#"></a>')
  279. .append(
  280. (tab.title = $(
  281. '<strong class="vertical-tabs__menu-item-title"></strong>',
  282. ).text(settings.title)),
  283. )
  284. .append(
  285. (tab.summary = $(
  286. '<span class="vertical-tabs__menu-item-summary"></span>',
  287. )),
  288. )),
  289. );
  290. return tab;
  291. };
  292. })(jQuery, Drupal, drupalSettings);