vertical-tabs.js 7.9 KB

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