grocery-app/frontend/src/hooks/useLocationMapViewControls.js
2026-06-15 23:55:32 -07:00

221 lines
5.4 KiB
JavaScript

import { useCallback, useEffect, useRef } from "react";
import useMobileMapAutoFit from "./useMobileMapAutoFit";
import { getObjectKey } from "../lib/locationMapUtils";
function cloneMapObjects(objects = []) {
return objects.map((object) => ({ ...object }));
}
function getHistorySnapshot(snapshot, fallbackSelectedObjectKey = null) {
if (Array.isArray(snapshot)) {
return {
objects: cloneMapObjects(snapshot),
selectedObjectKey: fallbackSelectedObjectKey,
};
}
return {
objects: cloneMapObjects(snapshot?.objects || []),
selectedObjectKey: snapshot?.selectedObjectKey ?? null,
};
}
function createHistorySnapshot(objects, selectedObjectKey) {
return {
objects: cloneMapObjects(objects),
selectedObjectKey: selectedObjectKey ?? null,
};
}
function isTextEditingTarget(target) {
return Boolean(target?.closest?.("input, textarea, select, [contenteditable]"));
}
export default function useLocationMapViewControls({
filters,
future = [],
hasUnsavedChanges,
history = [],
mapSize,
mapState,
mode,
objects,
previewDraft,
saving,
selectedObjectKey,
setFuture,
setHasUnsavedChanges,
setHistory,
setLayersOpen,
setMode,
setObjects,
setPreviewDraft,
setSelectedObjectKey,
setZoom,
svgRef,
syncMapDraftFromState,
zoom,
}) {
const shortcutControlsRef = useRef({
handleRedo: () => {},
handleUndo: () => {},
mode: "view",
saving: false,
});
useEffect(() => {
if (!selectedObjectKey) return;
const selectedMapObject = objects.find((object) => getObjectKey(object) === selectedObjectKey);
if (!selectedMapObject || !filters.showZones || selectedMapObject.visible === false) {
setSelectedObjectKey(null);
}
}, [filters.showZones, objects, selectedObjectKey, setSelectedObjectKey]);
const fitMapToCanvas = useMobileMapAutoFit({
mapSize,
mapState,
mode,
setZoom,
svgRef,
zoom,
});
const handleUndo = useCallback(() => {
if (history.length === 0) return;
const priorSnapshot = getHistorySnapshot(history[history.length - 1], selectedObjectKey);
setFuture([
createHistorySnapshot(objects, selectedObjectKey),
...future.slice(0, 19),
]);
setObjects(priorSnapshot.objects);
setSelectedObjectKey(priorSnapshot.selectedObjectKey);
setHasUnsavedChanges(history.length > 1);
setHistory(history.slice(0, -1));
}, [
future, history, objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
setObjects, setSelectedObjectKey,
]);
const handleRedo = useCallback(() => {
if (future.length === 0) return;
const nextSnapshot = getHistorySnapshot(future[0], selectedObjectKey);
setHistory([
...history.slice(-19),
createHistorySnapshot(objects, selectedObjectKey),
]);
setObjects(nextSnapshot.objects);
setSelectedObjectKey(nextSnapshot.selectedObjectKey);
setHasUnsavedChanges(true);
setFuture(future.slice(1));
}, [
future, history, objects, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory,
setObjects, setSelectedObjectKey,
]);
shortcutControlsRef.current = {
handleRedo,
handleUndo,
mode,
saving,
};
useEffect(() => {
if (typeof window === "undefined") {
return undefined;
}
const handleKeyDown = (event) => {
if (
event.defaultPrevented ||
event.altKey ||
!(event.ctrlKey || event.metaKey) ||
isTextEditingTarget(event.target)
) {
return;
}
const controls = shortcutControlsRef.current;
if (controls.mode !== "edit" || controls.saving) {
return;
}
const key = event.key.toLowerCase();
const wantsUndo = key === "z" && !event.shiftKey;
const wantsRedo = key === "y" || (key === "z" && event.shiftKey);
if (wantsUndo) {
event.preventDefault();
controls.handleUndo();
return;
}
if (wantsRedo) {
event.preventDefault();
controls.handleRedo();
}
};
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, []);
const handlePreviewDraft = useCallback(() => {
setMode("view");
setLayersOpen(false);
setPreviewDraft(true);
setSelectedObjectKey(null);
}, [setLayersOpen, setMode, setPreviewDraft, setSelectedObjectKey]);
const handleViewMode = useCallback(() => {
setMode("view");
setLayersOpen(false);
if ((mode === "edit" || previewDraft) && (hasUnsavedChanges || mapState?.draft_map)) {
setPreviewDraft(true);
return;
}
setPreviewDraft(false);
if (mapState) {
syncMapDraftFromState(mapState, "view", false);
}
}, [
hasUnsavedChanges,
mapState,
mode,
previewDraft,
setLayersOpen,
setMode,
setPreviewDraft,
setSelectedObjectKey,
syncMapDraftFromState,
]);
const handleEditMode = useCallback(() => {
setMode("edit");
setLayersOpen(false);
setPreviewDraft(true);
if (!previewDraft && mapState && !hasUnsavedChanges) {
syncMapDraftFromState(mapState, "edit", true);
}
}, [
hasUnsavedChanges,
mapState,
previewDraft,
setLayersOpen,
setMode,
setPreviewDraft,
syncMapDraftFromState,
]);
return {
handleEditMode,
handleFitMap: fitMapToCanvas,
handlePreviewDraft,
handleRedo,
handleUndo,
handleViewMode,
};
}