vertical-tabs.es6.js 8.7 KB

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