Chris@0: /** Chris@0: * @file Chris@0: * Define vertical tabs functionality. Chris@0: */ Chris@0: Chris@0: /** Chris@0: * Triggers when form values inside a vertical tab changes. Chris@0: * Chris@0: * This is used to update the summary in vertical tabs in order to know what Chris@0: * are the important fields' values. Chris@0: * Chris@0: * @event summaryUpdated Chris@0: */ Chris@0: Chris@0: (function ($, Drupal, drupalSettings) { Chris@0: /** Chris@0: * Show the parent vertical tab pane of a targeted page fragment. Chris@0: * Chris@0: * In order to make sure a targeted element inside a vertical tab pane is Chris@0: * visible on a hash change or fragment link click, show all parent panes. Chris@0: * Chris@0: * @param {jQuery.Event} e Chris@0: * The event triggered. Chris@0: * @param {jQuery} $target Chris@0: * The targeted node as a jQuery object. Chris@0: */ Chris@0: const handleFragmentLinkClickOrHashChange = (e, $target) => { Chris@0: $target.parents('.vertical-tabs__pane').each((index, pane) => { Chris@0: $(pane).data('verticalTab').focus(); Chris@0: }); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * This script transforms a set of details into a stack of vertical tabs. Chris@0: * Chris@0: * Each tab may have a summary which can be updated by another Chris@0: * script. For that to work, each details element has an associated Chris@0: * 'verticalTabCallback' (with jQuery.data() attached to the details), Chris@0: * which is called every time the user performs an update to a form Chris@0: * element inside the tab pane. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches behaviors for vertical tabs. Chris@0: */ Chris@0: Drupal.behaviors.verticalTabs = { Chris@0: attach(context) { Chris@0: const width = drupalSettings.widthBreakpoint || 640; Chris@0: const mq = `(max-width: ${width}px)`; Chris@0: Chris@0: if (window.matchMedia(mq).matches) { Chris@0: return; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Binds a listener to handle fragment link clicks and URL hash changes. Chris@0: */ Chris@0: $('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange); Chris@0: Chris@0: $(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () { Chris@0: const $this = $(this).addClass('vertical-tabs__panes'); Chris@0: const focusID = $this.find(':hidden.vertical-tabs__active-tab').val(); Chris@0: let tabFocus; Chris@0: Chris@0: // Check if there are some details that can be converted to Chris@0: // vertical-tabs. Chris@0: const $details = $this.find('> details'); Chris@0: if ($details.length === 0) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Create the tab column. Chris@0: const tabList = $(''); Chris@0: $this.wrap('
').before(tabList); Chris@0: Chris@0: // Transform each details into a tab. Chris@0: $details.each(function () { Chris@0: const $that = $(this); Chris@0: const verticalTab = new Drupal.verticalTab({ Chris@0: title: $that.find('> summary').text(), Chris@0: details: $that, Chris@0: }); Chris@0: tabList.append(verticalTab.item); Chris@0: $that Chris@0: .removeClass('collapsed') Chris@0: // prop() can't be used on browsers not supporting details element, Chris@0: // the style won't apply to them if prop() is used. Chris@0: .attr('open', true) Chris@0: .addClass('vertical-tabs__pane') Chris@0: .data('verticalTab', verticalTab); Chris@0: if (this.id === focusID) { Chris@0: tabFocus = $that; Chris@0: } Chris@0: }); Chris@0: Chris@0: $(tabList).find('> li').eq(0).addClass('first'); Chris@0: $(tabList).find('> li').eq(-1).addClass('last'); Chris@0: Chris@0: if (!tabFocus) { Chris@0: // If the current URL has a fragment and one of the tabs contains an Chris@0: // element that matches the URL fragment, activate that tab. Chris@0: const $locationHash = $this.find(window.location.hash); Chris@0: if (window.location.hash && $locationHash.length) { Chris@0: tabFocus = $locationHash.closest('.vertical-tabs__pane'); Chris@0: } Chris@0: else { Chris@0: tabFocus = $this.find('> .vertical-tabs__pane').eq(0); Chris@0: } Chris@0: } Chris@0: if (tabFocus.length) { Chris@0: tabFocus.data('verticalTab').focus(); Chris@0: } Chris@0: }); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * The vertical tab object represents a single tab within a tab group. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {object} settings Chris@0: * Settings object. Chris@0: * @param {string} settings.title Chris@0: * The name of the tab. Chris@0: * @param {jQuery} settings.details Chris@0: * The jQuery object of the details element that is the tab pane. Chris@0: * Chris@0: * @fires event:summaryUpdated Chris@0: * Chris@0: * @listens event:summaryUpdated Chris@0: */ Chris@0: Drupal.verticalTab = function (settings) { Chris@0: const self = this; Chris@0: $.extend(this, settings, Drupal.theme('verticalTab', settings)); Chris@0: Chris@0: this.link.attr('href', `#${settings.details.attr('id')}`); Chris@0: Chris@0: this.link.on('click', (e) => { Chris@0: e.preventDefault(); Chris@0: self.focus(); Chris@0: }); Chris@0: Chris@0: // Keyboard events added: Chris@0: // Pressing the Enter key will open the tab pane. Chris@0: this.link.on('keydown', (event) => { Chris@0: if (event.keyCode === 13) { Chris@0: event.preventDefault(); Chris@0: self.focus(); Chris@0: // Set focus on the first input field of the visible details/tab pane. Chris@0: $('.vertical-tabs__pane :input:visible:enabled').eq(0).trigger('focus'); Chris@0: } Chris@0: }); Chris@0: Chris@0: this.details Chris@0: .on('summaryUpdated', () => { Chris@0: self.updateSummary(); Chris@0: }) Chris@0: .trigger('summaryUpdated'); Chris@0: }; Chris@0: Chris@0: Drupal.verticalTab.prototype = { Chris@0: Chris@0: /** Chris@0: * Displays the tab's content pane. Chris@0: */ Chris@0: focus() { Chris@0: this.details Chris@0: .siblings('.vertical-tabs__pane') Chris@0: .each(function () { Chris@0: const tab = $(this).data('verticalTab'); Chris@0: tab.details.hide(); Chris@0: tab.item.removeClass('is-selected'); Chris@0: }) Chris@0: .end() Chris@0: .show() Chris@0: .siblings(':hidden.vertical-tabs__active-tab') Chris@0: .val(this.details.attr('id')); Chris@0: this.item.addClass('is-selected'); Chris@0: // Mark the active tab for screen readers. Chris@0: $('#active-vertical-tab').remove(); Chris@0: this.link.append(`${Drupal.t('(active tab)')}`); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Updates the tab's summary. Chris@0: */ Chris@0: updateSummary() { Chris@0: this.summary.html(this.details.drupalGetSummary()); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Shows a vertical tab pane. Chris@0: * Chris@0: * @return {Drupal.verticalTab} Chris@0: * The verticalTab instance. Chris@0: */ Chris@0: tabShow() { Chris@0: // Display the tab. Chris@0: this.item.show(); Chris@0: // Show the vertical tabs. Chris@0: this.item.closest('.js-form-type-vertical-tabs').show(); Chris@0: // Update .first marker for items. We need recurse from parent to retain Chris@0: // the actual DOM element order as jQuery implements sortOrder, but not Chris@0: // as public method. Chris@0: this.item Chris@0: .parent() Chris@0: .children('.vertical-tabs__menu-item') Chris@0: .removeClass('first') Chris@0: .filter(':visible') Chris@0: .eq(0) Chris@0: .addClass('first'); Chris@0: // Display the details element. Chris@0: this.details.removeClass('vertical-tab--hidden').show(); Chris@0: // Focus this tab. Chris@0: this.focus(); Chris@0: return this; Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Hides a vertical tab pane. Chris@0: * Chris@0: * @return {Drupal.verticalTab} Chris@0: * The verticalTab instance. Chris@0: */ Chris@0: tabHide() { Chris@0: // Hide this tab. Chris@0: this.item.hide(); Chris@0: // Update .first marker for items. We need recurse from parent to retain Chris@0: // the actual DOM element order as jQuery implements sortOrder, but not Chris@0: // as public method. Chris@0: this.item Chris@0: .parent() Chris@0: .children('.vertical-tabs__menu-item') Chris@0: .removeClass('first') Chris@0: .filter(':visible') Chris@0: .eq(0) Chris@0: .addClass('first'); Chris@0: // Hide the details element. Chris@0: this.details.addClass('vertical-tab--hidden').hide(); Chris@0: // Focus the first visible tab (if there is one). Chris@0: const $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0); Chris@0: if ($firstTab.length) { Chris@0: $firstTab.data('verticalTab').focus(); Chris@0: } Chris@0: // Hide the vertical tabs (if no tabs remain). Chris@0: else { Chris@0: this.item.closest('.js-form-type-vertical-tabs').hide(); Chris@0: } Chris@0: return this; Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Theme function for a vertical tab. Chris@0: * Chris@0: * @param {object} settings Chris@0: * An object with the following keys: Chris@0: * @param {string} settings.title Chris@0: * The name of the tab. Chris@0: * Chris@0: * @return {object} Chris@0: * This function has to return an object with at least these keys: Chris@0: * - item: The root tab jQuery element Chris@0: * - link: The anchor tag that acts as the clickable area of the tab Chris@0: * (jQuery version) Chris@0: * - summary: The jQuery element that contains the tab summary Chris@0: */ Chris@0: Drupal.theme.verticalTab = function (settings) { Chris@0: const tab = {}; Chris@0: tab.item = $('
  • ') Chris@0: .append(tab.link = $('') Chris@0: .append(tab.title = $('').text(settings.title)) Chris@0: .append(tab.summary = $(''), Chris@0: ), Chris@0: ); Chris@0: return tab; Chris@0: }; Chris@0: }(jQuery, Drupal, drupalSettings));