106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
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<K extends string> {
|
|
/** 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<HTMLElement>) => void;
|
|
onDragEnd: () => void;
|
|
};
|
|
/** Auf die Spalte legen. */
|
|
columnProps: (column: K) => {
|
|
onDragOver: (event: DragEvent<HTMLElement>) => void;
|
|
onDragLeave: (event: DragEvent<HTMLElement>) => void;
|
|
onDrop: (event: DragEvent<HTMLElement>) => void;
|
|
};
|
|
}
|
|
|
|
export function useKanbanDrag<K extends string>(
|
|
onMove: (id: number, column: K) => void
|
|
): KanbanDrag<K> {
|
|
const [draggedId, setDraggedId] = useState<number | null>(null);
|
|
const [hoverColumn, setHoverColumn] = useState<K | null>(null);
|
|
|
|
const cardProps = useCallback(
|
|
(id: number) =>
|
|
({
|
|
draggable: true,
|
|
onDragStart: (event: DragEvent<HTMLElement>) => {
|
|
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<HTMLElement>) => {
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = 'move';
|
|
setHoverColumn((current) => (current === column ? current : column));
|
|
},
|
|
onDragLeave: (event: DragEvent<HTMLElement>) => {
|
|
// 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<HTMLElement>) => {
|
|
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' : '';
|
|
}
|