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, }; }