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) => {
36 * This script transforms a set of details into a stack of vertical tabs.
38 * Each tab may have a summary which can be updated by another
39 * script. For that to work, each details element has an associated
40 * 'verticalTabCallback' (with jQuery.data() attached to the details),
41 * which is called every time the user performs an update to a form
42 * element inside the tab pane.
44 * @type {Drupal~behavior}
46 * @prop {Drupal~behaviorAttach} attach
47 * Attaches behaviors for vertical tabs.
49 Drupal.behaviors.verticalTabs = {
51 const width = drupalSettings.widthBreakpoint || 640;
52 const mq = `(max-width: ${width}px)`;
54 if (window.matchMedia(mq).matches) {
59 * Binds a listener to handle fragment link clicks and URL hash changes.
62 .once('vertical-tabs-fragments')
64 'formFragmentLinkClickOrHashChange.verticalTabs',
65 handleFragmentLinkClickOrHashChange,
69 .find('[data-vertical-tabs-panes]')
70 .once('vertical-tabs')
72 const $this = $(this).addClass('vertical-tabs__panes');
73 const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
76 // Check if there are some details that can be converted to
78 const $details = $this.find('> details');
79 if ($details.length === 0) {
83 // Create the tab column.
84 const tabList = $('<ul class="vertical-tabs__menu"></ul>');
86 .wrap('<div class="vertical-tabs clearfix"></div>')
89 // Transform each details into a tab.
90 $details.each(function() {
91 const $that = $(this);
92 const verticalTab = new Drupal.verticalTab({
93 title: $that.find('> summary').text(),
96 tabList.append(verticalTab.item);
98 .removeClass('collapsed')
99 // prop() can't be used on browsers not supporting details element,
100 // the style won't apply to them if prop() is used.
102 .addClass('vertical-tabs__pane')
103 .data('verticalTab', verticalTab);
104 if (this.id === focusID) {
119 // If the current URL has a fragment and one of the tabs contains an
120 // element that matches the URL fragment, activate that tab.
121 const $locationHash = $this.find(window.location.hash);
122 if (window.location.hash && $locationHash.length) {
123 tabFocus = $locationHash.closest('.vertical-tabs__pane');
125 tabFocus = $this.find('> .vertical-tabs__pane').eq(0);
128 if (tabFocus.length) {
129 tabFocus.data('verticalTab').focus();
136 * The vertical tab object represents a single tab within a tab group.
140 * @param {object} settings
142 * @param {string} settings.title
143 * The name of the tab.
144 * @param {jQuery} settings.details
145 * The jQuery object of the details element that is the tab pane.
147 * @fires event:summaryUpdated
149 * @listens event:summaryUpdated
151 Drupal.verticalTab = function(settings) {
153 $.extend(this, settings, Drupal.theme('verticalTab', settings));
155 this.link.attr('href', `#${settings.details.attr('id')}`);
157 this.link.on('click', e => {
162 // Keyboard events added:
163 // Pressing the Enter key will open the tab pane.
164 this.link.on('keydown', event => {
165 if (event.keyCode === 13) {
166 event.preventDefault();
168 // Set focus on the first input field of the visible details/tab pane.
169 $('.vertical-tabs__pane :input:visible:enabled')
176 .on('summaryUpdated', () => {
177 self.updateSummary();
179 .trigger('summaryUpdated');
182 Drupal.verticalTab.prototype = {
184 * Displays the tab's content pane.
188 .siblings('.vertical-tabs__pane')
190 const tab = $(this).data('verticalTab');
192 tab.item.removeClass('is-selected');
196 .siblings(':hidden.vertical-tabs__active-tab')
197 .val(this.details.attr('id'));
198 this.item.addClass('is-selected');
199 // Mark the active tab for screen readers.
200 $('#active-vertical-tab').remove();
202 `<span id="active-vertical-tab" class="visually-hidden">${Drupal.t(
209 * Updates the tab's summary.
212 this.summary.html(this.details.drupalGetSummary());
216 * Shows a vertical tab pane.
218 * @return {Drupal.verticalTab}
219 * The verticalTab instance.
224 // Show the vertical tabs.
225 this.item.closest('.js-form-type-vertical-tabs').show();
226 // Update .first marker for items. We need recurse from parent to retain
227 // the actual DOM element order as jQuery implements sortOrder, but not
231 .children('.vertical-tabs__menu-item')
232 .removeClass('first')
236 // Display the details element.
237 this.details.removeClass('vertical-tab--hidden').show();
244 * Hides a vertical tab pane.
246 * @return {Drupal.verticalTab}
247 * The verticalTab instance.
252 // Update .first marker for items. We need recurse from parent to retain
253 // the actual DOM element order as jQuery implements sortOrder, but not
257 .children('.vertical-tabs__menu-item')
258 .removeClass('first')
262 // Hide the details element.
263 this.details.addClass('vertical-tab--hidden').hide();
264 // Focus the first visible tab (if there is one).
265 const $firstTab = this.details
266 .siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)')
268 if ($firstTab.length) {
269 $firstTab.data('verticalTab').focus();
271 // Hide the vertical tabs (if no tabs remain).
273 this.item.closest('.js-form-type-vertical-tabs').hide();
280 * Theme function for a vertical tab.
282 * @param {object} settings
283 * An object with the following keys:
284 * @param {string} settings.title
285 * The name of the tab.
288 * This function has to return an object with at least these keys:
289 * - item: The root tab jQuery element
290 * - link: The anchor tag that acts as the clickable area of the tab
292 * - summary: The jQuery element that contains the tab summary
294 Drupal.theme.verticalTab = function(settings) {
297 '<li class="vertical-tabs__menu-item" tabindex="-1"></li>',
299 (tab.link = $('<a href="#"></a>')
302 '<strong class="vertical-tabs__menu-item-title"></strong>',
303 ).text(settings.title)),
307 '<span class="vertical-tabs__menu-item-summary"></span>',
313 })(jQuery, Drupal, drupalSettings);