// ========================================================================== // HTTP helpers // ========================================================================== async function _post(formObj, target, func) { try { const body = await createFormData(formObj, target, func); const res = await fetch('/rss/php/handler.php', { method: 'POST', body }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error('Post failed', err); return { ok: false, error: String(err?.message || err) }; } } async function createFormData(data = {}, target, func) { const fd = new FormData(); try { Object.entries(data || {}).forEach(([k, v]) => { if (Array.isArray(v)) v.forEach((item) => fd.append(`${k}[]`, item)); else if (v !== undefined && v !== null) fd.append(k, v); }); if (target !== undefined) fd.append('target', target); if (func !== undefined) fd.append('function', func); } catch (err) { console.error('Incorrect use of object', err); } return fd; } // ========================================================================== // DOM helpers // ========================================================================== function createEl(tag, classArr = null, text = null, dataAttrs = null) { const el = document.createElement(tag); if (text !== null && text !== undefined) el.append(document.createTextNode(String(text))); if (classArr && classArr.length) el.classList.add(...classArr); if (dataAttrs && Object.keys(dataAttrs).length) { for (const [k, v] of Object.entries(dataAttrs)) el.dataset[k] = v; } return el; } function getRandomID() { if (crypto?.randomUUID) return crypto.randomUUID(); return Math.random().toString(36).slice(2); } // ========================================================================== // Alerts (aligned with .alert + .alert-{name}) // ========================================================================== /* generateAlert('success', 'Saved!') →
Saved!
Options: { dismissAfter: ms, withClose: boolean, role: 'status'|'alert' } */ async function generateAlert(type = 'info', text = '', opts = {}) { const { dismissAfter = 0, withClose = false, role = 'status' } = opts; const alert = createEl('div', ['alert', `alert-${type}`]); alert.setAttribute('role', role); // status (polite) or alert (assertive) alert.setAttribute('aria-live', role === 'alert' ? 'assertive' : 'polite'); alert.append(document.createTextNode(text)); if (withClose) { const btn = createEl('button', ['btn', 'btn-sm', 'btn-ghost-dark'], '×', { dismiss: 'alert' }); btn.setAttribute('aria-label', 'Close alert'); btn.style.marginLeft = '0.5rem'; btn.addEventListener('click', () => alert.remove()); alert.append(btn); } if (dismissAfter > 0) setTimeout(() => alert.remove(), dismissAfter); return alert; } async function removeAlert(scope = document) { scope.querySelectorAll('.alert').forEach((a) => a.remove()); } // ========================================================================== // On load // ========================================================================== document.addEventListener('DOMContentLoaded', () => { // Menu toggler (pointerdown → click for better a11y; add ARIA state) const toggler = document.getElementById('menu-toggler'); const menu = document.getElementById('mainMenu'); if (toggler && menu) { toggler.setAttribute('aria-expanded', 'false'); toggler.setAttribute('aria-controls', 'mainMenu'); const toggleMenu = () => { const isOpen = menu.classList.toggle('open'); toggler.setAttribute('aria-expanded', String(isOpen)); }; toggler.addEventListener('click', toggleMenu); toggler.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(); } }); } // Section reveal const sections = document.querySelectorAll('section'); const observer = new IntersectionObserver( (entries, obs) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('visible'); obs.unobserve(entry.target); } }); }, { threshold: 0.05, rootMargin: '0px 0px -50px 0px' } ); sections.forEach((section) => { section.classList.add('section-animation'); observer.observe(section); }); (() => { function resolveTarget(el) { const viaData = el.getAttribute('data-target'); const viaHref = el.getAttribute('href'); if (viaData) return document.querySelector(viaData); if (viaHref && viaHref.startsWith('#')) return document.querySelector(viaHref); // Fallback: if only one modal exists and no selector provided const all = document.querySelectorAll('.modal'); return all.length === 1 ? all[0] : null; } function trapFocus(modal, e) { const focusables = modal.querySelectorAll('a,button,input,textarea,select,[tabindex]:not([tabindex="-1"])'); if (!focusables.length) return; const first = focusables[0], last = focusables[focusables.length - 1]; if (e.shiftKey && document.activeElement === first) { last.focus(); e.preventDefault(); } else if (!e.shiftKey && document.activeElement === last) { first.focus(); e.preventDefault(); } } let lastActive = null; function openModal(modal) { if (!modal) return; lastActive = document.activeElement; modal.classList.add('open'); modal.setAttribute('aria-hidden', 'false'); (modal.querySelector('.modal__dialog') || modal).focus({ preventScroll: true }); modal.addEventListener('keydown', onKeyDown); } function closeModal(modal) { if (!modal) return; modal.classList.remove('open'); modal.setAttribute('aria-hidden', 'true'); modal.removeEventListener('keydown', onKeyDown); if (lastActive) lastActive.focus({ preventScroll: true }); } function onKeyDown(e) { const modal = e.currentTarget; if (e.key === 'Escape') closeModal(modal); if (e.key === 'Tab') trapFocus(modal, e); } // Openers document.querySelectorAll('.modal-open').forEach(btn => { btn.addEventListener('click', (e) => { const modal = resolveTarget(btn); if (modal) openModal(modal); e.preventDefault(); }); btn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); } }); }); // Closers (overlay or close button) document.addEventListener('click', (e) => { const overlay = e.target.closest('.modal__overlay'); const closeBtn = e.target.closest('.modal__close'); if (!overlay && !closeBtn) return; const modal = e.target.closest('.modal'); if (modal) closeModal(modal); }); })(); (() => { function getTargetId(tab) { const href = tab.getAttribute('href'); if (href && href.startsWith('#')) return href.slice(1); const aria = tab.getAttribute('aria-controls'); if (aria) return aria; return null; } document.querySelectorAll('.tabs').forEach(tabs => { const tabEls = tabs.querySelectorAll('.tab'); if (!tabEls.length) return; // If panels are used, they should be siblings elsewhere with .tab-panel function activateTab(tab) { // Toggle active tab tabEls.forEach(t => t.classList.toggle('active', t === tab)); // Panels (optional) const targetId = getTargetId(tab); const panels = document.querySelectorAll('.tab-panel'); if (panels.length) { panels.forEach(p => { const match = p.id && targetId && p.id === targetId; p.hidden = !match; }); } } // Initial state: keep existing .active or default to first const current = tabs.querySelector('.tab.active') || tabEls[0]; if (current) activateTab(current); tabEls.forEach(tab => { tab.setAttribute('role', 'tab'); tab.addEventListener('click', (e) => { activateTab(tab); if (tab.hasAttribute('href')) e.preventDefault(); // prevent jump for href="#id" }); tab.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tab.click(); } // Optional arrow key UX within the same .tabs group if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault(); const arr = Array.from(tabEls); const i = arr.indexOf(tab); const next = e.key === 'ArrowRight' ? (i + 1) % arr.length : (i - 1 + arr.length) % arr.length; arr[next].focus(); } }); }); }); })(); }); document.querySelectorAll('.accordion').forEach(acc => { const single = acc.classList.contains('accordion--single'); acc.querySelectorAll('.accordion__header').forEach(btn => { btn.addEventListener('click', () => { const item = btn.closest('.accordion__item'); const isOpen = item.classList.contains('is-open'); if (single) { acc.querySelectorAll('.accordion__item.is-open').forEach(other => { if (other !== item) other.classList.remove('is-open'); }); } item.classList.toggle('is-open', !isOpen); }); btn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); } }); }); });