3 * Define vertical tabs functionality.
7 * Triggers when form values inside a vertical tab changes.
9 * This is used to update the summary in vertical tabs in order to know what
10 * are the important fields' values.
12 * @event summaryUpdated
15 (function ($, Drupal, drupalSettings) {
17 * Show the parent vertical tab pane of a targeted page fragment.
19 * In order to make sure a targeted element inside a vertical tab pane is
20 * visible on a hash change or fragment link click, show all parent panes.
22 * @param {jQuery.Event} e
23 * The event triggered.
24 * @param {jQuery} $target
25 * The targeted node as a jQuery object.
27 const handleFragmentLinkClickOrHashChange = (e, $target) => {
28 $target.parents('.vertical-tabs__pane').each((index, pane) => {
29 $(pane).data('verticalTab').focus();
34 * This script transforms a set of details into a stack of vertical tabs.
36 * Each tab may have a summary which can be updated by another
37 * script. For that to work, each details element has an associated
38 * 'verticalTabCallback' (with jQuery.data() attached to the details),
39 * which is called every time the user performs an update to a form
40 * element inside the tab pane.
42 * @type {Drupal~behavior}
44 * @prop {Drupal~behaviorAttach} attach
45 * Attaches behaviors for vertical tabs.
47 Drupal.behaviors.verticalTabs = {
49 const width = drupalSettings.widthBreakpoint || 640;
50 const mq = `(max-width: ${width}px)`;
52 if (window.matchMedia(mq).matches) {
57 * Binds a listener to handle fragment link clicks and URL hash changes.
59 $('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
61 $(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
62 const $this = $(this).addClass('vertical-tabs__panes');
63 const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
66 // Check if there are some details that can be converted to
68 const $details = $this.find('> details');
69 if ($details.length === 0) {
73 // Create the tab column.
74 const tab_list = $('<ul class="vertical-tabs__menu"></ul>');
75 $this.wrap('<div class="vertical-tabs clearfix"></div>').before(tab_list);
77 // Transform each details into a tab.
78 $details.each(function () {
79 const $that = $(this);
80 const vertical_tab = new Drupal.verticalTab({
81 title: $that.find('> summary').text(),
84 tab_list.append(vertical_tab.item);
86 .removeClass('collapsed')
87 // prop() can't be used on browsers not supporting details element,
88 // the style won't apply to them if prop() is used.
90 .addClass('vertical-tabs__pane')
91 .data('verticalTab', vertical_tab);
92 if (this.id === focusID) {
97 $(tab_list).find('> li').eq(0).addClass('first');
98 $(tab_list).find('> li').eq(-1).addClass('last');
101 // If the current URL has a fragment and one of the tabs contains an
102 // element that matches the URL fragment, activate that tab.
103 const $locationHash = $this.find(window.location.hash);
104 if (window.location.hash && $locationHash.length) {
105 tab_focus = $locationHash.closest('.vertical-tabs__pane');
108 tab_focus = $this.find('> .vertical-tabs__pane').eq(0);
111 if (tab_focus.length) {
112 tab_focus.data('verticalTab').focus();
119 * The vertical tab object represents a single tab within a tab group.
123 * @param {object} settings
125 * @param {string} settings.title
126 * The name of the tab.
127 * @param {jQuery} settings.details
128 * The jQuery object of the details element that is the tab pane.
130 * @fires event:summaryUpdated
132 * @listens event:summaryUpdated
134 Drupal.verticalTab = function (settings) {
136 $.extend(this, settings, Drupal.theme('verticalTab', settings));
138 this.link.attr('href', `#${settings.details.attr('id')}`);
140 this.link.on('click', (e) => {
145 // Keyboard events added:
146 // Pressing the Enter key will open the tab pane.
147 this.link.on('keydown', (event) => {
148 if (event.keyCode === 13) {
149 event.preventDefault();
151 // Set focus on the first input field of the visible details/tab pane.
152 $('.vertical-tabs__pane :input:visible:enabled').eq(0).trigger('focus');
157 .on('summaryUpdated', () => {
158 self.updateSummary();
160 .trigger('summaryUpdated');
163 Drupal.verticalTab.prototype = {
166 * Displays the tab's content pane.
170 .siblings('.vertical-tabs__pane')
172 const tab = $(this).data('verticalTab');
174 tab.item.removeClass('is-selected');
178 .siblings(':hidden.vertical-tabs__active-tab')
179 .val(this.details.attr('id'));
180 this.item.addClass('is-selected');
181 // Mark the active tab for screen readers.
182 $('#active-vertical-tab').remove();
183 this.link.append(`<span id="active-vertical-tab" class="visually-hidden">${Drupal.t('(active tab)')}</span>`);
187 * Updates the tab's summary.
190 this.summary.html(this.details.drupalGetSummary());
194 * Shows a vertical tab pane.
196 * @return {Drupal.verticalTab}
197 * The verticalTab instance.
202 // Show the vertical tabs.
203 this.item.closest('.js-form-type-vertical-tabs').show();
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
207 this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
208 .filter(':visible').eq(0).addClass('first');
209 // Display the details element.
210 this.details.removeClass('vertical-tab--hidden').show();
217 * Hides a vertical tab pane.
219 * @return {Drupal.verticalTab}
220 * The verticalTab instance.
225 // Update .first marker for items. We need recurse from parent to retain
226 // the actual DOM element order as jQuery implements sortOrder, but not
228 this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
229 .filter(':visible').eq(0).addClass('first');
230 // Hide the details element.
231 this.details.addClass('vertical-tab--hidden').hide();
232 // Focus the first visible tab (if there is one).
233 const $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0);
234 if ($firstTab.length) {
235 $firstTab.data('verticalTab').focus();
237 // Hide the vertical tabs (if no tabs remain).
239 this.item.closest('.js-form-type-vertical-tabs').hide();
246 * Theme function for a vertical tab.
248 * @param {object} settings
249 * An object with the following keys:
250 * @param {string} settings.title
251 * The name of the tab.
254 * This function has to return an object with at least these keys:
255 * - item: The root tab jQuery element
256 * - link: The anchor tag that acts as the clickable area of the tab
258 * - summary: The jQuery element that contains the tab summary
260 Drupal.theme.verticalTab = function (settings) {
262 tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>')
263 .append(tab.link = $('<a href="#"></a>')
264 .append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title))
265 .append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>'),
270 }(jQuery, Drupal, drupalSettings));