Add store location map manager #20
@ -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 (
|
||||
<g
|
||||
key={key}
|
||||
data-object-key={key}
|
||||
className={[
|
||||
"location-map-object",
|
||||
isSelected ? "is-selected" : "",
|
||||
@ -165,9 +222,18 @@ export default function LocationMapCanvas({
|
||||
"location-map-canvas-shell",
|
||||
mode === "edit" ? "is-edit-mode" : "is-view-mode",
|
||||
editorTool === "edit" ? "is-object-tool" : "is-pan-tool",
|
||||
isPanning ? "is-panning" : "",
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
<div className="location-map-scroll">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="location-map-scroll"
|
||||
onPointerDown={startPan}
|
||||
onPointerMove={movePan}
|
||||
onPointerUp={endPan}
|
||||
onPointerCancel={endPan}
|
||||
onPointerLeave={endPan}
|
||||
>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
className={[
|
||||
|
||||
@ -203,6 +203,15 @@
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
.location-map-canvas-shell.is-pan-tool .location-map-scroll {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.location-map-canvas-shell.is-panning .location-map-scroll {
|
||||
cursor: grabbing;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.location-map-svg {
|
||||
min-width: 320px;
|
||||
min-height: 260px;
|
||||
|
||||
@ -238,6 +238,19 @@ test("admin creates a map from zones, saves a draft, and publishes it", async ({
|
||||
await expect(page.getByRole("button", { name: "Edit Objects" })).toBeVisible();
|
||||
await expect(page.locator(".location-map-pin")).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "Pan Mode" }).click();
|
||||
const bakeryPanObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first();
|
||||
const panBox = await bakeryPanObject.boundingBox();
|
||||
expect(panBox).not.toBeNull();
|
||||
if (!panBox) throw new Error("Bakery map object was not measurable");
|
||||
await page.mouse.move(panBox.x + panBox.width / 2, panBox.y + panBox.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(panBox.x + panBox.width / 2 - 80, panBox.y + panBox.height / 2 - 20, {
|
||||
steps: 8,
|
||||
});
|
||||
await page.mouse.up();
|
||||
|
||||
await page.getByRole("button", { name: "Edit Objects" }).click();
|
||||
const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first();
|
||||
await bakeryObject.click();
|
||||
await page.getByRole("textbox", { name: "Label" }).fill("Bread Wall");
|
||||
@ -246,7 +259,7 @@ test("admin creates a map from zones, saves a draft, and publishes it", async ({
|
||||
expect(savedPayload).not.toBeNull();
|
||||
expect(savedPayload?.objects).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ label: "Bread Wall", zone_id: 501 }),
|
||||
expect.objectContaining({ label: "Bread Wall", zone_id: 501, x: 28, y: 28 }),
|
||||
])
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user