fix(upload): gezogenes Vorschaubild wurde erneut hochgeladen

Zieht man das angezeigte Bild ein Stueck zur Seite, legt Chrome es als
echte Datei in den Ziehvorgang. dataTransfer.types enthaelt dann
"Files" - nicht zu unterscheiden von einer Datei aus dem Dateimanager.
Die Ablageflaeche lud das eigene Bild daraufhin ein zweites Mal hoch.

Zwei Riegel: draggable={false} an allen Bildern, die kein eigenes
Ziehen brauchen (die Kachel bleibt ziehbar und verschiebt die Datei),
und useInternalDrag - was in dieser Seite losgezogen ist, wird nie als
Upload behandelt. dragstart feuert nur bei eigener Quelle.

Gemessen: vorher 1 Upload, nachher 0. Ein Zug von aussen laedt
unveraendert hoch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-24 17:24:22 +02:00
1 parent e9e07f9bed
commit b5a83e4057
11 files changed
+92 -3

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import { useEffect, useRef } from 'react'
/**
* Läuft gerade ein Ziehvorgang, der **in dieser Seite** begonnen hat?
*
* Anlass war ein Fehler, den man nur beim Benutzen findet: Zieht man das
* angezeigte Bild ein Stück zur Seite, legt Chrome es als echte Datei in die
* Zwischenablage des Ziehvorgangs – `dataTransfer.types` enthält dann
* `Files`, genau wie bei einer Datei aus dem Dateimanager. Die Ablagefläche
* konnte beides nicht unterscheiden und lud das Bild ein zweites Mal hoch.
*
* Die Kennungen `aether-file-id` und `aether-folder-id` helfen hier nicht:
* Die setzt unser eigener Verschiebe-Code, ein vom Browser begonnener
* Bild-Zug hat sie nicht.
*
* Deshalb der Umweg über das Ereignis: `dragstart` feuert nur, wenn die
* Quelle in diesem Dokument liegt. Kommt etwas von außen, gibt es kein
* `dragstart` – und nur dann darf hochgeladen werden.
*/
export function useInternalDrag(): () => boolean {
const active = useRef(false)
useEffect(() => {
const start = () => { active.current = true }
// Der Rückweg über zwei Ereignisse: `dragend` feuert an der Quelle,
// `drop` am Ziel. Fällt eines aus (abgebrochener Zug außerhalb des
// Fensters), setzt das andere zurück.
const stop = () => { active.current = false }
// Setzen in der Erfassungsphase, damit die Marke steht, bevor irgendein
// Handler sie liest. **Zurücksetzen dagegen in der Blasenphase**: Ein
// `drop`-Listener am Dokument mit `capture` liefe *vor* der Ablagefläche,
// hätte die Marke also schon gelöscht, wenn sie gebraucht wird.
document.addEventListener('dragstart', start, true)
document.addEventListener('dragend', stop)
document.addEventListener('drop', stop)
return () => {
document.removeEventListener('dragstart', start, true)
document.removeEventListener('dragend', stop)
document.removeEventListener('drop', stop)
}
}, [])
return () => active.current
}