Browse Source

fix: replace HTML5 drag with pointer events for clip reorder

HTML5 drag-and-drop doesn't work inside React Flow nodes because
React Flow captures pointer events at a higher level. Switch to
pointer-event-based reorder with visual feedback (dragged item dims,
hover target highlights with blue ring).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
5bc99ce03e
  1. 78
      src/components/nodes/VideoStitchNode.tsx

78
src/components/nodes/VideoStitchNode.tsx

@ -163,44 +163,56 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
extractThumbnails(); extractThumbnails();
}, [orderedClips]); }, [orderedClips]);
// Drag and drop for reordering clips // Pointer-based drag reorder (HTML5 drag doesn't work inside React Flow nodes)
const [draggedClipId, setDraggedClipId] = useState<string | null>(null); const [draggedClipId, setDraggedClipId] = useState<string | null>(null);
const [hoverClipId, setHoverClipId] = useState<string | null>(null);
const handleDragStart = useCallback((e: React.DragEvent, edgeId: string) => { const handlePointerDown = useCallback((e: React.PointerEvent, edgeId: string) => {
// Only left mouse button
if (e.button !== 0) return;
e.stopPropagation();
setDraggedClipId(edgeId); setDraggedClipId(edgeId);
e.dataTransfer.effectAllowed = "move"; setHoverClipId(null);
(e.target as HTMLElement).setPointerCapture(e.pointerId);
}, []); }, []);
const handleDragOver = useCallback((e: React.DragEvent) => { const handlePointerMove = useCallback((e: React.PointerEvent) => {
e.preventDefault(); if (!draggedClipId) return;
e.dataTransfer.dropEffect = "move"; // Find which clip element the pointer is over
}, []); const elementsUnder = document.elementsFromPoint(e.clientX, e.clientY);
for (const el of elementsUnder) {
const handleDrop = useCallback( const clipEl = (el as HTMLElement).closest("[data-clip-id]") as HTMLElement | null;
(e: React.DragEvent, targetEdgeId: string) => { if (clipEl) {
e.preventDefault(); const targetId = clipEl.dataset.clipId!;
if (!draggedClipId || draggedClipId === targetEdgeId) { if (targetId !== draggedClipId) {
setDraggedClipId(null); setHoverClipId(targetId);
}
return; return;
} }
}
setHoverClipId(null);
}, [draggedClipId]);
const currentOrder = [...nodeData.clipOrder]; const handlePointerUp = useCallback(() => {
const draggedIndex = currentOrder.indexOf(draggedClipId); if (!draggedClipId || !hoverClipId || draggedClipId === hoverClipId) {
const targetIndex = currentOrder.indexOf(targetEdgeId); setDraggedClipId(null);
setHoverClipId(null);
return;
}
if (draggedIndex === -1 || targetIndex === -1) { const currentOrder = [...(nodeData.clipOrder || [])];
setDraggedClipId(null); const draggedIndex = currentOrder.indexOf(draggedClipId);
return; const targetIndex = currentOrder.indexOf(hoverClipId);
}
if (draggedIndex !== -1 && targetIndex !== -1) {
currentOrder.splice(draggedIndex, 1); currentOrder.splice(draggedIndex, 1);
currentOrder.splice(targetIndex, 0, draggedClipId); currentOrder.splice(targetIndex, 0, draggedClipId);
updateNodeData(id, { clipOrder: currentOrder }); updateNodeData(id, { clipOrder: currentOrder });
setDraggedClipId(null); }
},
[draggedClipId, nodeData.clipOrder, id, updateNodeData] setDraggedClipId(null);
); setHoverClipId(null);
}, [draggedClipId, hoverClipId, nodeData.clipOrder, id, updateNodeData]);
const handleRemoveClip = useCallback( const handleRemoveClip = useCallback(
(edgeId: string) => { (edgeId: string) => {
@ -404,11 +416,17 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
return ( return (
<div <div
key={clip.edgeId} key={clip.edgeId}
draggable data-clip-id={clip.edgeId}
onDragStart={(e) => handleDragStart(e, clip.edgeId)} onPointerDown={(e) => handlePointerDown(e, clip.edgeId)}
onDragOver={handleDragOver} onPointerMove={handlePointerMove}
onDrop={(e) => handleDrop(e, clip.edgeId)} onPointerUp={handlePointerUp}
className="nodrag relative w-full aspect-video bg-neutral-800 border border-neutral-600 rounded cursor-move hover:border-neutral-500 transition-colors group" className={`nodrag relative w-full aspect-video bg-neutral-800 border rounded cursor-move transition-colors group ${
draggedClipId === clip.edgeId
? "opacity-50 border-blue-500"
: hoverClipId === clip.edgeId && draggedClipId
? "border-blue-400 ring-1 ring-blue-400/50"
: "border-neutral-600 hover:border-neutral-500"
}`}
> >
{thumbnail ? ( {thumbnail ? (
<img <img

Loading…
Cancel
Save