vertical-tabs.es6.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 tab_focus;
  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 tab_list = $('<ul class="vertical-tabs__menu"></ul>');
  67. $this.wrap('<div class="vertical-tabs clearfix"></div>').before(tab_list);
  68. // Transform each details into a tab.
  69. $details.each(function () {
  70. const $that = $(this);
  71. const vertical_tab = new Drupal.verticalTab({
  72. title: $that.find('> summary').text(),
  73. details: $that,
  74. });
  75. tab_list.append(vertical_tab.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', vertical_tab);
  83. if (this.id === focusID) {
  84. tab_focus = $that;
  85. }
  86. });
  87. $(tab_list).find('> li').eq(0).addClass('first');
  88. $(tab_list).find('> li').eq(-1).addClass('last');
  89. if (!tab_focus) {
  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. tab_focus = $locationHash.closest('.vertical-tabs__pane');
  95. }
  96. else {
  97. tab_focus = $this.find('> .vertical-tabs__pane').eq(0);
  98. }
  99. }
  100. if (tab_focus.length) {
  101. tab_focus.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.parent().children('.vertical-tabs__menu-item').removeClass('first')
  188. .filter(':visible').eq(0).addClass('first');
  189. // Display the details element.
  190. this.details.removeClass('vertical-tab--hidden').show();
  191. // Focus this tab.
  192. this.focus();
  193. return this;
  194. },
  195. /**
  196. * Hides a vertical tab pane.
  197. *
  198. * @return {Drupal.verticalTab}
  199. * The verticalTab instance.
  200. */
  201. tabHide() {
  202. // Hide this tab.
  203. this.item.hide();
  204. // Update .first marker for items. We need recurse from parent to retain
  205. // the actual DOM element order as jQuery implements sortOrder, but not
  206. // as public method.
  207. this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
  208. .filter(':visible').eq(0).addClass('first');
  209. // Hide the details element.
  210. this.details.addClass('vertical-tab--hidden').hide();
  211. // Focus the first visible tab (if there is one).
  212. const $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0);
  213. if ($firstTab.length) {
  214. $firstTab.data('verticalTab').focus();
  215. }
  216. // Hide the vertical tabs (if no tabs remain).
  217. else {
  218. this.item.closest('.js-form-type-vertical-tabs').hide();
  219. }
  220. return this;
  221. },
  222. };
  223. /**
  224. * Theme function for a vertical tab.
  225. *
  226. * @param {object} settings
  227. * An object with the following keys:
  228. * @param {string} settings.title
  229. * The name of the tab.
  230. *
  231. * @return {object}
  232. * This function has to return an object with at least these keys:
  233. * - item: The root tab jQuery element
  234. * - link: The anchor tag that acts as the clickable area of the tab
  235. * (jQuery version)
  236. * - summary: The jQuery element that contains the tab summary
  237. */
  238. Drupal.theme.verticalTab = function (settings) {
  239. const tab = {};
  240. tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>')
  241. .append(tab.link = $('<a href="#"></a>')
  242. .append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title))
  243. .append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>'),
  244. ),
  245. );
  246. return tab;
  247. };
  248. }(jQuery, Drupal, drupalSettings));