/** * Initializes tab navigation * Usage: * - Tab buttons: * - Tab panels:
...
* - Container:
...
* - 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'; container.dataset.tabsReady = '0'; 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); } container.dataset.tabsReady = '1'; // --- Form validation awareness --- const form = container.closest('form'); if (form) { const inputSelector = 'input,select,textarea'; const formId = form.id; const belongsToForm = (field) => { const f = field.getAttribute('form'); if (!f) return true; // kein form-Attr → gehört zum Ancestor-Form if (!formId) return false; // Feld hat form-Attr, unser Form hat keine ID return f === formId; }; const clearInvalidMarkers = () => { tabs.forEach(tab => tab.classList.remove('has-invalid')); }; const findInvalidPanels = () => { clearInvalidMarkers(); let firstInvalidTab = null; panels.forEach(panel => { const fields = panel.querySelectorAll(inputSelector); const hasInvalid = Array.from(fields).some(f => belongsToForm(f) && !f.disabled && !f.checkValidity()); if (hasInvalid) { const tabName = panel.dataset.tabPanel; const tab = tabs.find(t => t.dataset.tab === tabName); if (tab) { tab.classList.add('has-invalid'); } if (!firstInvalidTab) { firstInvalidTab = tabName; } } }); return firstInvalidTab; }; // Intercept submit buttons — both inside the form and external ones linked via form="id". // We use document-level click (capture) so we catch ALL submit triggers before native validation. document.addEventListener('click', (e) => { const btn = e.target.closest('button, [type="submit"]'); if (!btn) {return;} // Check if this button submits OUR form const btnFormAttr = btn.getAttribute('form'); const isInternalSubmit = !btnFormAttr && form.contains(btn) && (btn.type === 'submit' || (!btn.type && btn.tagName === 'BUTTON')); const isExternalSubmit = btnFormAttr && formId && btnFormAttr === formId && btn.type === 'submit'; if (!isInternalSubmit && !isExternalSubmit) {return;} const firstInvalidTab = findInvalidPanels(); if (!firstInvalidTab) {return;} // Active panel already shows the first invalid tab — let native validation handle it const activePanel = panels.find(p => !p.hidden); const needsTabSwitch = activePanel?.dataset.tabPanel !== firstInvalidTab; // Open any closed
ancestors of invalid fields so the browser can focus them const invalidFields = form.querySelectorAll(inputSelector); for (const field of invalidFields) { if (!belongsToForm(field) || field.disabled || field.checkValidity()) {continue;} let el = field.parentElement; while (el && el !== form) { if (el.tagName === 'DETAILS' && !el.open) { el.open = true; } el = el.parentElement; } } if (!needsTabSwitch) {return;} // Prevent native submit so the browser doesn't try to focus a hidden field e.preventDefault(); activateTab(firstInvalidTab); requestAnimationFrame(() => { form.reportValidity(); }); }, true); form.addEventListener('input', (e) => { const panel = e.target.closest('[data-tab-panel]'); if (!panel) {return;} const tabName = panel.dataset.tabPanel; const tab = tabs.find(t => t.dataset.tab === tabName); if (!tab) {return;} const fields = panel.querySelectorAll(inputSelector); if (Array.from(fields).every(f => !belongsToForm(f) || f.disabled || f.checkValidity())) { tab.classList.remove('has-invalid'); } }); form.addEventListener('change', (e) => { const panel = e.target.closest('[data-tab-panel]'); if (!panel) {return;} const tabName = panel.dataset.tabPanel; const tab = tabs.find(t => t.dataset.tab === tabName); if (!tab) {return;} const fields = panel.querySelectorAll(inputSelector); if (Array.from(fields).every(f => !belongsToForm(f) || f.disabled || f.checkValidity())) { tab.classList.remove('has-invalid'); } }); } // 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(); }