'use client'; import { useMemoizedFn, useMount } from '@/hooks'; import { useRouter } from 'next/navigation'; import { useState, useRef, useEffect, useMemo } from 'react'; import React from 'react'; import { AppModal } from '../modal/AppModal'; type PreventNavigationProps = { isDirty: boolean; title: string; description: string; cancelText?: string; okText?: string; onOk: () => Promise; onCancel: () => Promise; onClose?: () => void; doNotLeavePageOnOkay?: boolean; }; export const PreventNavigation: React.FC = React.memo( ({ isDirty, cancelText = 'Discard changes', okText = 'Save changes', doNotLeavePageOnOkay, ...props }) => { const [canceling, setCanceling] = useState(false); const [okaying, setOkaying] = useState(false); const [leavingPage, setLeavingPage] = useState(false); const router = useRouter(); /** * Function that will be called when the user selects `yes` in the confirmation modal, * redirected to the selected page. */ const confirmationFn = useRef<() => void>(() => {}); // Used to make popstate event trigger when back button is clicked. // Without this, the popstate event will not fire because it needs there to be a href to return. /** * Used to prevent navigation when use click in navigation `` or ``. * @param e The triggered event. */ const handleClick = useMemoizedFn((event: MouseEvent) => { let target = event.target as HTMLElement; let href: string | null = null; // Traverse up the DOM tree looking for an anchor tag with href while (target && !href) { if (target instanceof HTMLAnchorElement && target.href) { href = target.href; break; } target = target.parentElement as HTMLElement; } if (isDirty) { event.preventDefault(); event.stopPropagation(); console.log(href); confirmationFn.current = () => { if (href) router.push(href); }; setLeavingPage(true); } }); /** * Used to prevent navigation when use `back` browser buttons. */ const handlePopState = useMemoizedFn(() => { if (isDirty) { window.history.pushState(null, document.title, window.location.href); confirmationFn.current = () => { console.warn('TODO - make sure we can navigate back to the correct page'); router.back(); }; setLeavingPage(true); } else { window.history.back(); } }); /** * Used to prevent navigation when reload page or navigate to another page, in diffenret origin. * @param e The triggered event. */ const handleBeforeUnload = useMemoizedFn((e: BeforeUnloadEvent) => { if (isDirty) { e.preventDefault(); e.returnValue = true; } }); useEffect(() => { /* *************************** Open listeners ************************** */ document.querySelectorAll('a').forEach((link) => { link.addEventListener('click', handleClick); }); window.addEventListener('popstate', handlePopState); window.addEventListener('beforeunload', handleBeforeUnload); /* ************** Return from useEffect closing listeners ************** */ return () => { document.querySelectorAll('a').forEach((link) => { link.removeEventListener('click', handleClick); }); window.removeEventListener('popstate', handlePopState); window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [isDirty]); const onClose = useMemoizedFn(async () => { setLeavingPage(false); await props.onClose?.(); confirmationFn.current = () => {}; }); const noCallback = useMemoizedFn(async () => { setLeavingPage(false); await props.onCancel?.(); confirmationFn.current(); confirmationFn.current = () => {}; }); const yesCallback = useMemoizedFn(async () => { await props.onOk?.(); if (!doNotLeavePageOnOkay) confirmationFn.current(); setLeavingPage(false); confirmationFn.current = () => {}; }); useMount(() => { window.history.pushState(null, document.title, window.location.href); }); if (!isDirty) return null; return ( ); } ); PreventNavigation.displayName = 'PreventNavigation'; const LeavingDialog: React.FC<{ isOpen: boolean; noCallback: () => void; yesCallback: () => void; onClose: () => void; title: string; description: string; cancelText: string; okText: string; canceling: boolean; okaying: boolean; }> = React.memo( ({ onClose, isOpen, okaying, canceling, noCallback, yesCallback, title, description, okText, cancelText }) => { const disableButtons = okaying || canceling; const memoizedHeader = useMemo(() => { return { title, description }; }, [title, description]); const memoizedFooter = useMemo(() => { return { primaryButton: { text: cancelText, onClick: noCallback, loading: canceling, disabled: disableButtons }, secondaryButton: { text: okText, onClick: yesCallback, loading: okaying, disabled: disableButtons } }; }, [okaying, canceling, disableButtons, noCallback, yesCallback, cancelText, okText]); return ( ); } ); LeavingDialog.displayName = 'LeavingDialog';