import { useCallback, useState, type DragEvent } from 'react'; /* * Ziehen und Fallenlassen auf den Kanban-Brettern (Leads, Ausschreibungen). * * Die Gesten stecken hier und nicht in den Ansichten, weil sie dreimal an * denselben Stellen schiefgehen: * * 1. **Ohne Nutzlast keine Geste.** Firefox startet eine Ziehgeste nur, wenn * `dragstart` etwas in `dataTransfer` legt. Chrome ist da nachsichtiger - * deshalb faellt es beim Entwickeln nicht auf und beim Benutzen sofort. * 2. **Der Zustand allein reicht nicht.** Die Kennung wird zusaetzlich in * `dataTransfer` mitgegeben. Sie ueberlebt damit auch einen Neuaufbau der * Liste waehrend der Geste. * 3. **Ohne `dropEffect` zeigt der Zeiger ein Verbotsschild**, obwohl das * Fallenlassen erlaubt ist. * * Bilder in den Kacheln (Logo, Foto) muessen zusaetzlich `draggable={false}` * tragen - sonst zieht der Browser das Bild statt der Kachel. Das erledigt die * Avatar-Komponente. */ /** Eigenes Format neben `text/plain`: erkennt eigene Kacheln von fremdem Text. */ const MIME = 'application/x-icon-card'; interface KanbanDrag { /** Kennung der Kachel am Zeiger - fuer die gedaempfte Darstellung. */ draggedId: number | null; /** Spalte unter dem Zeiger - fuer die Hervorhebung des Ziels. */ hoverColumn: K | null; /** Auf die Kachel legen. */ cardProps: (id: number) => { draggable: true; onDragStart: (event: DragEvent) => void; onDragEnd: () => void; }; /** Auf die Spalte legen. */ columnProps: (column: K) => { onDragOver: (event: DragEvent) => void; onDragLeave: (event: DragEvent) => void; onDrop: (event: DragEvent) => void; }; } export function useKanbanDrag( onMove: (id: number, column: K) => void ): KanbanDrag { const [draggedId, setDraggedId] = useState(null); const [hoverColumn, setHoverColumn] = useState(null); const cardProps = useCallback( (id: number) => ({ draggable: true, onDragStart: (event: DragEvent) => { event.dataTransfer.setData(MIME, String(id)); event.dataTransfer.setData('text/plain', String(id)); event.dataTransfer.effectAllowed = 'move'; setDraggedId(id); }, onDragEnd: () => { setDraggedId(null); setHoverColumn(null); }, }) as const, [] ); const columnProps = useCallback( (column: K) => ({ onDragOver: (event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; setHoverColumn((current) => (current === column ? current : column)); }, onDragLeave: (event: DragEvent) => { // Der Zeiger wandert staendig ueber Kindelemente - nur das Verlassen // der Spalte selbst zaehlt. if (event.currentTarget.contains(event.relatedTarget as Node | null)) return; setHoverColumn((current) => (current === column ? null : current)); }, onDrop: (event: DragEvent) => { event.preventDefault(); const carried = event.dataTransfer.getData(MIME) || event.dataTransfer.getData('text/plain'); const id = draggedId ?? Number(carried); setDraggedId(null); setHoverColumn(null); if (Number.isInteger(id) && id > 0) onMove(id, column); }, }), [draggedId, onMove] ); return { draggedId, hoverColumn, cardProps, columnProps }; } /** Rahmen der Spalte, solange eine Kachel darueber schwebt. */ export function columnClass(isHover: boolean): string { return isHover ? 'border-brand-400 bg-brand-50/40' : 'border-ink-200 bg-canvas'; } /** Die Kachel am Zeiger wird gedaempft - sonst sieht man nicht, was man zieht. */ export function cardClass(isDragged: boolean): string { return isDragged ? 'opacity-40' : ''; }