hugin/static/css-framework/framework.js
2026-10-27 08:33:21 +01:00

192 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
FRAMEWORK.JS — CSS-Framework JavaScript API
Version: 2.1.0
Zwei gleichwertige Wege:
1. Deklarativ über data-action (CSP-fest, empfohlen):
<button data-action="modal-open" data-target="m">Öffnen</button>
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: <script src="/static/js/framework.js" defer></script>
============================================================ */
// ---- 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 ----
// <div class="progress-bar" data-progress="75"></div>
// 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'));
});