grocery-app/frontend/src/hooks/useMobileMapAutoFit.js

112 lines
3.8 KiB
JavaScript

import { useCallback, useEffect, useMemo, useRef } from "react";
import { DEFAULT_MAP_SIZE, clampMapZoom } from "../lib/locationMapUtils";
const COMPACT_MAP_LAYOUT_QUERY = "(max-width: 839px)";
function getAutoFitKey(mapState, mapSize) {
return [
mapState?.location?.id || "",
mapState?.draft_map?.id || "",
mapState?.published_map?.id || "",
mapSize.width,
mapSize.height,
].join(":");
}
function getFitZoom(mapSize, scrollElement) {
const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
const isCompactLayout = typeof window !== "undefined"
&& window.matchMedia(COMPACT_MAP_LAYOUT_QUERY).matches;
const minimumWidth = isCompactLayout ? 160 : 280;
const minimumHeight = isCompactLayout ? 120 : 220;
const availableWidth = Math.max(minimumWidth, (scrollElement?.clientWidth || fallbackWidth) - 24);
const availableHeight = Math.max(minimumHeight, (scrollElement?.clientHeight || fallbackHeight) - 24);
return Number(clampMapZoom(
Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
).toFixed(2));
}
export default function useMobileMapAutoFit({
mapSize,
mapState,
mode,
setZoom,
svgRef,
zoom,
}) {
const lastAutoFitKeyRef = useRef(null);
const lastFittedZoomRef = useRef(null);
const latestZoomRef = useRef(zoom);
const autoFitKey = useMemo(
() => getAutoFitKey(mapState, mapSize),
[mapSize, mapState]
);
useEffect(() => {
latestZoomRef.current = zoom;
}, [zoom]);
const fitMapToCanvas = useCallback(() => {
const scrollElement = svgRef.current?.closest(".location-map-scroll");
const nextZoom = getFitZoom(mapSize, scrollElement);
lastFittedZoomRef.current = nextZoom;
setZoom(nextZoom);
scrollElement?.scrollTo({ left: 0, top: 0 });
}, [mapSize, setZoom, svgRef]);
useEffect(() => {
if (typeof window === "undefined") return undefined;
if (!mapState || mode === "setup") return undefined;
if (!window.matchMedia(COMPACT_MAP_LAYOUT_QUERY).matches) return undefined;
if (lastAutoFitKeyRef.current === autoFitKey) return undefined;
const frameId = window.requestAnimationFrame(() => {
if (!svgRef.current) return;
lastAutoFitKeyRef.current = autoFitKey;
fitMapToCanvas();
});
return () => window.cancelAnimationFrame(frameId);
}, [autoFitKey, fitMapToCanvas, mapState, mode, svgRef]);
useEffect(() => {
if (typeof window === "undefined") return undefined;
if (!mapState || mode === "setup") return undefined;
let frameId = null;
const mediaQuery = window.matchMedia(COMPACT_MAP_LAYOUT_QUERY);
const shouldPreserveManualZoom = () => {
const lastFittedZoom = lastFittedZoomRef.current;
return lastFittedZoom !== null && Math.abs(latestZoomRef.current - lastFittedZoom) > 0.01;
};
const scheduleFit = () => {
if (!mediaQuery.matches || shouldPreserveManualZoom()) return;
if (frameId !== null) {
window.cancelAnimationFrame(frameId);
}
frameId = window.requestAnimationFrame(() => {
frameId = null;
fitMapToCanvas();
});
};
window.addEventListener("resize", scheduleFit);
window.addEventListener("orientationchange", scheduleFit);
window.visualViewport?.addEventListener("resize", scheduleFit);
scheduleFit();
return () => {
if (frameId !== null) {
window.cancelAnimationFrame(frameId);
}
window.removeEventListener("resize", scheduleFit);
window.removeEventListener("orientationchange", scheduleFit);
window.visualViewport?.removeEventListener("resize", scheduleFit);
};
}, [fitMapToCanvas, mapState, mode]);
return fitMapToCanvas;
}