PDFs go through the same upload field as images, are checked by their content and kept unchanged in their own table. Every link field can pick one, and a page that links a PDF opens it in a dialog with the browser's own viewer; touch devices and browsers without a viewer get a new tab instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
7.3 KiB
JavaScript
185 lines
7.3 KiB
JavaScript
/*
|
||
* 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">×</button>
|
||
<button class="zoom-prev" type="button" aria-label="Vorheriges Bild">‹</button>
|
||
<button class="zoom-next" type="button" aria-label="Nächstes Bild">›</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">×</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();
|
||
});
|
||
}
|
||
})();
|