Path: blob/main/src/resources/formats/html/tabby/js/tabby.js
12923 views
(function (root, factory) {1if (typeof define === "function" && define.amd) {2define([], function () {3return factory(root);4});5} else if (typeof exports === "object") {6module.exports = factory(root);7} else {8root.Tabby = factory(root);9}10})(11typeof global !== "undefined"12? global13: typeof window !== "undefined"14? window15: this,16function (window) {17"use strict";1819//20// Variables21//2223var defaults = {24idPrefix: "tabby-toggle_",25default: "[data-tabby-default]",26};2728//29// Methods30//3132/**33* Merge two or more objects together.34* @param {Object} objects The objects to merge together35* @returns {Object} Merged values of defaults and options36*/37var extend = function () {38var merged = {};39Array.prototype.forEach.call(arguments, function (obj) {40for (var key in obj) {41if (!obj.hasOwnProperty(key)) return;42merged[key] = obj[key];43}44});45return merged;46};4748/**49* Emit a custom event50* @param {String} type The event type51* @param {Node} tab The tab to attach the event to52* @param {Node} details Details about the event53*/54var emitEvent = function (tab, details) {55// Create a new event56var event;57if (typeof window.CustomEvent === "function") {58event = new CustomEvent("tabby", {59bubbles: true,60cancelable: true,61detail: details,62});63} else {64event = document.createEvent("CustomEvent");65event.initCustomEvent("tabby", true, true, details);66}6768// Dispatch the event69tab.dispatchEvent(event);70};7172var focusHandler = function (event) {73toggle(event.target);74};7576var getKeyboardFocusableElements = function (element) {77return [78...element.querySelectorAll(79'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'80),81].filter(82(el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden")83);84};8586/**87* Remove roles and attributes from a tab and its content88* @param {Node} tab The tab89* @param {Node} content The tab content90* @param {Object} settings User settings and options91*/92var destroyTab = function (tab, content, settings) {93// Remove the generated ID94if (tab.id.slice(0, settings.idPrefix.length) === settings.idPrefix) {95tab.id = "";96}9798// remove event listener99tab.removeEventListener("focus", focusHandler, true);100101// Remove roles102tab.removeAttribute("role");103tab.removeAttribute("aria-controls");104tab.removeAttribute("aria-selected");105tab.removeAttribute("tabindex");106tab.closest("li").removeAttribute("role");107content.removeAttribute("role");108content.removeAttribute("aria-labelledby");109content.removeAttribute("hidden");110};111112/**113* Add the required roles and attributes to a tab and its content114* @param {Node} tab The tab115* @param {Node} content The tab content116* @param {Object} settings User settings and options117*/118var setupTab = function (tab, content, settings) {119// Give tab an ID if it doesn't already have one120if (!tab.id) {121tab.id = settings.idPrefix + content.id;122}123124// Add roles125tab.setAttribute("role", "tab");126tab.setAttribute("aria-controls", content.id);127tab.closest("li").setAttribute("role", "presentation");128content.setAttribute("role", "tabpanel");129content.setAttribute("aria-labelledby", tab.id);130131// Add selected state132if (tab.matches(settings.default)) {133tab.setAttribute("aria-selected", "true");134} else {135tab.setAttribute("aria-selected", "false");136content.setAttribute("hidden", "hidden");137}138139// add focus event listender140tab.addEventListener("focus", focusHandler);141};142143/**144* Hide a tab and its content145* @param {Node} newTab The new tab that's replacing it146*/147var hide = function (newTab) {148// Variables149var tabGroup = newTab.closest('[role="tablist"]');150if (!tabGroup) return {};151var tab = tabGroup.querySelector('[role="tab"][aria-selected="true"]');152if (!tab) return {};153var content = document.querySelector(tab.hash);154155// Hide the tab156tab.setAttribute("aria-selected", "false");157158// Hide the content159if (!content) return { previousTab: tab };160content.setAttribute("hidden", "hidden");161162// Return the hidden tab and content163return {164previousTab: tab,165previousContent: content,166};167};168169/**170* Show a tab and its content171* @param {Node} tab The tab172* @param {Node} content The tab content173*/174var show = function (tab, content) {175tab.setAttribute("aria-selected", "true");176content.removeAttribute("hidden");177tab.focus();178};179180/**181* Toggle a new tab182* @param {Node} tab The tab to show183*/184var toggle = function (tab) {185// Make sure there's a tab to toggle and it's not already active186if (!tab || tab.getAttribute("aria-selected") == "true") return;187188// Variables189var content = document.querySelector(tab.hash);190if (!content) return;191192// Hide active tab and content193var details = hide(tab);194195// Show new tab and content196show(tab, content);197198// Add event details199details.tab = tab;200details.content = content;201202// Emit a custom event203emitEvent(tab, details);204};205206/**207* Get all of the tabs in a tablist208* @param {Node} tab A tab from the list209* @return {Object} The tabs and the index of the currently active one210*/211var getTabsMap = function (tab) {212var tabGroup = tab.closest('[role="tablist"]');213var tabs = tabGroup ? tabGroup.querySelectorAll('[role="tab"]') : null;214if (!tabs) return;215return {216tabs: tabs,217index: Array.prototype.indexOf.call(tabs, tab),218};219};220221/**222* Switch the active tab based on keyboard activity223* @param {Node} tab The currently active tab224* @param {Key} key The key that was pressed225*/226var switchTabs = function (tab, key) {227// Get a map of tabs228var map = getTabsMap(tab);229if (!map) return;230var length = map.tabs.length - 1;231var index;232233// Go to previous tab234if (["ArrowUp", "ArrowLeft", "Up", "Left"].indexOf(key) > -1) {235index = map.index < 1 ? length : map.index - 1;236}237238// Go to next tab239else if (["ArrowDown", "ArrowRight", "Down", "Right"].indexOf(key) > -1) {240index = map.index === length ? 0 : map.index + 1;241}242243// Go to home244else if (key === "Home") {245index = 0;246}247248// Go to end249else if (key === "End") {250index = length;251}252253// Toggle the tab254toggle(map.tabs[index]);255};256257/**258* Create the Constructor object259*/260var Constructor = function (selector, options) {261//262// Variables263//264265var publicAPIs = {};266var settings, tabWrapper;267268//269// Methods270//271272publicAPIs.destroy = function () {273// Get all tabs274var tabs = tabWrapper.querySelectorAll("a");275276// Add roles to tabs277Array.prototype.forEach.call(tabs, function (tab) {278// Get the tab content279var content = document.querySelector(tab.hash);280if (!content) return;281282// Setup the tab283destroyTab(tab, content, settings);284});285286// Remove role from wrapper287tabWrapper.removeAttribute("role");288289// Remove event listeners290document.documentElement.removeEventListener(291"click",292clickHandler,293true294);295tabWrapper.removeEventListener("keydown", keyHandler, true);296297// Reset variables298settings = null;299tabWrapper = null;300};301302/**303* Setup the DOM with the proper attributes304*/305publicAPIs.setup = function () {306// Variables307tabWrapper = document.querySelector(selector);308if (!tabWrapper) return;309var tabs = tabWrapper.querySelectorAll("a");310311// Add role to wrapper312tabWrapper.setAttribute("role", "tablist");313314// Add roles to tabs. provide dynanmic tab indexes if we are within reveal315var contentTabindexes =316window.document.body.classList.contains("reveal-viewport");317var nextTabindex = 1;318Array.prototype.forEach.call(tabs, function (tab) {319if (contentTabindexes) {320tab.setAttribute("tabindex", "" + nextTabindex++);321} else {322tab.setAttribute("tabindex", "0");323}324325// Get the tab content326var content = document.querySelector(tab.hash);327if (!content) return;328329// set tab indexes for content330if (contentTabindexes) {331getKeyboardFocusableElements(content).forEach(function (el) {332el.setAttribute("tabindex", "" + nextTabindex++);333});334}335336// Setup the tab337setupTab(tab, content, settings);338});339};340341/**342* Toggle a tab based on an ID343* @param {String|Node} id The tab to toggle344*/345publicAPIs.toggle = function (id) {346// Get the tab347var tab = id;348if (typeof id === "string") {349tab = document.querySelector(350selector + ' [role="tab"][href*="' + id + '"]'351);352}353354// Toggle the tab355toggle(tab);356};357358/**359* Handle click events360*/361var clickHandler = function (event) {362// Only run on toggles363var tab = event.target.closest(selector + ' [role="tab"]');364if (!tab) return;365366// Prevent link behavior367event.preventDefault();368369// Toggle the tab370toggle(tab);371};372373/**374* Handle keydown events375*/376var keyHandler = function (event) {377// Only run if a tab is in focus378var tab = document.activeElement;379if (!tab.matches(selector + ' [role="tab"]')) return;380381// Only run for specific keys382if (["Home", "End"].indexOf(event.key) < 0) return;383384// Switch tabs385switchTabs(tab, event.key);386};387388/**389* Initialize the instance390*/391var init = function () {392// Merge user options with defaults393settings = extend(defaults, options || {});394395// Setup the DOM396publicAPIs.setup();397398// Add event listeners399document.documentElement.addEventListener("click", clickHandler, true);400tabWrapper.addEventListener("keydown", keyHandler, true);401};402403//404// Initialize and return the Public APIs405//406407init();408return publicAPIs;409};410411//412// Return the Constructor413//414415return Constructor;416}417);418419420