Das Auswahlfeld des Browsers sah aus wie das Auswahlfeld des Browsers. Hochgeladen wird jetzt über eine Ablagefläche: Bilder hineinziehen oder klicken, darunter steht, welche Dateien gewählt sind und welche übersprungen wurden. Darunter liegt weiter ein echtes input[type=file] - unsichtbar, aber fokussierbar, mit der Fläche als label und dem Fokusring über :focus-within. Ohne JavaScript bleibt genau das übrig: ein Dateifeld mit Beschriftung. Das Skript ergänzt Ziehen und benennt die Auswahl; abgeschickt wird nie von selbst. Kein "required", weil der Browser seine Meldung nicht an etwas Unsichtbares hängen kann - der Server weist einen leeren Upload mit einem lesbaren Satz ab. docs/UPDATE.md plant das Update über einen Git-Tag: Vorprüfung, Sicherung, Wartungszustand mit 503 für die Website und offenem /admin, Checkout, composer, Migration, Cache, Rauchtest, Protokoll - und den Rückweg in derselben Reihenfolge rückwärts. Migrationen bleiben nicht umkehrbar, der Rückweg ist die Sicherung. Kein Updater, der eine Adresse aus dem Formular zieht, und kein automatisches Update im Hintergrund. Zwei Entscheidungen hängen am Hosting: Composer auf dem Server oder vendor/ im Tag, und ob es Shell-Zugriff gibt. Gebaut ist davon nichts. 219 PHP-Tests / 881 Assertions unter PHP 8.4.22, darunter Upload über das Formular mit zwei Dateien, CSRF-Pflicht, Versionskonflikt, Archivfilter und Blättern mit 26 Bildern. Playwright prüft die Ablagefläche am echten Dateifeld; axe bleibt auf elf Bildschirmen ohne Verstöße. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52 lines
2.2 KiB
JavaScript
52 lines
2.2 KiB
JavaScript
/**
|
||
* Dragging images 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'];
|
||
|
||
const describe = (files, ignored = 0) => {
|
||
if (!files.length) {
|
||
chosen.textContent = ignored
|
||
? `Keine Bilddatei dabei. ${ignored} Datei(en) übersprungen – erlaubt sind JPEG, PNG und WebP.`
|
||
: '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 und WebP.` : '');
|
||
};
|
||
|
||
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);
|
||
});
|
||
}
|