Spaces:
Running
Running
| (function (root, factory) { | |
| if (typeof define === "function" && define.amd) { | |
| define([], function () { | |
| return factory(root); | |
| }); | |
| } else if (typeof exports === "object") { | |
| module.exports = factory(root); | |
| } else { | |
| root.Tabby = factory(root); | |
| } | |
| })( | |
| typeof global !== "undefined" | |
| ? global | |
| : typeof window !== "undefined" | |
| ? window | |
| : this, | |
| function (window) { | |
| "use strict"; | |
| // | |
| // Variables | |
| // | |
| var defaults = { | |
| idPrefix: "tabby-toggle_", | |
| default: "[data-tabby-default]", | |
| }; | |
| // | |
| // Methods | |
| // | |
| /** | |
| * Merge two or more objects together. | |
| * @param {Object} objects The objects to merge together | |
| * @returns {Object} Merged values of defaults and options | |
| */ | |
| var extend = function () { | |
| var merged = {}; | |
| Array.prototype.forEach.call(arguments, function (obj) { | |
| for (var key in obj) { | |
| if (!obj.hasOwnProperty(key)) return; | |
| merged[key] = obj[key]; | |
| } | |
| }); | |
| return merged; | |
| }; | |
| /** | |
| * Emit a custom event | |
| * @param {String} type The event type | |
| * @param {Node} tab The tab to attach the event to | |
| * @param {Node} details Details about the event | |
| */ | |
| var emitEvent = function (tab, details) { | |
| // Create a new event | |
| var event; | |
| if (typeof window.CustomEvent === "function") { | |
| event = new CustomEvent("tabby", { | |
| bubbles: true, | |
| cancelable: true, | |
| detail: details, | |
| }); | |
| } else { | |
| event = document.createEvent("CustomEvent"); | |
| event.initCustomEvent("tabby", true, true, details); | |
| } | |
| // Dispatch the event | |
| tab.dispatchEvent(event); | |
| }; | |
| var focusHandler = function (event) { | |
| toggle(event.target); | |
| }; | |
| var getKeyboardFocusableElements = function (element) { | |
| return [ | |
| ...element.querySelectorAll( | |
| 'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])' | |
| ), | |
| ].filter( | |
| (el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden") | |
| ); | |
| }; | |
| /** | |
| * Remove roles and attributes from a tab and its content | |
| * @param {Node} tab The tab | |
| * @param {Node} content The tab content | |
| * @param {Object} settings User settings and options | |
| */ | |
| var destroyTab = function (tab, content, settings) { | |
| // Remove the generated ID | |
| if (tab.id.slice(0, settings.idPrefix.length) === settings.idPrefix) { | |
| tab.id = ""; | |
| } | |
| // remove event listener | |
| tab.removeEventListener("focus", focusHandler, true); | |
| // Remove roles | |
| tab.removeAttribute("role"); | |
| tab.removeAttribute("aria-controls"); | |
| tab.removeAttribute("aria-selected"); | |
| tab.removeAttribute("tabindex"); | |
| tab.closest("li").removeAttribute("role"); | |
| content.removeAttribute("role"); | |
| content.removeAttribute("aria-labelledby"); | |
| content.removeAttribute("hidden"); | |
| }; | |
| /** | |
| * Add the required roles and attributes to a tab and its content | |
| * @param {Node} tab The tab | |
| * @param {Node} content The tab content | |
| * @param {Object} settings User settings and options | |
| */ | |
| var setupTab = function (tab, content, settings) { | |
| // Give tab an ID if it doesn't already have one | |
| if (!tab.id) { | |
| tab.id = settings.idPrefix + content.id; | |
| } | |
| // Add roles | |
| tab.setAttribute("role", "tab"); | |
| tab.setAttribute("aria-controls", content.id); | |
| tab.closest("li").setAttribute("role", "presentation"); | |
| content.setAttribute("role", "tabpanel"); | |
| content.setAttribute("aria-labelledby", tab.id); | |
| // Add selected state | |
| if (tab.matches(settings.default)) { | |
| tab.setAttribute("aria-selected", "true"); | |
| } else { | |
| tab.setAttribute("aria-selected", "false"); | |
| content.setAttribute("hidden", "hidden"); | |
| } | |
| // add focus event listender | |
| tab.addEventListener("focus", focusHandler); | |
| }; | |
| /** | |
| * Hide a tab and its content | |
| * @param {Node} newTab The new tab that's replacing it | |
| */ | |
| var hide = function (newTab) { | |
| // Variables | |
| var tabGroup = newTab.closest('[role="tablist"]'); | |
| if (!tabGroup) return {}; | |
| var tab = tabGroup.querySelector('[role="tab"][aria-selected="true"]'); | |
| if (!tab) return {}; | |
| var content = document.querySelector(tab.hash); | |
| // Hide the tab | |
| tab.setAttribute("aria-selected", "false"); | |
| // Hide the content | |
| if (!content) return { previousTab: tab }; | |
| content.setAttribute("hidden", "hidden"); | |
| // Return the hidden tab and content | |
| return { | |
| previousTab: tab, | |
| previousContent: content, | |
| }; | |
| }; | |
| /** | |
| * Show a tab and its content | |
| * @param {Node} tab The tab | |
| * @param {Node} content The tab content | |
| */ | |
| var show = function (tab, content) { | |
| tab.setAttribute("aria-selected", "true"); | |
| content.removeAttribute("hidden"); | |
| tab.focus(); | |
| }; | |
| /** | |
| * Toggle a new tab | |
| * @param {Node} tab The tab to show | |
| */ | |
| var toggle = function (tab) { | |
| // Make sure there's a tab to toggle and it's not already active | |
| if (!tab || tab.getAttribute("aria-selected") == "true") return; | |
| // Variables | |
| var content = document.querySelector(tab.hash); | |
| if (!content) return; | |
| // Hide active tab and content | |
| var details = hide(tab); | |
| // Show new tab and content | |
| show(tab, content); | |
| // Add event details | |
| details.tab = tab; | |
| details.content = content; | |
| // Emit a custom event | |
| emitEvent(tab, details); | |
| }; | |
| /** | |
| * Get all of the tabs in a tablist | |
| * @param {Node} tab A tab from the list | |
| * @return {Object} The tabs and the index of the currently active one | |
| */ | |
| var getTabsMap = function (tab) { | |
| var tabGroup = tab.closest('[role="tablist"]'); | |
| var tabs = tabGroup ? tabGroup.querySelectorAll('[role="tab"]') : null; | |
| if (!tabs) return; | |
| return { | |
| tabs: tabs, | |
| index: Array.prototype.indexOf.call(tabs, tab), | |
| }; | |
| }; | |
| /** | |
| * Switch the active tab based on keyboard activity | |
| * @param {Node} tab The currently active tab | |
| * @param {Key} key The key that was pressed | |
| */ | |
| var switchTabs = function (tab, key) { | |
| // Get a map of tabs | |
| var map = getTabsMap(tab); | |
| if (!map) return; | |
| var length = map.tabs.length - 1; | |
| var index; | |
| // Go to previous tab | |
| if (["ArrowUp", "ArrowLeft", "Up", "Left"].indexOf(key) > -1) { | |
| index = map.index < 1 ? length : map.index - 1; | |
| } | |
| // Go to next tab | |
| else if (["ArrowDown", "ArrowRight", "Down", "Right"].indexOf(key) > -1) { | |
| index = map.index === length ? 0 : map.index + 1; | |
| } | |
| // Go to home | |
| else if (key === "Home") { | |
| index = 0; | |
| } | |
| // Go to end | |
| else if (key === "End") { | |
| index = length; | |
| } | |
| // Toggle the tab | |
| toggle(map.tabs[index]); | |
| }; | |
| /** | |
| * Create the Constructor object | |
| */ | |
| var Constructor = function (selector, options) { | |
| // | |
| // Variables | |
| // | |
| var publicAPIs = {}; | |
| var settings, tabWrapper; | |
| // | |
| // Methods | |
| // | |
| publicAPIs.destroy = function () { | |
| // Get all tabs | |
| var tabs = tabWrapper.querySelectorAll("a"); | |
| // Add roles to tabs | |
| Array.prototype.forEach.call(tabs, function (tab) { | |
| // Get the tab content | |
| var content = document.querySelector(tab.hash); | |
| if (!content) return; | |
| // Setup the tab | |
| destroyTab(tab, content, settings); | |
| }); | |
| // Remove role from wrapper | |
| tabWrapper.removeAttribute("role"); | |
| // Remove event listeners | |
| document.documentElement.removeEventListener( | |
| "click", | |
| clickHandler, | |
| true | |
| ); | |
| tabWrapper.removeEventListener("keydown", keyHandler, true); | |
| // Reset variables | |
| settings = null; | |
| tabWrapper = null; | |
| }; | |
| /** | |
| * Setup the DOM with the proper attributes | |
| */ | |
| publicAPIs.setup = function () { | |
| // Variables | |
| tabWrapper = document.querySelector(selector); | |
| if (!tabWrapper) return; | |
| var tabs = tabWrapper.querySelectorAll("a"); | |
| // Add role to wrapper | |
| tabWrapper.setAttribute("role", "tablist"); | |
| // Add roles to tabs. provide dynanmic tab indexes if we are within reveal | |
| var contentTabindexes = | |
| window.document.body.classList.contains("reveal-viewport"); | |
| var nextTabindex = 1; | |
| Array.prototype.forEach.call(tabs, function (tab) { | |
| if (contentTabindexes) { | |
| tab.setAttribute("tabindex", "" + nextTabindex++); | |
| } else { | |
| tab.setAttribute("tabindex", "0"); | |
| } | |
| // Get the tab content | |
| var content = document.querySelector(tab.hash); | |
| if (!content) return; | |
| // set tab indexes for content | |
| if (contentTabindexes) { | |
| getKeyboardFocusableElements(content).forEach(function (el) { | |
| el.setAttribute("tabindex", "" + nextTabindex++); | |
| }); | |
| } | |
| // Setup the tab | |
| setupTab(tab, content, settings); | |
| }); | |
| }; | |
| /** | |
| * Toggle a tab based on an ID | |
| * @param {String|Node} id The tab to toggle | |
| */ | |
| publicAPIs.toggle = function (id) { | |
| // Get the tab | |
| var tab = id; | |
| if (typeof id === "string") { | |
| tab = document.querySelector( | |
| selector + ' [role="tab"][href*="' + id + '"]' | |
| ); | |
| } | |
| // Toggle the tab | |
| toggle(tab); | |
| }; | |
| /** | |
| * Handle click events | |
| */ | |
| var clickHandler = function (event) { | |
| // Only run on toggles | |
| var tab = event.target.closest(selector + ' [role="tab"]'); | |
| if (!tab) return; | |
| // Prevent link behavior | |
| event.preventDefault(); | |
| // Toggle the tab | |
| toggle(tab); | |
| }; | |
| /** | |
| * Handle keydown events | |
| */ | |
| var keyHandler = function (event) { | |
| // Only run if a tab is in focus | |
| var tab = document.activeElement; | |
| if (!tab.matches(selector + ' [role="tab"]')) return; | |
| // Only run for specific keys | |
| if (["Home", "End"].indexOf(event.key) < 0) return; | |
| // Switch tabs | |
| switchTabs(tab, event.key); | |
| }; | |
| /** | |
| * Initialize the instance | |
| */ | |
| var init = function () { | |
| // Merge user options with defaults | |
| settings = extend(defaults, options || {}); | |
| // Setup the DOM | |
| publicAPIs.setup(); | |
| // Add event listeners | |
| document.documentElement.addEventListener("click", clickHandler, true); | |
| tabWrapper.addEventListener("keydown", keyHandler, true); | |
| }; | |
| // | |
| // Initialize and return the Public APIs | |
| // | |
| init(); | |
| return publicAPIs; | |
| }; | |
| // | |
| // Return the Constructor | |
| // | |
| return Constructor; | |
| } | |
| ); | |