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