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>
52 lines
2.2 KiB
JavaScript
52 lines
2.2 KiB
JavaScript
/**
|
||
* Dragging images and PDFs onto the upload field, and saying which files are about to go up.
|
||
*
|
||
* Everything here is an addition. Without this script the field is still a file input with a
|
||
* label: clicking it opens the file picker and the form submits the same way. The script never
|
||
* uploads by itself - the button stays the moment where something is sent.
|
||
*/
|
||
const zone = document.querySelector('[data-dropzone]');
|
||
const input = zone?.querySelector('input[type="file"]');
|
||
const chosen = document.querySelector('[data-chosen]');
|
||
if (zone && input && chosen) {
|
||
const ACCEPTED = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
|
||
|
||
const describe = (files, ignored = 0) => {
|
||
if (!files.length) {
|
||
chosen.textContent = ignored
|
||
? `Keine passende Datei dabei. ${ignored} Datei(en) übersprungen – erlaubt sind JPEG, PNG, WebP und PDF.`
|
||
: 'Noch keine Datei gewählt.';
|
||
return;
|
||
}
|
||
const names = [...files].map(file => file.name).join(', ');
|
||
const size = [...files].reduce((total, file) => total + file.size, 0);
|
||
chosen.textContent = `${files.length} Datei(en) gewählt (${(size / 1048576).toFixed(1)} MB): ${names}`
|
||
+ (ignored ? ` · ${ignored} Datei(en) übersprungen, erlaubt sind JPEG, PNG, WebP und PDF.` : '');
|
||
};
|
||
|
||
input.addEventListener('change', () => describe(input.files));
|
||
|
||
// Without preventDefault the browser navigates to the dropped file and the page is gone.
|
||
for (const event of ['dragenter', 'dragover']) {
|
||
zone.addEventListener(event, dragged => {
|
||
dragged.preventDefault();
|
||
zone.classList.add('is-dragging');
|
||
});
|
||
}
|
||
for (const event of ['dragleave', 'dragend']) {
|
||
zone.addEventListener(event, () => zone.classList.remove('is-dragging'));
|
||
}
|
||
|
||
zone.addEventListener('drop', dropped => {
|
||
dropped.preventDefault();
|
||
zone.classList.remove('is-dragging');
|
||
const files = [...(dropped.dataTransfer?.files ?? [])];
|
||
const images = files.filter(file => ACCEPTED.includes(file.type));
|
||
// The input is what the form submits, so the dropped files have to end up in it.
|
||
const transfer = new DataTransfer();
|
||
images.forEach(file => transfer.items.add(file));
|
||
input.files = transfer.files;
|
||
describe(input.files, files.length - images.length);
|
||
});
|
||
}
|