Files
KantSites/public/assets/lightbox.js
T
kantarciandClaude Opus 5.5 78083fac6a Use own SVG icons instead of text glyphs on the website
Phones turn glyphs such as arrows and crosses into emoji. The viewer, scroll button, video toggle, menus, breadcrumbs, pagers and quote stars now draw SVG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 20:02:24 +01:00

188 lines
7.6 KiB
JavaScript

/* Symbole als SVG: Schriftzeichen macht ein Telefon zu Emoji. */
const symbol = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="${p}"/></svg>`;
/*
* Der Bildbetrachter.
*
* Ohne dieses Skript ist ein vergrößerbares Bild schlicht ein Verweis auf die große Fassung: ein
* Klick öffnet sie, zurück führt die Zurück-Taste. Das ist die Grundlage, und sie funktioniert
* immer. Dieses Skript fängt den Klick ab und zeigt das Bild stattdessen über der Seite - mit
* Blättern, wenn es zu einer Galerie gehört.
*
* Benutzt wird <dialog>: der Browser bringt damit das Fangen des Fokus, das Schließen mit Escape
* und die Auszeichnung als Dialog schon mit. Eine nachgebaute Fassung aus <div> und Tastatur-
* abfragen wäre mehr Code und weniger zugänglich.
*/
(() => {
const zoomable = [...document.querySelectorAll('a[data-zoom]')];
if (0 === zoomable.length) {
return;
}
const dialog = document.createElement('dialog');
dialog.className = 'zoom-dialog';
dialog.innerHTML = `
<figure class="zoom-figure">
<img class="zoom-image" alt="">
<figcaption class="zoom-caption"></figcaption>
</figure>
<button class="zoom-close" type="button" aria-label="Schließen">${symbol('M6 6l12 12M18 6 6 18')}</button>
<button class="zoom-prev" type="button" aria-label="Vorheriges Bild">${symbol('M15 5.5 8.5 12l6.5 6.5')}</button>
<button class="zoom-next" type="button" aria-label="Nächstes Bild">${symbol('m9 5.5 6.5 6.5L9 18.5')}</button>
<p class="zoom-counter" aria-live="polite"></p>
`;
document.body.append(dialog);
const bild = dialog.querySelector('.zoom-image');
const beschriftung = dialog.querySelector('.zoom-caption');
const zaehler = dialog.querySelector('.zoom-counter');
const zurueck = dialog.querySelector('.zoom-prev');
const vor = dialog.querySelector('.zoom-next');
/** Die Bilder, zwischen denen geblättert wird: die der Galerie, sonst nur dieses eine. */
const gruppeVon = (link) => {
const galerie = link.closest('[data-zoom-group]');
return galerie ? [...galerie.querySelectorAll('a[data-zoom]')] : [link];
};
let gruppe = [];
let stelle = 0;
const zeige = (index) => {
stelle = (index + gruppe.length) % gruppe.length;
const link = gruppe[stelle];
const inneres = link.querySelector('img');
bild.src = link.href;
bild.alt = inneres ? inneres.alt : '';
const text = link.dataset.caption || '';
beschriftung.textContent = text;
beschriftung.hidden = '' === text;
const mehrere = gruppe.length > 1;
zurueck.hidden = !mehrere;
vor.hidden = !mehrere;
zaehler.hidden = !mehrere;
zaehler.textContent = mehrere ? `${stelle + 1} von ${gruppe.length}` : '';
};
for (const link of zoomable) {
link.addEventListener('click', event => {
// Mit gedrückter Zusatztaste oder mittlerer Maustaste bleibt es ein normaler Verweis:
// wer ein Bild in einem neuen Reiter öffnen will, soll das können.
if (event.metaKey || event.ctrlKey || event.shiftKey || 1 === event.button) {
return;
}
event.preventDefault();
gruppe = gruppeVon(link);
zeige(gruppe.indexOf(link));
dialog.showModal();
});
}
zurueck.addEventListener('click', () => zeige(stelle - 1));
vor.addEventListener('click', () => zeige(stelle + 1));
dialog.querySelector('.zoom-close').addEventListener('click', () => dialog.close());
// Ein Klick neben das Bild schließt - erwartet, und der Dialog selbst ist die Fläche daneben.
dialog.addEventListener('click', event => {
if (event.target === dialog) {
dialog.close();
}
});
dialog.addEventListener('keydown', event => {
if (1 === gruppe.length) {
return;
}
if ('ArrowLeft' === event.key) {
event.preventDefault();
zeige(stelle - 1);
}
if ('ArrowRight' === event.key) {
event.preventDefault();
zeige(stelle + 1);
}
});
})();
/*
* Der PDF-Betrachter - in derselben Datei wie der Bildbetrachter, damit eine Seite mit beidem
* nicht zwei Skripte laedt.
*
* Ohne dieses Skript ist ein Link auf ein PDF ein gewöhnlicher Link: der Browser öffnet die
* Datei. Das Skript zeigt sie stattdessen in einem Rahmen über der Seite, mit dem Betrachter,
* den der Browser ohnehin mitbringt - kein eigener Nachbau, der ein Megabyte wiegt.
*
* Wo der Browser keinen solchen Betrachter hat oder ihn im Rahmen nur halb zeigt, bleibt es beim
* neuen Tab: Chrome auf Android hat keinen, und Safari auf dem iPhone zeigt im Rahmen nur die
* erste Seite. Ein Link mit „In neuem Tab öffnen“ bleibt ohnehin, was er ist.
*/
(() => {
const PDF = /^\/media\/[a-f0-9]{32}\.pdf$/;
const links = [...document.querySelectorAll('a[href]')].filter(link => {
const url = new URL(link.href, location.href);
return url.origin === location.origin && PDF.test(url.pathname) && '_blank' !== link.target;
});
if (0 === links.length) {
return;
}
const eingebettet = false !== navigator.pdfViewerEnabled && !matchMedia('(pointer: coarse)').matches;
let dialog = null;
let rahmen = null;
let titel = null;
let laden = null;
const baue = () => {
dialog = document.createElement('dialog');
dialog.className = 'pdf-dialog';
dialog.setAttribute('aria-labelledby', 'pdf-dialog-title');
dialog.innerHTML = `
<div class="pdf-bar">
<p class="pdf-title" id="pdf-dialog-title"></p>
<a class="pdf-action pdf-download" download>Herunterladen</a>
<a class="pdf-action pdf-open" target="_blank" rel="noopener">Im neuen Tab</a>
<button class="pdf-close" type="button" aria-label="Schließen">${symbol('M6 6l12 12M18 6 6 18')}</button>
</div>
<iframe class="pdf-frame" title="PDF-Dokument"></iframe>
`;
document.body.append(dialog);
rahmen = dialog.querySelector('.pdf-frame');
titel = dialog.querySelector('.pdf-title');
laden = [dialog.querySelector('.pdf-download'), dialog.querySelector('.pdf-open')];
dialog.querySelector('.pdf-close').addEventListener('click', () => dialog.close());
dialog.addEventListener('click', event => {
if (event.target === dialog) {
dialog.close();
}
});
// Der Rahmen wird geleert, sonst läuft ein eingebettetes Video oder Audio im PDF weiter
// und das nächste Öffnen zeigt kurz das vorige Dokument.
dialog.addEventListener('close', () => rahmen.removeAttribute('src'));
};
for (const link of links) {
link.addEventListener('click', event => {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || 1 === event.button) {
return;
}
event.preventDefault();
if (!eingebettet) {
window.open(link.href, '_blank', 'noopener');
return;
}
if (null === dialog) {
baue();
}
const name = (link.getAttribute('aria-label') || link.textContent || '').trim();
titel.textContent = name || 'Dokument';
rahmen.title = name ? `PDF: ${name}` : 'PDF-Dokument';
for (const aktion of laden) {
aktion.href = link.href;
}
rahmen.src = link.href;
dialog.showModal();
});
}
})();