Upload PDFs to the library, link them from any link target, show them in a viewer

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>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-22 07:56:29 +02:00
1 parent 04927b9563
commit e8a9dcdf5c
20 files changed
+667 -30

No files matched your search

+83
View File
@@ -99,3 +99,86 @@
}
});
})();
/*
* 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();
});
}
})();