Skip to content

Commit 0826096

Browse files
authored
feat: hack route blocker (#236)
* chore: backup Signed-off-by: Innei <[email protected]> * fix: update Signed-off-by: Innei <[email protected]> * chore: cleanup Signed-off-by: Innei <[email protected]> --------- Signed-off-by: Innei <[email protected]>
1 parent 5861fa4 commit 0826096

File tree

5 files changed

+172
-2
lines changed

5 files changed

+172
-2
lines changed

src/components/modules/dashboard/home/Version.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export const Version = () => {
1818
</div>
1919
)
2020

21-
console.log(version, 'a')
2221
return (
2322
<div className="opacity-60">
2423
<p className="text-center">

src/components/modules/dashboard/writing/BaseWritingProvider.tsx

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { createContext, useContext, useMemo } from 'react'
1+
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
22
import { produce } from 'immer'
33
import { atom, useAtom } from 'jotai'
44
import type { PrimitiveAtom } from 'jotai'
55
import type { PropsWithChildren } from 'react'
66

7+
import { EmitKeyMap } from '~/constants/keys'
8+
import { useBeforeUnload } from '~/hooks/common/use-before-unload'
9+
710
const BaseWritingContext = createContext<PrimitiveAtom<BaseModelType>>(null!)
811

912
type BaseModelType = {
@@ -17,6 +20,22 @@ type BaseModelType = {
1720
export const BaseWritingProvider = <T extends BaseModelType>(
1821
props: { atom: PrimitiveAtom<T> } & PropsWithChildren,
1922
) => {
23+
const [isFormDirty, setIsDirty] = useState(false)
24+
useEffect(() => {
25+
const handler = () => {
26+
setIsDirty(true)
27+
}
28+
window.addEventListener(EmitKeyMap.EditDataUpdate, handler)
29+
30+
return () => {
31+
window.removeEventListener(EmitKeyMap.EditDataUpdate, handler)
32+
}
33+
}, [])
34+
useBeforeUnload(isFormDirty)
35+
36+
useBeforeUnload.forceRoute(() => {
37+
console.log('forceRoute')
38+
})
2039
return (
2140
<BaseWritingContext.Provider value={props.atom as any}>
2241
{props.children}

src/hooks/common/use-before-unload.ts

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/// useBeforeUnload.ts
2+
'use client'
3+
4+
import { useEffect, useId } from 'react'
5+
import { useRouter } from 'next/navigation'
6+
7+
let isForceRouting = false
8+
const activeIds: string[] = []
9+
let lastKnownHref: string
10+
11+
export const useBeforeUnload = (isActive = true) => {
12+
const id = useId()
13+
14+
// Handle <Link> clicks & onbeforeunload(attemptimg to close/refresh browser)
15+
useEffect(() => {
16+
if (!isActive) return
17+
lastKnownHref = window.location.href
18+
19+
activeIds.push(id)
20+
21+
const handleAnchorClick = (e: Event) => {
22+
const targetUrl = (e.currentTarget as HTMLAnchorElement).href,
23+
currentUrl = window.location.href
24+
25+
if (targetUrl !== currentUrl) {
26+
const res = beforeUnloadFn()
27+
if (!res) e.preventDefault()
28+
lastKnownHref = window.location.href
29+
}
30+
}
31+
32+
let anchorElements: HTMLAnchorElement[] = []
33+
34+
const disconnectAnchors = () => {
35+
anchorElements.forEach((anchor) => {
36+
anchor.removeEventListener('click', handleAnchorClick)
37+
})
38+
}
39+
40+
const handleMutation = () => {
41+
disconnectAnchors()
42+
43+
anchorElements = Array.from(document.querySelectorAll('a[href]'))
44+
anchorElements.forEach((anchor) => {
45+
anchor.addEventListener('click', handleAnchorClick)
46+
})
47+
}
48+
49+
const mutationObserver = new MutationObserver(handleMutation)
50+
mutationObserver.observe(document.body, { childList: true, subtree: true })
51+
addEventListener('beforeunload', beforeUnloadFn)
52+
53+
return () => {
54+
removeEventListener('beforeunload', beforeUnloadFn)
55+
disconnectAnchors()
56+
mutationObserver.disconnect()
57+
58+
activeIds.splice(activeIds.indexOf(id), 1)
59+
}
60+
}, [isActive, id])
61+
}
62+
63+
const beforeUnloadFn = (event?: BeforeUnloadEvent) => {
64+
if (isForceRouting) return true
65+
66+
const message = 'Discard unsaved changes?'
67+
68+
if (event) {
69+
event.returnValue = message
70+
return message
71+
} else {
72+
return confirm(message)
73+
}
74+
}
75+
76+
const BeforeUnloadProvider = ({ children }: React.PropsWithChildren) => {
77+
const router = useRouter()
78+
useEffect(() => {
79+
lastKnownHref = window.location.href
80+
})
81+
82+
// Hack nextjs13 popstate impl, so it will include route cancellation.
83+
// This Provider has to be rendered in the layout phase wrapping the page.
84+
useEffect(() => {
85+
let nextjsPopStateHandler: (...args: any[]) => void
86+
87+
function popStateHandler(...args: any[]) {
88+
useBeforeUnload.ensureSafeNavigation(
89+
() => {
90+
nextjsPopStateHandler(...args)
91+
lastKnownHref = window.location.href
92+
},
93+
() => {
94+
router.replace(lastKnownHref, { scroll: false })
95+
},
96+
)
97+
}
98+
99+
addEventListener('popstate', popStateHandler)
100+
const originalAddEventListener = window.addEventListener
101+
window.addEventListener = (...args: any[]) => {
102+
if (args[0] === 'popstate') {
103+
nextjsPopStateHandler = args[1]
104+
window.addEventListener = originalAddEventListener
105+
} else {
106+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
107+
// @ts-ignore
108+
originalAddEventListener(...args)
109+
}
110+
}
111+
112+
originalAddEventListener('popstate', (e) => {
113+
e.preventDefault()
114+
})
115+
116+
// window.addEventListener('popstate', () => {
117+
// history.pushState(null, '', null)
118+
// })
119+
return () => {
120+
window.addEventListener = originalAddEventListener
121+
removeEventListener('popstate', popStateHandler)
122+
}
123+
}, [])
124+
125+
return children
126+
}
127+
128+
useBeforeUnload.Provider = BeforeUnloadProvider
129+
130+
useBeforeUnload.forceRoute = async (cb: () => void | Promise<void>) => {
131+
try {
132+
isForceRouting = true
133+
await cb()
134+
} finally {
135+
isForceRouting = false
136+
}
137+
}
138+
139+
useBeforeUnload.ensureSafeNavigation = (
140+
onPerformRoute: () => void,
141+
onRouteRejected?: () => void,
142+
) => {
143+
if (activeIds.length === 0 || beforeUnloadFn()) {
144+
onPerformRoute()
145+
} else {
146+
onRouteRejected?.()
147+
}
148+
}

src/providers/root/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { PropsWithChildren } from 'react'
1111

1212
import { PeekPortal } from '~/components/modules/peek/PeekPortal'
1313
import { ModalStackProvider } from '~/components/ui/modal'
14+
import { useBeforeUnload } from '~/hooks/common/use-before-unload'
1415

1516
import { ProviderComposer } from '../../components/common/ProviderComposer'
1617
import { AuthProvider } from './auth-provider'
@@ -58,6 +59,7 @@ export function WebAppProviders({ children }: PropsWithChildren) {
5859
const dashboardContexts: JSX.Element[] = baseContexts.concat(
5960
<ReactQueryProviderForDashboard key="reactQueryProvider" />,
6061
<AuthProvider key="auth" />,
62+
<useBeforeUnload.Provider />,
6163
)
6264
export function DashboardAppProviders({ children }: PropsWithChildren) {
6365
return (

src/queries/definition/note.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMutation } from '@tanstack/react-query'
22
import dayjs from 'dayjs'
3+
import { revalidateTag } from 'next/cache'
34
import type { NoteModel, NoteWrappedPayload } from '@mx-space/api-client'
45
import type { NoteDto } from '~/models/writing'
56

@@ -92,6 +93,7 @@ export const useCreateNote = () =>
9293
})
9394
},
9495
onSuccess: () => {
96+
revalidateTag('note')
9597
toast.success('创建成功')
9698
},
9799
})

0 commit comments

Comments
 (0)