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@4: (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@4: $(pane) Chris@4: .data('verticalTab') Chris@4: .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@4: $('body') Chris@4: .once('vertical-tabs-fragments') Chris@4: .on( Chris@4: 'formFragmentLinkClickOrHashChange.verticalTabs', Chris@4: handleFragmentLinkClickOrHashChange, Chris@4: ); Chris@0: Chris@4: $(context) Chris@4: .find('[data-vertical-tabs-panes]') Chris@4: .once('vertical-tabs') Chris@4: .each(function() { Chris@4: const $this = $(this).addClass('vertical-tabs__panes'); Chris@4: const focusID = $this.find(':hidden.vertical-tabs__active-tab').val(); Chris@4: let tabFocus; Chris@0: Chris@4: // Check if there are some details that can be converted to Chris@4: // vertical-tabs. Chris@4: const $details = $this.find('> details'); Chris@4: if ($details.length === 0) { Chris@4: return; Chris@4: } Chris@0: Chris@4: // Create the tab column. Chris@4: const tabList = $(''); Chris@4: $this Chris@4: .wrap('
') Chris@4: .before(tabList); Chris@0: Chris@4: // Transform each details into a tab. Chris@4: $details.each(function() { Chris@4: const $that = $(this); Chris@4: const verticalTab = new Drupal.verticalTab({ Chris@4: title: $that.find('> summary').text(), Chris@4: details: $that, Chris@4: }); Chris@4: tabList.append(verticalTab.item); Chris@4: $that Chris@4: .removeClass('collapsed') Chris@4: // prop() can't be used on browsers not supporting details element, Chris@4: // the style won't apply to them if prop() is used. Chris@4: .attr('open', true) Chris@4: .addClass('vertical-tabs__pane') Chris@4: .data('verticalTab', verticalTab); Chris@4: if (this.id === focusID) { Chris@4: tabFocus = $that; Chris@4: } Chris@0: }); Chris@4: Chris@4: $(tabList) Chris@4: .find('> li') Chris@4: .eq(0) Chris@4: .addClass('first'); Chris@4: $(tabList) Chris@4: .find('> li') Chris@4: .eq(-1) Chris@4: .addClass('last'); Chris@4: Chris@4: if (!tabFocus) { Chris@4: // If the current URL has a fragment and one of the tabs contains an Chris@4: // element that matches the URL fragment, activate that tab. Chris@4: const $locationHash = $this.find(window.location.hash); Chris@4: if (window.location.hash && $locationHash.length) { Chris@4: tabFocus = $locationHash.closest('.vertical-tabs__pane'); Chris@4: } else { Chris@4: tabFocus = $this.find('> .vertical-tabs__pane').eq(0); Chris@4: } Chris@4: } Chris@4: if (tabFocus.length) { Chris@4: 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@4: 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@4: 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@4: 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@4: $('.vertical-tabs__pane :input:visible:enabled') Chris@4: .eq(0) Chris@4: .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: * Displays the tab's content pane. Chris@0: */ Chris@0: focus() { Chris@0: this.details Chris@0: .siblings('.vertical-tabs__pane') Chris@4: .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@4: this.link.append( Chris@4: `${Drupal.t( Chris@4: '(active tab)', Chris@4: )}`, Chris@4: ); 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@4: const $firstTab = this.details Chris@4: .siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)') Chris@4: .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@4: Drupal.theme.verticalTab = function(settings) { Chris@0: const tab = {}; Chris@4: tab.item = $( Chris@4: '
  • ', Chris@4: ).append( Chris@4: (tab.link = $('') Chris@4: .append( Chris@4: (tab.title = $( Chris@4: '', Chris@4: ).text(settings.title)), Chris@4: ) Chris@4: .append( Chris@4: (tab.summary = $( Chris@4: '', Chris@4: )), Chris@4: )), Chris@4: ); Chris@0: return tab; Chris@0: }; Chris@4: })(jQuery, Drupal, drupalSettings);