diff --git a/frontend/src/components/maps/LocationMapCanvas.jsx b/frontend/src/components/maps/LocationMapCanvas.jsx index 2d06ae7..1b2bacd 100644 --- a/frontend/src/components/maps/LocationMapCanvas.jsx +++ b/frontend/src/components/maps/LocationMapCanvas.jsx @@ -1,3 +1,4 @@ +import { useRef, useState } from "react"; import { clientPointToMap, getObjectKey, @@ -22,6 +23,11 @@ export default function LocationMapCanvas({ updateObjects, }) { const canEditObjects = mode === "edit" && editorTool === "edit"; + const canDragPan = mode === "view" || editorTool === "pan"; + const scrollRef = useRef(null); + const panDragRef = useRef(null); + const suppressPanClickRef = useRef(false); + const [isPanning, setIsPanning] = useState(false); const startDrag = (event, object, type) => { if (!canEditObjects || object.locked || !svgRef.current) return; @@ -71,17 +77,67 @@ export default function LocationMapCanvas({ const handleObjectClick = (event, object) => { event.stopPropagation(); + if (suppressPanClickRef.current) { + suppressPanClickRef.current = false; + return; + } if (mode === "view" || canEditObjects) { setSelectedObjectKey(getObjectKey(object)); } }; const clearSelection = () => { + if (suppressPanClickRef.current) { + suppressPanClickRef.current = false; + return; + } if (!dragState) { setSelectedObjectKey(null); } }; + const startPan = (event) => { + if (!canDragPan || !scrollRef.current || event.button !== 0) return; + const objectElement = event.target.closest?.(".location-map-object"); + event.currentTarget.setPointerCapture?.(event.pointerId); + panDragRef.current = { + startX: event.clientX, + startY: event.clientY, + scrollLeft: scrollRef.current.scrollLeft, + scrollTop: scrollRef.current.scrollTop, + objectKey: objectElement?.getAttribute("data-object-key") || null, + moved: false, + }; + setIsPanning(true); + }; + + const movePan = (event) => { + if (!panDragRef.current || !scrollRef.current) return; + const deltaX = event.clientX - panDragRef.current.startX; + const deltaY = event.clientY - panDragRef.current.startY; + if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4) { + panDragRef.current.moved = true; + event.preventDefault(); + } + scrollRef.current.scrollLeft = panDragRef.current.scrollLeft - deltaX; + scrollRef.current.scrollTop = panDragRef.current.scrollTop - deltaY; + }; + + const endPan = (event) => { + if (!panDragRef.current) return; + const wasMoved = panDragRef.current.moved; + const objectKey = panDragRef.current.objectKey; + if (!wasMoved && mode === "view" && objectKey) { + setSelectedObjectKey(objectKey); + suppressPanClickRef.current = true; + } else { + suppressPanClickRef.current = wasMoved; + } + panDragRef.current = null; + setIsPanning(false); + event.currentTarget.releasePointerCapture?.(event.pointerId); + }; + const fittedLabel = (object, fallback) => { const rawLabel = object.label || object.zone_name || fallback; const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 14)); @@ -115,6 +171,7 @@ export default function LocationMapCanvas({ return ( -
+