mirror of https://github.com/buster-so/buster.git
222 lines
5.9 KiB
TypeScript
222 lines
5.9 KiB
TypeScript
'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<void>;
|
|
onCancel: () => Promise<void>;
|
|
onClose?: () => void;
|
|
doNotLeavePageOnOkay?: boolean;
|
|
};
|
|
|
|
export const PreventNavigation: React.FC<PreventNavigationProps> = 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 `<Link />` or `<a />`.
|
|
* @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 (
|
|
<LeavingDialog
|
|
{...props}
|
|
canceling={canceling}
|
|
okaying={okaying}
|
|
cancelText={cancelText}
|
|
okText={okText}
|
|
isOpen={leavingPage}
|
|
onClose={onClose}
|
|
noCallback={noCallback}
|
|
yesCallback={yesCallback}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
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 (
|
|
<AppModal
|
|
open={isOpen}
|
|
onClose={onClose}
|
|
header={memoizedHeader}
|
|
footer={memoizedFooter}></AppModal>
|
|
);
|
|
}
|
|
);
|
|
|
|
LeavingDialog.displayName = 'LeavingDialog';
|