/**
|
* 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;
|
}
|
|
//TODO: Initialize notification system
|
// this.notify('tab-switched', {
|
// current: this.activeTab
|
// });
|
|
// 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;
|