Files
tsv08kulmbach-website/public/assets/js/lightbox.js

152 lines
5.5 KiB
JavaScript
Raw Normal View History

Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
/* Lightbox: rüstet eine Bildergalerie ([data-lightbox-gallery]) von normalen
Bildlinks auf eine Großansicht im Overlay um. Ohne JS bleibt jedes
Vorschaubild ein Link auf die größte Variante der Datei der Klick führt
dann einfach zum Bild, nichts ist kaputt oder tot.
Bewusst generisch gehalten (keine veranstaltung"-Bezüge, Hooks nur über
data-lightbox-*), damit eine zweite Galerie auf der Seite dieselbe Datei
nutzen kann dasselbe Prinzip wie beim generischen carousel.js.
Das Overlay-Markup liefert PHP und ist `hidden`, bis JS es übernimmt (wie die
Steuerung des Stadionzeitungs-Viewers). Die Großansicht erbt ihr srcset vom
angeklickten Vorschaubild: so lädt ein Handy die kleine Variante und es gibt
nur EINE Bildliste im Markup. */
(function () {
'use strict';
function initGallery(root) {
var links = Array.prototype.slice.call(root.querySelectorAll('[data-lightbox]'));
var dialog = root.querySelector('[data-lightbox-dialog]');
if (!dialog || links.length === 0) {
return;
}
var image = dialog.querySelector('[data-lightbox-img]');
var counter = dialog.querySelector('[data-lightbox-current]');
var closeBtn = dialog.querySelector('[data-lightbox-close]');
var prevBtn = dialog.querySelector('[data-lightbox-prev]');
var nextBtn = dialog.querySelector('[data-lightbox-next]');
if (!image || !closeBtn) {
return;
}
var index = 0;
var isOpen = false;
function show(i) {
index = Math.max(0, Math.min(links.length - 1, i));
var thumb = links[index].querySelector('img');
image.src = links[index].getAttribute('href');
image.srcset = thumb ? thumb.getAttribute('srcset') || '' : '';
image.alt = thumb ? thumb.getAttribute('alt') || '' : '';
if (counter) {
counter.textContent = String(index + 1);
}
if (prevBtn) {
prevBtn.disabled = index === 0;
}
if (nextBtn) {
nextBtn.disabled = index === links.length - 1;
}
// Am ersten/letzten Bild wird der gerade fokussierte Knopf disabled
// und der Fokus fiele auf <body> — dann griffe die Fokusfalle unten
// nicht mehr und Tab liefe hinter das Overlay.
if (document.activeElement && document.activeElement.disabled) {
closeBtn.focus();
}
}
function onKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
hide();
return;
}
if (event.key === 'ArrowLeft') {
event.preventDefault();
show(index - 1);
return;
}
if (event.key === 'ArrowRight') {
event.preventDefault();
show(index + 1);
return;
}
if (event.key !== 'Tab') {
return;
}
// Fokusfalle: im Overlay sind nur diese Knöpfe erreichbar.
var focusable = [prevBtn, nextBtn, closeBtn].filter(function (btn) {
return btn && !btn.disabled;
});
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
function reveal(i) {
show(i);
dialog.hidden = false;
// Hintergrund festhalten, sonst scrollt die Seite unter dem Overlay
// weg (gleiches Muster wie body.nav-open beim Burger-Menü).
document.body.classList.add('lightbox-open');
document.addEventListener('keydown', onKeydown);
isOpen = true;
closeBtn.focus();
}
function hide() {
if (!isOpen) {
return;
}
dialog.hidden = true;
document.body.classList.remove('lightbox-open');
document.removeEventListener('keydown', onKeydown);
isOpen = false;
// Fokus zurück auf das ZULETZT gezeigte Vorschaubild, nicht auf das
// zuerst angeklickte: nach mehrmals Weiter ist das die Stelle, an
// der man im Raster gerade steht.
links[index].focus();
}
links.forEach(function (link, i) {
link.addEventListener('click', function (event) {
event.preventDefault();
reveal(i);
});
});
if (prevBtn) {
prevBtn.addEventListener('click', function () { show(index - 1); });
}
if (nextBtn) {
nextBtn.addEventListener('click', function () { show(index + 1); });
}
closeBtn.addEventListener('click', hide);
dialog.addEventListener('click', function (event) {
// Klick auf die Fläche neben dem Bild schließt; Klicks auf Bild und
// Knöpfe nicht.
if (event.target === dialog) {
hide();
}
});
root.classList.add('is-enhanced');
}
Array.prototype.slice.call(document.querySelectorAll('[data-lightbox-gallery]')).forEach(initGallery);
})();