(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; } );