/** * Handles the tabs functionality, given the html requirements are met */ class TabsContainer{ constructor(container, callbacks = {}, parent = null){ this.tabs = container.querySelector('.tabs'); this.a11y = window.jvbA11y; this.updateURL = true; this.parent = parent; this.childTabs = new Map(); if ('updateURL' in callbacks && callbacks.updateURL === false) { this.updateURL = false; } this.callbacks = callbacks; this.activeTab = (this.updateURL) ? this.getInitialTabFromHash() : container.querySelector('button.tab.active')?.dataset.tab; this.container = container; if (this.tabs) { this.tabs.addEventListener('click', e => { const tab = e.target.closest('[data-tab]'); if (tab) { let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true; this.switchTab(tab.dataset.tab, update); } }); } // Check for and initialize child tab containers this.initializeChildTabs(); // Find and set up select dropdown listeners this.selectDropdown = document.querySelector('select.tab-list'); if (this.selectDropdown) { this.selectDropdown.addEventListener('change', e => { let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true; this.switchTab(e.target.value, update); }); } let update = ('updateURL' in this.callbacks) ? this.callbacks.updateURL : true; if (!this.activeTab){ this.activeTab = document.querySelector('button.tab')?.dataset.tab; } this.switchTab(this.activeTab, update); } /** * Auto-detect and initialize child tab containers */ initializeChildTabs() { this.tabs.querySelectorAll('button').forEach(tab => { let hasChildren = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`); if(hasChildren && hasChildren.querySelector('.tabs')){ let container = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`); let tabs = new window.jvbTabs(container, {updateURL: false}, this); this.childTabs.set(tab.dataset.tab, tabs); } }); // Find all tab content panels in this container // const tabPanels = this.container.querySelectorAll('.tab-content'); // tabPanels.forEach(panel => { // const tabId = panel.dataset.tab; // if (!tabId) return; // // // Look for nested tab containers within this panel // const nestedTabContainers = panel.querySelectorAll('.tabs, [data-tabs]'); // // nestedTabContainers.forEach(nestedContainer => { // // Create a new TabsContainer for this nested container // const childContainer = new TabsContainer(nestedContainer, {}, this); // // // Store the child container // this.childTabs.set(tabId, childContainer); // }); // }); } /** * Get initial tab from URL hash * @returns {string|false} Tab identifier from hash or null if not present */ getInitialTabFromHash() { if (!window.location.hash) return false; const hash = window.location.hash.substring(1); // Remove the # character const pathParts = hash.split('/'); // If this is a root container, check first path part if (!this.parent) { const rootTabId = pathParts[0]; // Check if this hash corresponds to a valid tab const matchingTab = this.tabs.querySelector(`[data-tab="${rootTabId}"]`); if (matchingTab) { return rootTabId; } } // If this is a child container, check if our parent's active tab matches the path else if (this.parent && pathParts.length > 1) { const parentIdx = this.getParentDepth(); if (parentIdx < pathParts.length) { const childTabId = pathParts[parentIdx]; const matchingTab = this.tabs.querySelector(`[data-tab="${childTabId}"]`); if (matchingTab) { return childTabId; } } } return null; } /** * Calculate the depth of this tabs container in the hierarchy * @returns {number} The depth (0 for root, 1 for first level child, etc.) */ getParentDepth() { let depth = 0; let parent = this.parent; while (parent) { depth++; parent = parent.parent; } return depth; } /** * Build the full path for this tab including all parent tabs * @param {string} tabId - The current tab ID * @returns {string} The full path including parent tabs */ getFullTabPath(tabId) { if (!this.parent) { return tabId; } // Get parent's active tab path and append this tab const parentPath = this.parent.getFullTabPath(this.parent.activeTab); return `${parentPath}/${tabId}`; } /** * Switch between tabs * @param {string} tab - Tab to switch to ('items' or 'lists') * @param {boolean} updateHistory - Whether to push the state to the url */ switchTab(tab, updateHistory = false) { document.activeElement?.blur(); // Update tab buttons this.tabs.querySelectorAll('[data-tab]').forEach(tabBtn => { tabBtn.classList.toggle('active', tabBtn.dataset.tab === tab); tabBtn.setAttribute('aria-selected', tabBtn.dataset.tab === tab); }); // Update tab panels this.container.querySelectorAll('.tab-content').forEach(content => { content.classList.toggle('active', content.dataset.tab === tab); content.setAttribute('aria-hidden', content.dataset.tab !== tab); content.hidden = content.dataset.tab !== tab; }); // Update state this.activeTab = tab; if (this.callbacks[tab]) { this.callbacks[tab](); } // Activate first child tab if this tab has children const childContainer = this.childTabs.get(tab); if (childContainer) { const firstTab = childContainer.container.querySelector('button.tab')?.dataset.tab; if (firstTab) { childContainer.switchTab(firstTab, false); } } // Update URL hash with full path (only from root container) if (updateHistory) { if (!this.parent) { window.history.pushState({ tab: tab }, '', `#${tab}`); } else { // This is a child container, notify parent to update URL this.parent.updateUrlFromChild(); } } // Update select dropdown if it exists if (this.selectDropdown && this.selectDropdown.querySelector(`option[value="${tab}"]`)) { this.selectDropdown.value = tab; } // Announce to screen readers this.a11y.announce(`Switched to ${tab} tab`); } /** * Update URL when a child tab changes */ updateUrlFromChild() { console.log('Updating URL'); if (('updateURL' in this.callbacks) ? this.callbacks.updateURL : true) { if (!this.parent) { // Only the root container should update the URL const fullPath = this.getFullTabPath(this.activeTab); window.history.pushState({ tab: fullPath }, '', `#${fullPath}`); } else { // Propagate up to the root this.parent.updateUrlFromChild(); } } } } window.jvbTabs = TabsContainer;