// Stops a page with unsaved edits from losing them without asking. // // Closing or reloading the tab gets the browser's own prompt. A move inside the // app goes past the nav guard, and the browser back button is caught with a // spare history entry: while there are edits the page pushes a copy of its own // URL, so back lands on the same page and the prompt can be shown. import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import { navigate } from "wouter-preact/use-browser-location"; import { setNavGuard } from "../../shell/navGuard.js"; // `to` is where the person was headed, or null for the back button. interface Pending { to: string | null; } export function useLeaveGuard(dirty: boolean): { pending: boolean; leave: () => void; stay: () => void; // Lets the page's own navigation through, such as after deleting the flow. release: () => void; } { const [pending, setPending] = useState(null); const spare = useRef(false); const released = useRef(false); useEffect(() => { if (!dirty) return; const warn = (e: BeforeUnloadEvent) => e.preventDefault(); window.addEventListener("beforeunload", warn); return () => window.removeEventListener("beforeunload", warn); }, [dirty]); useEffect(() => { if (!dirty) return; setNavGuard((to) => { if (released.current) return true; setPending({ to }); return false; }); return () => setNavGuard(null); }, [dirty]); useEffect(() => { const onPop = () => { if (!spare.current) return; if (dirty && !released.current) { // Back landed on the page again; the spare entry is used up. spare.current = false; setPending({ to: null }); } else { // Nothing left to protect: go on to where back was meant to go. spare.current = false; history.back(); } }; window.addEventListener("popstate", onPop); if (dirty && !spare.current) { history.pushState(history.state, "", location.href); spare.current = true; } return () => window.removeEventListener("popstate", onPop); }, [dirty]); const stay = useCallback(() => { if (pending?.to === null && !spare.current) { history.pushState(history.state, "", location.href); spare.current = true; } setPending(null); }, [pending]); const leave = useCallback(() => { released.current = true; setNavGuard(null); const to = pending?.to; setPending(null); if (to === null) history.back(); else if (to !== undefined) navigate(to); }, [pending]); const release = useCallback(() => { released.current = true; setNavGuard(null); }, []); return { pending: pending !== null, leave, stay, release }; }