Files
corp_base/rss/js/main.js
2025-10-11 00:21:00 +02:00

262 lines
9.2 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.

// ==========================================================================
// 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!') → <div class="alert alert-success" ...>Saved!</div>
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(); }
});
});
});