Files
med-notes/.pnpm-store/v10/files/56/6b7dd8ebdbcec9474c8d4150f2c501e17a34d9094b564ccea0b088db089d567287fcb7f9c5ade641987355e3395f2118bd8cc2fbe776fe01ff2a01c58afab7
2025-05-09 05:30:08 +02:00

307 lines
7.6 KiB
Plaintext

import * as React from 'react'
import { useRouter } from './useRouter'
import type {
BlockerFnArgs,
HistoryAction,
HistoryLocation,
} from '@tanstack/history'
import type {
AnyRoute,
AnyRouter,
ParseRoute,
RegisteredRouter,
} from '@tanstack/router-core'
interface ShouldBlockFnLocation<
out TRouteId,
out TFullPath,
out TAllParams,
out TFullSearchSchema,
> {
routeId: TRouteId
fullPath: TFullPath
pathname: string
params: TAllParams
search: TFullSearchSchema
}
type AnyShouldBlockFnLocation = ShouldBlockFnLocation<any, any, any, any>
type MakeShouldBlockFnLocationUnion<
TRouter extends AnyRouter = RegisteredRouter,
TRoute extends AnyRoute = ParseRoute<TRouter['routeTree']>,
> = TRoute extends any
? ShouldBlockFnLocation<
TRoute['id'],
TRoute['fullPath'],
TRoute['types']['allParams'],
TRoute['types']['fullSearchSchema']
>
: never
type BlockerResolver<TRouter extends AnyRouter = RegisteredRouter> =
| {
status: 'blocked'
current: MakeShouldBlockFnLocationUnion<TRouter>
next: MakeShouldBlockFnLocationUnion<TRouter>
action: HistoryAction
proceed: () => void
reset: () => void
}
| {
status: 'idle'
current: undefined
next: undefined
action: undefined
proceed: undefined
reset: undefined
}
type ShouldBlockFnArgs<TRouter extends AnyRouter = RegisteredRouter> = {
current: MakeShouldBlockFnLocationUnion<TRouter>
next: MakeShouldBlockFnLocationUnion<TRouter>
action: HistoryAction
}
export type ShouldBlockFn<TRouter extends AnyRouter = RegisteredRouter> = (
args: ShouldBlockFnArgs<TRouter>,
) => boolean | Promise<boolean>
export type UseBlockerOpts<
TRouter extends AnyRouter = RegisteredRouter,
TWithResolver extends boolean = boolean,
> = {
shouldBlockFn: ShouldBlockFn<TRouter>
enableBeforeUnload?: boolean | (() => boolean)
disabled?: boolean
withResolver?: TWithResolver
}
type LegacyBlockerFn = () => Promise<any> | any
type LegacyBlockerOpts = {
blockerFn?: LegacyBlockerFn
condition?: boolean | any
}
function _resolveBlockerOpts(
opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
condition?: boolean | any,
): UseBlockerOpts {
if (opts === undefined) {
return {
shouldBlockFn: () => true,
withResolver: false,
}
}
if ('shouldBlockFn' in opts) {
return opts
}
if (typeof opts === 'function') {
const shouldBlock = Boolean(condition ?? true)
const _customBlockerFn = async () => {
if (shouldBlock) return await opts()
return false
}
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
withResolver: false,
}
}
const shouldBlock = Boolean(opts.condition ?? true)
const fn = opts.blockerFn
const _customBlockerFn = async () => {
if (shouldBlock && fn !== undefined) {
return await fn()
}
return shouldBlock
}
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
withResolver: fn === undefined,
}
}
export function useBlocker<
TRouter extends AnyRouter = RegisteredRouter,
TWithResolver extends boolean = false,
>(
opts: UseBlockerOpts<TRouter, TWithResolver>,
): TWithResolver extends true ? BlockerResolver<TRouter> : void
/**
* @deprecated Use the shouldBlockFn property instead
*/
export function useBlocker(blockerFnOrOpts?: LegacyBlockerOpts): BlockerResolver
/**
* @deprecated Use the UseBlockerOpts object syntax instead
*/
export function useBlocker(
blockerFn?: LegacyBlockerFn,
condition?: boolean | any,
): BlockerResolver
export function useBlocker(
opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
condition?: boolean | any,
): BlockerResolver | void {
const {
shouldBlockFn,
enableBeforeUnload = true,
disabled = false,
withResolver = false,
} = _resolveBlockerOpts(opts, condition)
const router = useRouter()
const { history } = router
const [resolver, setResolver] = React.useState<BlockerResolver>({
status: 'idle',
current: undefined,
next: undefined,
action: undefined,
proceed: undefined,
reset: undefined,
})
React.useEffect(() => {
const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
function getLocation(
location: HistoryLocation,
): AnyShouldBlockFnLocation {
const parsedLocation = router.parseLocation(undefined, location)
const matchedRoutes = router.getMatchedRoutes(parsedLocation)
if (matchedRoutes.foundRoute === undefined) {
throw new Error(`No route found for location ${location.href}`)
}
return {
routeId: matchedRoutes.foundRoute.id,
fullPath: matchedRoutes.foundRoute.fullPath,
pathname: parsedLocation.pathname,
params: matchedRoutes.routeParams,
search: parsedLocation.search,
}
}
const current = getLocation(blockerFnArgs.currentLocation)
const next = getLocation(blockerFnArgs.nextLocation)
const shouldBlock = await shouldBlockFn({
action: blockerFnArgs.action,
current,
next,
})
if (!withResolver) {
return shouldBlock
}
if (!shouldBlock) {
return false
}
const promise = new Promise<boolean>((resolve) => {
setResolver({
status: 'blocked',
current,
next,
action: blockerFnArgs.action,
proceed: () => resolve(false),
reset: () => resolve(true),
})
})
const canNavigateAsync = await promise
setResolver({
status: 'idle',
current: undefined,
next: undefined,
action: undefined,
proceed: undefined,
reset: undefined,
})
return canNavigateAsync
}
return disabled
? undefined
: history.block({ blockerFn: blockerFnComposed, enableBeforeUnload })
}, [
shouldBlockFn,
enableBeforeUnload,
disabled,
withResolver,
history,
router,
])
return resolver
}
const _resolvePromptBlockerArgs = (
props: PromptProps | LegacyPromptProps,
): UseBlockerOpts => {
if ('shouldBlockFn' in props) {
return { ...props }
}
const shouldBlock = Boolean(props.condition ?? true)
const fn = props.blockerFn
const _customBlockerFn = async () => {
if (shouldBlock && fn !== undefined) {
return await fn()
}
return shouldBlock
}
return {
shouldBlockFn: _customBlockerFn,
enableBeforeUnload: shouldBlock,
withResolver: fn === undefined,
}
}
export function Block<
TRouter extends AnyRouter = RegisteredRouter,
TWithResolver extends boolean = boolean,
>(opts: PromptProps<TRouter, TWithResolver>): React.ReactNode
/**
* @deprecated Use the UseBlockerOpts property instead
*/
export function Block(opts: LegacyPromptProps): React.ReactNode
export function Block(opts: PromptProps | LegacyPromptProps): React.ReactNode {
const { children, ...rest } = opts
const args = _resolvePromptBlockerArgs(rest)
const resolver = useBlocker(args)
return children
? typeof children === 'function'
? children(resolver as any)
: children
: null
}
type LegacyPromptProps = {
blockerFn?: LegacyBlockerFn
condition?: boolean | any
children?: React.ReactNode | ((params: BlockerResolver) => React.ReactNode)
}
type PromptProps<
TRouter extends AnyRouter = RegisteredRouter,
TWithResolver extends boolean = boolean,
TParams = TWithResolver extends true ? BlockerResolver<TRouter> : void,
> = UseBlockerOpts<TRouter, TWithResolver> & {
children?: React.ReactNode | ((params: TParams) => React.ReactNode)
}