import * as React from "react" ; import { useSubscription } from "use-subscription" ; import { first } from "./helpers" ; import { createPath , getCurrentLocation , hasInitialLocationChanged , history , parsePath , subscribe , useLocation , } from "./history" ; import { getMatcher , match , matchAll , matchToHistoryPath } from "./matcher" ; import { ExtractRoutesParams , GetNestedRoutes , Matcher , Params , ParamsArg , PrependBasePath , Simplify , } from "./types" ; export { decodeSearch , encodeSearch } from "./search" ; export type { Location , Search } from "./types" ; const focusableElements = { A : true , INPUT : true , SELECT : true , TEXTAREA : true , } ; export const createRouter = < Routes extends Record < string , string > , BasePath extends string = string > ( routes , options = { } ) => { type RoutesWithBasePath = PrependBasePath < Routes , BasePath > ; type NestedRoutes = GetNestedRoutes < RoutesWithBasePath > ; type NestedRoutesParams = ExtractRoutesParams < NestedRoutes > ; type FiniteRoutes = Omit < RoutesWithBasePath , keyof NestedRoutes > ; type FiniteRoutesParams = ExtractRoutesParams < FiniteRoutes > ; type RoutesParams = NestedRoutesParams & FiniteRoutesParams ; const { basePath = "" , blockerMessage = "" } = options ; const matchers = { } as Record < keyof Routes , Matcher > ; const rankedMatchers = [ ] ; for ( const routeName in routes ) { if ( Object . prototype . hasOwnProperty . call ( routes , routeName ) ) { const matcher = getMatcher ( routeName , `${ basePath }/${ routes [ routeName ] }` ) ; matchers [ routeName ] = matcher ; rankedMatchers . push ( matcher ) ; } } rankedMatchers . sort ( ( matcherA , matcherB ) => matcherB . ranking - matcherA . ranking ) ; const goForward = ( ) : void => history . forward ( ) ; const goBack = ( ) : void => history . back ( ) ; const unsafeNavigate = ( url ) : void => { const { pathname = "/" , search = "" , hash = "" } = parsePath ( url ) ; history . push ( { pathname , search , hash } ) ; } ; const unsafeReplace = ( url ) => { const { pathname = "/" , search = "" , hash = "" } = parsePath ( url ) ; history . replace ( { pathname , search , hash } ) ; } ; const createURL = < RouteName extends keyof FiniteRoutes > ( routeName , ... args ) : string => createPath ( matchToHistoryPath ( matchers [ routeName ] , first ( args ) ) ) ; const navigate = < RouteName extends keyof FiniteRoutes > ( routeName , ... args ) : void => history . push ( matchToHistoryPath ( matchers [ routeName ] , first ( args ) ) ) ; const replace = < RouteName extends keyof FiniteRoutes > ( routeName , ... args ) : void => history . replace ( matchToHistoryPath ( matchers [ routeName ] , first ( args ) ) ) ; const useRoutes = < RouteName extends keyof FiniteRoutes | keyof NestedRoutes > ( routeNames , { orderBy = "desc" } = { } ) : ( RouteName extends string ? { name : RouteName ; params : Simplify < RoutesParams [ RouteName ] > ; } : never ) [ ] => { const routes = useSubscription ( React . useMemo ( ( ) => { const matchers = rankedMatchers . filter ( ( { name } ) => routeNames . includes ( name as RouteName ) ) ; return { getCurrentValue : ( ) => { const routes = matchAll ( getCurrentLocation ( ) , matchers ) ; if ( orderBy === "asc" ) { routes . reverse ( ) ; } return JSON . stringify ( routes ) ; } , subscribe , } ; } , [ JSON . stringify ( routeNames ) , orderBy ] ) ) ; return JSON . parse ( routes ) ; } ; const useRoute = < RouteName extends keyof FiniteRoutes | keyof NestedRoutes > ( routeNames ) : RouteName extends string ? { name : RouteName ; params : Simplify < RoutesParams [ RouteName ] > ; } | undefined : never => { const route = useSubscription ( React . useMemo ( ( ) => { const matchers = rankedMatchers . filter ( ( { name } ) => routeNames . includes ( name as RouteName ) ) ; return { getCurrentValue : ( ) => { const route = match ( getCurrentLocation ( ) , matchers ) ; return route ? JSON . stringify ( route ) : route ; } , subscribe , } ; } , [ JSON . stringify ( routeNames ) ] ) ) ; return route ? JSON . parse ( route ) : route ; } ; const useLink = ( { href , replace = false , target , } ) => { const active = useSubscription ( React . useMemo ( ( ) => ( { getCurrentValue : ( ) => href === getCurrentLocation ( ) . url , subscribe , } ) , [ href ] ) ) ; const shouldReplace = replace || active ; const shouldIgnoreTarget = ! target || target === "_self" ; return { active , onClick : React . useCallback ( ( event ) => { if ( ! event . defaultPrevented && shouldIgnoreTarget && event . button === 0 && ! ( event . metaKey || event . altKey || event . ctrlKey || event . shiftKey ) ) { event . preventDefault ( ) ; if ( shouldReplace ) { unsafeReplace ( href ) ; } else { unsafeNavigate ( href ) ; } } } , [ shouldReplace , shouldIgnoreTarget , href ] ) , } ; } ; const useBlocker = ( blocked , message = blockerMessage ) => { React . useEffect ( ( ) => { if ( ! blocked ) { return ; } const unblock = history . block ( ( transition ) => { if ( window . confirm ( message ) ) { unblock ( ) ; transition . retry ( ) ; } } ) ; return unblock ; } , [ blocked ] ) ; } ; const useRouteFocus = ( { route , containerRef , } ) => { const updateKey = JSON . stringify ( route ) ; React . useEffect ( ( ) => { const element = containerRef . current as HTMLElement | undefined ; if ( element && hasInitialLocationChanged ( ) ) { try { const name = element . nodeName ; if ( element . getAttribute ( "tabIndex" ) == null && focusableElements [ name ] == null ) { element . setAttribute ( "tabIndex" , "-1" ) ; } element . focus ( ) ; } catch ( _error ) { } } } , [ containerRef , updateKey ] ) ; } ; return { getLocation : getCurrentLocation , createURL , goBack , goForward , navigate , replace , subscribe , unsafeNavigate , unsafeReplace , useLink , useLocation , useRoutes , useRoute , useRouteFocus , useBlocker , } ; } ;