baseline
This commit is contained in:
118
web/js/components/app-tabs.js
Normal file
118
web/js/components/app-tabs.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Initializes tab navigation
|
||||
* Usage:
|
||||
* - Tab buttons: <button data-tab="system">System</button>
|
||||
* - Tab panels: <div data-tab-panel="system">...</div>
|
||||
* - Container: <div data-tabs>...</div>
|
||||
* - URL param: ?tab=system
|
||||
*/
|
||||
export function initTabs(root = document) {
|
||||
const containers = root.querySelectorAll('[data-tabs]');
|
||||
|
||||
containers.forEach((container) => {
|
||||
if (container.dataset.tabsBound === '1') return;
|
||||
container.dataset.tabsBound = '1';
|
||||
|
||||
const tabs = Array.from(container.querySelectorAll('[data-tab]'));
|
||||
const panels = Array.from(container.querySelectorAll('[data-tab-panel]'));
|
||||
|
||||
if (!tabs.length || !panels.length) return;
|
||||
|
||||
const storageKey = container.dataset.tabsStorageKey || (container.id ? `tabs:${container.id}` : null);
|
||||
const urlParamName = container.dataset.tabsParam || 'tab';
|
||||
|
||||
// Helper to get URL parameter
|
||||
const getUrlParam = (name) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name);
|
||||
};
|
||||
|
||||
// Helper to set URL parameter
|
||||
const setUrlParam = (name, value) => {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set(name, value);
|
||||
window.history.replaceState({}, '', url);
|
||||
};
|
||||
|
||||
// Get initial active tab from URL param, localStorage, or data-tab-default
|
||||
let activeTab = null;
|
||||
const urlTab = getUrlParam(urlParamName);
|
||||
if (urlTab && panels.some(p => p.dataset.tabPanel === urlTab)) {
|
||||
activeTab = urlTab;
|
||||
} else if (storageKey) {
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored && panels.some(p => p.dataset.tabPanel === stored)) {
|
||||
activeTab = stored;
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
if (!activeTab) {
|
||||
const defaultTab = container.querySelector('[data-tab-default]');
|
||||
activeTab = defaultTab?.dataset.tab || tabs[0]?.dataset.tab;
|
||||
}
|
||||
|
||||
const activateTab = (tabName) => {
|
||||
tabs.forEach(tab => {
|
||||
const isActive = tab.dataset.tab === tabName;
|
||||
tab.classList.toggle('is-active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
panels.forEach(panel => {
|
||||
const isVisible = panel.dataset.tabPanel === tabName;
|
||||
panel.hidden = !isVisible;
|
||||
panel.setAttribute('aria-hidden', isVisible ? 'false' : 'true');
|
||||
});
|
||||
|
||||
// Update URL parameter
|
||||
setUrlParam(urlParamName, tabName);
|
||||
|
||||
// Save to localStorage
|
||||
if (storageKey) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, tabName);
|
||||
} catch (e) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize tabs
|
||||
tabs.forEach(tab => {
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
activateTab(tab.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
panels.forEach(panel => {
|
||||
panel.setAttribute('role', 'tabpanel');
|
||||
});
|
||||
|
||||
container.setAttribute('role', 'tablist');
|
||||
|
||||
// Activate initial tab
|
||||
if (activeTab) {
|
||||
activateTab(activeTab);
|
||||
}
|
||||
|
||||
// Listen for popstate (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
const newTab = getUrlParam(urlParamName);
|
||||
if (newTab && panels.some(p => p.dataset.tabPanel === newTab)) {
|
||||
activateTab(newTab);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize on load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initTabs());
|
||||
} else {
|
||||
initTabs();
|
||||
}
|
||||
Reference in New Issue
Block a user