Fix: restliche CSP-blockierte Inline-Handler — Bild-Fallbacks (globaler data-fb Error-Handler) + Hover-Effekte (CSS-Utilities + data-hover-play)

App ist jetzt vollständig frei von Inline-Event-Handlern (onerror/onmouseenter/etc.).
data-fb Modi: hide/hide-parent/dim-grandparent/sibling/show-el/emoji/initials + data-fb-src.
Hover: .by-hover-lift/-surface2/-surface3 in utilities.css. SW v1165
This commit is contained in:
rene 2026-06-04 16:22:43 +02:00
parent 2ddd8ac350
commit c07b1cc01b
23 changed files with 125 additions and 68 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1164'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1165'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
@ -434,6 +434,62 @@ const App = (() => {
// NAVIGATION EVENTS
// ----------------------------------------------------------
function _bindNavigation() {
// Globaler Bild-Fallback — ersetzt CSP-blockierte onerror-Attribute.
// 'error' bubbelt nicht → Capture-Phase. Greift nur bei [data-fb]/[data-fb-src].
document.addEventListener('error', e => {
const el = e.target;
if (!el || el.tagName !== 'IMG') return;
const fb = el.dataset.fb, altSrc = el.dataset.fbSrc;
if (fb === undefined && altSrc === undefined) return;
// Schritt 1: Alternative Quelle versuchen (z.B. _preview → Original / Platzhalter)
if (altSrc && !el.dataset.fbTried) {
el.dataset.fbTried = '1';
el.src = altSrc;
return;
}
// Schritt 2: terminaler Fallback
switch (fb) {
case 'hide-parent':
if (el.parentElement) el.parentElement.style.display = 'none';
break;
case 'dim-grandparent':
if (el.parentElement?.parentElement) el.parentElement.parentElement.style.opacity = '.4';
break;
case 'sibling':
el.style.display = 'none';
if (el.nextElementSibling) el.nextElementSibling.style.display = 'flex';
break;
case 'show-el': {
el.style.display = 'none';
const t = el.dataset.fbEl && document.getElementById(el.dataset.fbEl);
if (t) t.style.display = 'flex';
break;
}
case 'emoji':
if (el.parentElement) el.parentElement.innerHTML =
`<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:${el.dataset.fbSize || '2rem'}">${el.dataset.fbEmoji || '🐾'}</div>`;
break;
case 'initials': {
const sz = parseInt(el.dataset.fbSize, 10) || 40;
el.outerHTML =
`<div style="width:${sz}px;height:${sz}px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;font-size:${Math.round(sz * 0.45)}px;font-weight:700;color:var(--c-primary)">${el.dataset.fbInitials || ''}</div>`;
break;
}
default: // 'hide'
el.style.display = 'none';
el.classList.add('img-broken');
}
}, true);
// Video-Vorschau bei Hover (ersetzt CSP-blockierte onmouseenter/leave).
// <video> hat keine Kinder → e.target ist das Video selbst (matches() O(1)).
document.addEventListener('mouseover', e => {
if (e.target.matches?.('[data-hover-play]')) e.target.play?.().catch(() => {});
});
document.addEventListener('mouseout', e => {
if (e.target.matches?.('[data-hover-play]')) e.target.pause?.();
});
// Bottom Nav + Sidebar Klicks
document.addEventListener('click', e => {
const item = e.target.closest('[data-page]');