/** * Addresses common concerns with the Tabs-family components; coordinates initialization and nesting of multiple tabs on a page * * Supports: * - empty tab titles * - spaces in tab titles * - special characters in tab titles * * When creating a new Tabs-based compoent, add a new selector to the "allTabTypes" array */ (function () { "use strict"; var NS = "cmp"; var IS; var selectors; /** * Tabs Configuration * * @typedef {Object} TabsConfig Represents a Tabs configuration * @property {HTMLElement} element The HTMLElement representing the Tabs * @property {Object} options The Tabs options */ /** * Tabs * * @class Tabs * @classdesc An interactive Tabs component for navigating a list of tabs * @param {TabsConfig} config The Tabs configuration */ function Tabs(config) { var that = this; if (config && config.element) { init(config); } $(window).on('hashchange', function() { init(config); }) /** * Initializes the Tabs * * @private * @param {TabsConfig} config The Tabs configuration */ function init(config) { // prevents multiple initialization config.element.removeAttribute("data-" + NS + "-is"); cacheElements(config.element); that._active = getActiveIndex(that._elements["tab"]); that._options = config.options; var pageURL = document.location.href; if (that._elements.tabpanel && (pageURL.split("#")[1] != "page-top")) { refreshActive(); } if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) { /* * Editor message handling: * - subscribe to "cmp.panelcontainer" message requests sent by the editor frame * - check that the message data panel container type is correct and that the id (path) matches this specific Tabs component * - if so, route the "navigate" operation to enact a navigation of the Tabs based on index data */ new window.Granite.author.MessageChannel("cqauthor", window).subscribeRequestMessage("cmp.panelcontainer", function (message) { if (message.data && (message.data.type === "cmp-tabs" || message.data.type === "cmp-imagetabs") && message.data.id === that._elements.self.dataset["cmpPanelcontainerId"]) { if (message.data.operation === "navigate") { navigate(message.data.index); } } }); } } /** * Returns the index of the active tab, if no tab is active returns 0 * * @param {Array} tabs Tab elements * @returns {Number} Index of the active tab, 0 if none is active */ function getActiveIndex(tabs) { let index1 = -1, index2 = -1; if (tabs) { index1 = tabs.findIndex((tab) => tab.classList.contains("active")); index2 = tabs.findIndex((tab) => tab.classList.contains(selectors.active.tab)); } return index1 >= 0 ? index1 : index2 >= 0 ? index2 : 0; } /** * Caches the Tabs elements as defined via the {@code data-tabs-hook="ELEMENT_NAME"} markup API * * @private * @param {HTMLElement} wrapper The Tabs wrapper element */ function cacheElements(wrapper) { that._elements = {}; that._elements.self = wrapper; var hooks = that._elements.self.querySelectorAll("[data-" + NS + "-hook-" + IS + "]"); for (var i = 0; i < hooks.length; i++) { var hook = hooks[i]; if (hook.closest("." + NS + "-" + IS) === that._elements.self) { // only process own tab elements var capitalized = IS; capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); var key = hook.dataset[NS + "Hook" + capitalized]; if (that._elements[key]) { that._elements[key].push(hook); } else { that._elements[key] = [hook]; } } } } /** * Refreshes the tab markup based on the current {@code Tabs#_active} index * * @private */ function refreshActive() { var tabpanels = that._elements["tabpanel"]; var tabs = that._elements["tab"]; var order = that._options.order; if (tabpanels) { for (var i = 0; i < tabpanels.length; i++) { const originalId = tabpanels[i].getAttribute("id"); let modifiedTabId = replaceInvalidChars(originalId, `${order}-${i+1}-tab`); if (modifiedTabId.slice(-4) !== "-tab") modifiedTabId += "-tab"; const modifiedPanelId = replaceInvalidChars(originalId, `${order}-${i+1}`); if (tabs && tabs[i]) { tabs[i].setAttribute("id", modifiedTabId); tabs[i].setAttribute("href", '#' + modifiedPanelId); tabs[i].setAttribute("aria-controls", modifiedPanelId); } tabpanels[i].setAttribute("id", modifiedPanelId); tabpanels[i].setAttribute("aria-labelledby", modifiedTabId); if (i === parseInt(that._active)) { tabpanels[i].classList.add(selectors.active.tabpanel); tabpanels[i].classList.add("active"); tabpanels[i].classList.add("show"); if (tabs && tabs[i]) { tabs[i].classList.add(selectors.active.tab); tabs[i].setAttribute("aria-selected", true); tabs[i].setAttribute("tabindex", "0"); } } else { tabpanels[i].classList.remove(selectors.active.tabpanel); tabpanels[i].classList.remove("active"); tabpanels[i].classList.add("show"); if (tabs && tabs[i]) { tabs[i].classList.remove(selectors.active.tab); tabs[i].classList.remove("active"); tabs[i].setAttribute("aria-selected", false); tabs[i].setAttribute("tabindex", "-1"); } } } } } // Regex is based on the spec https://www.w3.org/TR/html4/types.html#type-id function replaceInvalidChars(item, defaultId) { item = decodeURIComponent(item)?.trim(); //Replace them with dash in all id's item = item?.replace(/[^-_0-9a-zA-Z#]/g, "-"); if ((!item || item==='null' || item==='-tab' || item.includes('--')) && defaultId) { item = defaultId; } //Set URL pattern as # return item.toLowerCase(); } /** * Navigates to the tab at the provided index * * @private * @param {Number} index The index of the tab to navigate to */ function navigate(index) { that._active = index; refreshActive(); } $("coral3-Tree-contentContainer").on('click', function(e) { if ( $(e.target.closest("coral3-Tree-contentContainer")) .find(">coral-tree-item-content>span") .text() === "Image Tabs" ) { that._active = 1; refreshActive(); } }); } /** * Reads options data from the Tabs wrapper element, defined via {@code data-cmp-*} data attributes * * @private * @param {HTMLElement} element The Tabs element to read options data from * @returns {Object} The options read from the component data attributes */ function readData(element) { var data = element.dataset; var options = []; var capitalized = IS; capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); var reserved = ["is", "hook" + capitalized]; for (var key in data) { if (data.hasOwnProperty(key)) { var value = data[key]; if (key.indexOf(NS) === 0) { key = key.slice(NS.length); key = key.charAt(0).toLowerCase() + key.substring(1); if (reserved.indexOf(key) === -1) { options[key] = value; } } } } return options; } /** * Document ready handler and DOM mutation observers. Initializes Tabs components as necessary. * * @private */ function onDocumentReady() { const allTabTypes = ['tabs', 'imagetabs']; allTabTypes.forEach(function (type) { IS = type; selectors = { self: "[data-" + NS + '-is="' + IS + '"]', active: { tab: `cmp-tabs__tab--active`, tabpanel: `cmp-tabs__tabpanel--active` } }; var elements = document.querySelectorAll(selectors.self); for (var i = 0; i < elements.length; i++) { new Tabs({ element: elements[i], options: {...readData(elements[i]), order: `${type}${i+1}`} }); } var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; var body = document.querySelector("body"); var observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { // needed for IE var nodesArray = [].slice.call(mutation.addedNodes); if (nodesArray.length > 0) { nodesArray.forEach(function (addedNode) { if (addedNode.querySelectorAll) { var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); elementsArray.forEach(function (element) { new Tabs({ element: element, options: readData(element) }); }); } }); } }); }); observer.observe(body, { subtree: true, childList: true, characterData: true }); }); } if (document.readyState !== "loading") { onDocumentReady(); } else { document.addEventListener("DOMContentLoaded", onDocumentReady); } }());