import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useNavigate, type NavigateFunction, type NavigateOptions, type To } from "react-router-dom"; import Button from "./Button"; import Dialog from "./Dialog"; import DismissibleAlert from "./DismissibleAlert"; export type UnsavedNavigationAction = () => void; export type UnsavedChangesRegistration = { title?: string; message?: string; onSave: () => boolean | Promise; onDiscard?: () => void; }; export type UnsavedDraftGuardOptions = { dirty: boolean; title?: string; message?: string; onSave: () => boolean | Promise; onDiscard?: () => void; enabled?: boolean; }; type UnsavedChangesContextValue = { hasUnsavedChanges: boolean; registerUnsavedChanges: (registration: UnsavedChangesRegistration | null) => () => void; requestNavigation: (action: UnsavedNavigationAction) => void; }; const UnsavedChangesContext = createContext(null); export function UnsavedChangesProvider({ children }: {children: ReactNode;}) { const navigate = useNavigate(); const [registrations, setRegistrations] = useState>([]); const [pendingAction, setPendingAction] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(""); const registrationRef = useRef(null); const nextRegistrationId = useRef(1); const registration = registrations.length ? registrations[registrations.length - 1].registration : null; useEffect(() => { registrationRef.current = registration; }, [registration]); const hasUnsavedChanges = Boolean(registration); const registerUnsavedChanges = useCallback((next: UnsavedChangesRegistration | null) => { if (!next) return () => undefined; const id = nextRegistrationId.current; nextRegistrationId.current += 1; setRegistrations((current) => [...current, { id, registration: next }]); return () => { setRegistrations((current) => current.filter((item) => item.id !== id)); }; }, []); const proceed = useCallback((action: UnsavedNavigationAction) => { setPendingAction(null); setSaveError(""); action(); }, []); const requestNavigation = useCallback((action: UnsavedNavigationAction) => { const active = registrationRef.current; if (!active) { action(); return; } setSaveError(""); setPendingAction(() => action); }, []); useEffect(() => { function onBeforeUnload(event: BeforeUnloadEvent) { const active = registrationRef.current; if (!active) return; const message = active.message || "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"; event.preventDefault(); event.returnValue = message; return message; } window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, []); useEffect(() => { function onDocumentClick(event: MouseEvent) { if (!registrationRef.current) return; if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return; const target = event.target as Element | null; const anchor = target?.closest?.("a[href]") as HTMLAnchorElement | null; if (!anchor) return; if (anchor.target && anchor.target !== "_self") return; if (anchor.hasAttribute("download")) return; if (anchor.getAttribute("href")?.startsWith("#")) return; const destination = new URL(anchor.href, window.location.href); const current = new URL(window.location.href); if (destination.href === current.href) return; event.preventDefault(); event.stopPropagation(); requestNavigation(() => { if (destination.origin === current.origin) { navigate(`${destination.pathname}${destination.search}${destination.hash}`); } else { window.location.assign(destination.href); } }); } document.addEventListener("click", onDocumentClick, true); return () => document.removeEventListener("click", onDocumentClick, true); }, [navigate, requestNavigation]); async function handleSaveAndLeave() { const action = pendingAction; const active = registrationRef.current; if (!action || !active) return; setSaving(true); setSaveError(""); try { const ok = await active.onSave(); if (!ok) { setSaveError("i18n:govoplan-core.the_changes_could_not_be_saved_please_review_the.4615a3c6"); return; } proceed(action); } catch (err) { setSaveError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } function handleDiscardAndLeave() { const action = pendingAction; const active = registrationRef.current; if (!action) return; active?.onDiscard?.(); proceed(action); } const value = useMemo(() => ({ hasUnsavedChanges, registerUnsavedChanges, requestNavigation }), [hasUnsavedChanges, registerUnsavedChanges, requestNavigation]); return ( {children} {pendingAction && registration && setPendingAction(null)} footer={ <> }>

{registration.message ?? "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"}

{saveError && {saveError}}
}
); } const fallbackUnsavedChangesContext: UnsavedChangesContextValue = { hasUnsavedChanges: false, registerUnsavedChanges: () => () => undefined, requestNavigation: (action) => action() }; export function useUnsavedChanges() { return useContext(UnsavedChangesContext) ?? fallbackUnsavedChangesContext; } export function useRegisterUnsavedChanges(registration: UnsavedChangesRegistration | null) { const { registerUnsavedChanges } = useUnsavedChanges(); useEffect(() => { return registerUnsavedChanges(registration); }, [registerUnsavedChanges, registration]); } export function useUnsavedDraftGuard({ dirty, title = "i18n:govoplan-core.unsaved_changes.29267269", message = "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b", onSave, onDiscard, enabled = true }: UnsavedDraftGuardOptions) { const saveRef = useRef(onSave); const discardRef = useRef(onDiscard); useEffect(() => { saveRef.current = onSave; discardRef.current = onDiscard; }, [onDiscard, onSave]); const registration = useMemo(() => enabled && dirty ? { title, message, onSave: () => saveRef.current(), onDiscard: () => discardRef.current?.() } : null, [dirty, enabled, message, title]); useRegisterUnsavedChanges(registration); } export function useGuardedNavigate(): NavigateFunction { const navigate = useNavigate(); const { requestNavigation } = useUnsavedChanges(); return useCallback(((to: To | number, options?: NavigateOptions) => { requestNavigation(() => { if (typeof to === "number") { navigate(to); } else { navigate(to, options); } }); }) as NavigateFunction, [navigate, requestNavigation]); }