diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx index bedce4f..c75f26a 100644 --- a/src/pages/editor/components/EditorCanvas.tsx +++ b/src/pages/editor/components/EditorCanvas.tsx @@ -24,6 +24,10 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { const layerRef = useRef(null); const activeGuideIdsRef = useRef([]); const stageWrapRef = useRef(null); + const draggingGuideRef = useRef<{ + id: string; + orientation: Guide["orientation"]; + } | null>(null); const { tool, @@ -57,6 +61,7 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { orientation: Guide["orientation"]; position: number; } | null>(null); + const cleanupRulerDragRef = useRef<(() => void) | null>(null); useEffect(() => { if (stageRef.current) { @@ -107,6 +112,7 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { }; const handleGuideDragEnd = (id: string, position: number, shouldRemove: boolean) => { + draggingGuideRef.current = null; if (shouldRemove) { removeGuide(id); return; @@ -114,6 +120,51 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { handleGuidePositionChange(id, position); }; + useEffect(() => { + const forceFinishGuideDrag = () => { + const draggingGuide = draggingGuideRef.current; + if (!draggingGuide) return; + + const stage = stageRef.current; + if (!stage) return; + + const node = stage.findOne(`#${draggingGuide.id}`) as Konva.Node | null; + if (!node) { + draggingGuideRef.current = null; + return; + } + + if (node.isDragging()) { + node.stopDrag(); + } + + const nextPosition = + draggingGuide.orientation === "vertical" + ? node.x() + : node.y(); + const shouldRemove = + draggingGuide.orientation === "vertical" + ? nextPosition < 0 || nextPosition > stageSize.width + : nextPosition < 0 || nextPosition > stageSize.height; + + handleGuideDragEnd(draggingGuide.id, nextPosition, shouldRemove); + }; + + window.addEventListener("pointerup", forceFinishGuideDrag); + window.addEventListener("mouseup", forceFinishGuideDrag); + window.addEventListener("touchend", forceFinishGuideDrag); + window.addEventListener("pointercancel", forceFinishGuideDrag); + window.addEventListener("blur", forceFinishGuideDrag); + + return () => { + window.removeEventListener("pointerup", forceFinishGuideDrag); + window.removeEventListener("mouseup", forceFinishGuideDrag); + window.removeEventListener("touchend", forceFinishGuideDrag); + window.removeEventListener("pointercancel", forceFinishGuideDrag); + window.removeEventListener("blur", forceFinishGuideDrag); + }; + }, [stageSize.height, stageSize.width]); + useEffect(() => { const handleDeleteGuide = (e: KeyboardEvent) => { if (!selectedGuideId) return; @@ -138,8 +189,21 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { }; }, [selectedGuideId]); - const startRulerGuideDrag = (orientation: Guide["orientation"]) => { - const updateFromPointer = (event: MouseEvent) => { + useEffect(() => { + return () => { + cleanupRulerDragRef.current?.(); + cleanupRulerDragRef.current = null; + }; + }, []); + + const startRulerGuideDrag = ( + orientation: Guide["orientation"], + pointerId: number | null, + ) => { + cleanupRulerDragRef.current?.(); + + const updateFromPointer = (event: PointerEvent) => { + if (pointerId !== null && event.pointerId !== pointerId) return null; const rect = stageWrapRef.current?.getBoundingClientRect(); if (!rect) return null; const rawPos = @@ -149,7 +213,7 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { return rawPos; }; - const updateDraft = (event: MouseEvent) => { + const updateDraft = (event: PointerEvent) => { const pointerPos = updateFromPointer(event); if (pointerPos === null) return; const boundedPos = @@ -159,14 +223,24 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { setDraftGuide({ orientation, position: boundedPos }); }; - const handleMouseMove = (event: MouseEvent) => { + const handlePointerMove = (event: PointerEvent) => { updateDraft(event); }; - const handleMouseUp = (event: MouseEvent) => { + const cleanup = () => { + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerUp); + window.removeEventListener("pointercancel", handlePointerCancel); + window.removeEventListener("blur", handleBlur); + cleanupRulerDragRef.current = null; + }; + + const handlePointerUp = (event: PointerEvent) => { + if (pointerId !== null && event.pointerId !== pointerId) return; const rect = stageWrapRef.current?.getBoundingClientRect(); if (!rect) { setDraftGuide(null); + cleanup(); return; } @@ -187,12 +261,26 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { } setDraftGuide(null); - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", handleMouseUp); + cleanup(); }; - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("mouseup", handleMouseUp, { once: true }); + const handlePointerCancel = (event: PointerEvent) => { + if (pointerId !== null && event.pointerId !== pointerId) return; + setDraftGuide(null); + cleanup(); + }; + + const handleBlur = () => { + setDraftGuide(null); + cleanup(); + }; + + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerUp); + window.addEventListener("pointercancel", handlePointerCancel); + window.addEventListener("blur", handleBlur); + + cleanupRulerDragRef.current = cleanup; }; const handleDragMove = (e: Konva.KonvaEventObject) => { @@ -233,18 +321,18 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
{ + onPointerDown={(e) => { e.preventDefault(); - startRulerGuideDrag("vertical"); + startRulerGuideDrag("vertical", e.pointerId); }} title="Drag to create vertical guide" />
{ + onPointerDown={(e) => { e.preventDefault(); - startRulerGuideDrag("horizontal"); + startRulerGuideDrag("horizontal", e.pointerId); }} title="Drag to create horizontal guide" /> @@ -300,6 +388,10 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { selectedGuideId={selectedGuideId} draftGuide={draftGuide} onGuideSelect={setSelectedGuideId} + onGuideDragStart={(id, orientation) => { + draggingGuideRef.current = { id, orientation }; + setSelectedGuideId(id); + }} onGuideDragEnd={handleGuideDragEnd} /> diff --git a/src/pages/editor/components/canvas/GuidesLayer.tsx b/src/pages/editor/components/canvas/GuidesLayer.tsx index 3a7d5b1..875275e 100644 --- a/src/pages/editor/components/canvas/GuidesLayer.tsx +++ b/src/pages/editor/components/canvas/GuidesLayer.tsx @@ -9,6 +9,7 @@ type GuidesLayerProps = { selectedGuideId: string | null; draftGuide: { orientation: Guide["orientation"]; position: number } | null; onGuideSelect: (id: string) => void; + onGuideDragStart: (id: string, orientation: Guide["orientation"]) => void; onGuideDragEnd: (id: string, position: number, shouldRemove: boolean) => void; }; @@ -20,8 +21,23 @@ const GuidesLayer = ({ selectedGuideId, draftGuide, onGuideSelect, + onGuideDragStart, onGuideDragEnd, }: GuidesLayerProps) => { + const finalizeGuideDrag = ( + guideId: string, + orientation: Guide["orientation"], + x: number, + y: number, + ) => { + const nextPosition = orientation === "vertical" ? x : y; + const shouldRemove = + orientation === "vertical" + ? nextPosition < 0 || nextPosition > stageWidth + : nextPosition < 0 || nextPosition > stageHeight; + onGuideDragEnd(guideId, nextPosition, shouldRemove); + }; + return ( {guides.map((guide) => { @@ -46,13 +62,21 @@ const GuidesLayer = ({ isVertical ? { x: pos.x, y: 0 } : { x: 0, y: pos.y } } onMouseDown={() => onGuideSelect(guide.id)} + onDragStart={() => onGuideDragStart(guide.id, guide.orientation)} + onDragMove={(e) => { + const nativeEvent = e.evt as MouseEvent | PointerEvent | TouchEvent; + // Safari can miss dragend/mouseup; if no pressed pointer exists, end drag immediately. + if ("buttons" in nativeEvent && nativeEvent.buttons === 0) { + const node = e.target; + if (node.isDragging()) { + node.stopDrag(); + } + finalizeGuideDrag(guide.id, guide.orientation, node.x(), node.y()); + } + }} onDragEnd={(e) => { const node = e.target; - const nextPosition = isVertical ? node.x() : node.y(); - const shouldRemove = isVertical - ? nextPosition < 0 || nextPosition > stageWidth - : nextPosition < 0 || nextPosition > stageHeight; - onGuideDragEnd(guide.id, nextPosition, shouldRemove); + finalizeGuideDrag(guide.id, guide.orientation, node.x(), node.y()); }} /> );