/* ============================================================ FRAMEWORK.JS — CSS-Framework JavaScript API Version: 2.1.0 Zwei gleichwertige Wege: 1. Deklarativ über data-action (CSP-fest, empfohlen): Ein einziger Listener am document wertet die Attribute aus. Das funktioniert auch für Inhalte, die HTMX später nachlädt. 2. Globale Funktionen (wie bisher), z. B. openModal('m'). Inline-Handler wie onclick="…" verlangen aber 'unsafe-inline' in der CSP. Einbinden: ============================================================ */ // ---- SIDEBAR (Mobile) ---- function toggleSidebar() { document.body.classList.add('sidebar-open'); } function closeSidebar() { document.body.classList.remove('sidebar-open'); } // ---- MODAL ---- function openModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('modal-open'); } function closeModal(id) { document.getElementById(id).classList.remove('open'); if (!document.querySelector('.modal-backdrop.open')) document.body.classList.remove('modal-open'); } function closeOnBackdrop(e, id) { if (e.target === e.currentTarget) closeModal(id); } document.addEventListener('keydown', e => { if (e.key === 'Escape') { document.querySelectorAll('.modal-backdrop.open').forEach(m => m.classList.remove('open')); document.body.classList.remove('modal-open'); } }); // ---- TABS ---- function switchTab(btn, panelId) { const tabList = btn.closest('.tab-list'); tabList.querySelectorAll('button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const tabs = btn.closest('.tabs'); tabs.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); document.getElementById(panelId).classList.add('active'); } // ---- TOPNAV DROPDOWN ---- function toggleNavDd(btn) { const li = btn.closest('li'); const isOpen = li.classList.contains('open'); document.querySelectorAll('.topnav-links li.open').forEach(l => l.classList.remove('open')); if (!isOpen) li.classList.add('open'); } // ---- DROPDOWN ---- function toggleDropdown(id) { const dd = document.getElementById(id); const isOpen = dd.classList.contains('open'); document.querySelectorAll('.dropdown.open').forEach(d => d.classList.remove('open')); if (!isOpen) dd.classList.add('open'); } // ---- DISMISSIBLE ALERT ---- function dismissAlert(btn) { const alert = btn.closest('.alert'); alert.classList.add('is-dismissing'); alert.addEventListener('transitionend', () => alert.remove(), { once: true }); } // ---- SORTIERBARE TABELLEN ---- function sortTable(th) { const table = th.closest('table'); const col = [...th.parentElement.children].indexOf(th); const dir = th.dataset.sort === 'asc' ? 'desc' : 'asc'; table.querySelectorAll('th[data-sort]').forEach(t => { if (t !== th) t.dataset.sort = ''; }); th.dataset.sort = dir; const tbody = table.querySelector('tbody'); [...tbody.querySelectorAll('tr')] .sort((a, b) => { const aVal = a.cells[col].textContent.trim(); const bVal = b.cells[col].textContent.trim(); const aNum = parseFloat(aVal.replace(/[^0-9.-]/g, '')); const bNum = parseFloat(bVal.replace(/[^0-9.-]/g, '')); if (!isNaN(aNum) && !isNaN(bNum)) return dir === 'asc' ? aNum - bNum : bNum - aNum; return dir === 'asc' ? aVal.localeCompare(bVal, 'de') : bVal.localeCompare(aVal, 'de'); }) .forEach(r => tbody.appendChild(r)); } // ---- TOAST ---- // Baut den Toast über das DOM: kein innerHTML, kein style-Attribut. Die Meldung // wird als Text gesetzt, HTML darin wird also nicht ausgeführt. function showToast(message, type = 'info') { const icons = { success: '✓', warning: '⚠', danger: '✕', info: 'ℹ' }; if (!(type in icons)) type = 'info'; const id = 'toast-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7); const toast = document.createElement('div'); toast.className = 'toast'; toast.id = id; toast.setAttribute('role', 'status'); const icon = document.createElement('span'); icon.className = 'toast-icon toast-icon-' + type; icon.textContent = icons[type]; const content = document.createElement('div'); content.className = 'toast-content'; const title = document.createElement('p'); title.className = 'toast-title'; title.textContent = message; content.appendChild(title); const close = document.createElement('button'); close.className = 'toast-close'; close.type = 'button'; close.setAttribute('aria-label', 'Schließen'); close.dataset.action = 'toast-close'; close.textContent = '×'; toast.append(icon, content, close); document.getElementById('toast-container').appendChild(toast); setTimeout(() => removeToast(id), 4000); } function removeToast(id) { const el = document.getElementById(id); if (el) { el.classList.add('is-leaving'); setTimeout(() => el.remove(), 200); } } // ---- FORTSCHRITT ---- //
// Variable Breiten lassen sich nicht als Klasse ausdrücken. Über das DOM // gesetzte Styles erlaubt die CSP, style-Attribute im HTML nicht. function applyProgress(root) { (root || document).querySelectorAll('[data-progress]').forEach(el => { const wert = Math.max(0, Math.min(100, parseFloat(el.dataset.progress) || 0)); el.style.width = wert + '%'; }); } document.addEventListener('DOMContentLoaded', () => applyProgress()); // Nach jedem Austausch durch HTMX (Ereignis htmx:load) auch neue Inhalte document.addEventListener('htmx:load', e => applyProgress(e.target)); // ---- DATA-ACTION ---- // data-action Aktion (siehe Tabelle in README.md, „JavaScript-API“) // data-target ID des Ziels, z. B. Modal, Dropdown oder Tab-Panel // data-class Klasse für toggle-class // data-message Text für toast, data-type für dessen Art const frameworkActions = { 'sidebar-open': () => toggleSidebar(), 'sidebar-close': () => closeSidebar(), 'modal-open': el => openModal(el.dataset.target), 'modal-close': el => closeModal(el.dataset.target || el.closest('.modal-backdrop').id), 'modal-backdrop': (el, e) => { if (e.target === el) closeModal(el.id); }, 'tab': el => switchTab(el, el.dataset.target), 'nav-dropdown': el => toggleNavDd(el), 'dropdown': el => toggleDropdown(el.dataset.target || el.closest('.dropdown').id), 'alert-dismiss': el => dismissAlert(el), 'sort': el => sortTable(el), 'toggle-class': el => el.classList.toggle(el.dataset.class), 'toast': el => showToast(el.dataset.message, el.dataset.type), 'toast-close': el => removeToast(el.closest('.toast').id), }; document.addEventListener('click', e => { const el = e.target.closest('[data-action]'); const action = el && frameworkActions[el.dataset.action]; if (action) action(el, e); if (!e.target.closest('.topnav-links li')) document.querySelectorAll('.topnav-links li.open').forEach(l => l.classList.remove('open')); if (!e.target.closest('.dropdown')) document.querySelectorAll('.dropdown.open').forEach(d => d.classList.remove('open')); });