import type { ActivatedRoute , MatchingRoute , Routes } from '../types' ; import type { FSRouterHistory , LoadingListener , RequiredTitle , ResolverListener } from './types' ; import { boundMethod } from 'autobind-decorator' ; import { Action , createBrowserHistory , History as BrowserHistory , Location , LocationDescriptor , LocationDescriptorObject , LocationListener , TransitionPromptHook , UnregisterCallback } from 'history' ; import { uniqueId } from 'lodash-es' ; import { promisedEntries } from '../../utils' ; import { buildMatchers , matchRoute , resolveRoute , stringifyLocation } from './utils.web' ; import { createKey , Matchers } from './utils.base' ; import { INTERNAL , queueMethod } from './queue.decorator' ; export class History implements FSRouterHistory { private get nextLoad ( ) : Promise < void > { return new Promise ( resolve => { const remove = this . observeLoading ( ( ) => { resolve ( ) ; remove ( ) ; } ) ; } ) ; } public get length ( ) : number { return this . browserHistory . length ; } public get action ( ) : Action { return this . _action ; } public get location ( ) : Location < unknown > { return this . _location ; } private readonly matchers : Matchers = buildMatchers ( this . routes ) ; private readonly browserHistory : BrowserHistory = createBrowserHistory ( ) ; private readonly activationObservers : Map < string , ResolverListener > = new Map ( ) ; private readonly loadingObservers : Map < string , LoadingListener > = new Map ( ) ; private readonly locationObservers : Map < string , LocationListener > = new Map ( ) ; private _action : Action = this . browserHistory . action ; private _location : Location = Object . freeze ( { pathname : '__SPLASH__' , hash : '' , search : '' , state : { } , key : createKey ( ) } ) ; constructor ( private readonly routes ) { this . initialNavigation ( ) . then ( ( ) => { this . observeNavigation ( ) ; } ) . catch ( ) ; } public open ( path , state ? ) ; public open ( location ) ; @ boundMethod @ queueMethod public async open ( to , state ? ) { if ( typeof to === 'string' ) { return this . push ( to , state , INTERNAL ) ; } else { return this . push ( to , INTERNAL ) ; } } public push ( path , state ? , _internal ? ) ; public push ( location , _internal ? ) ; @ boundMethod @ queueMethod public async push ( to , state ? , _internal ? ) { if ( typeof to === 'string' ) { if ( /^\w+:\/\// . exec ( to ) ) { window . location . href = to ; } else { this . browserHistory . push ( to , state ) ; await this . nextLoad ; } } else { if ( to ?. pathname && /^\w+:\/\// . exec ( to . pathname ) ) { window . location . href = to . pathname ; } else if ( to ) { this . browserHistory . push ( to ) ; await this . nextLoad ; } } } public replace ( path , state ? ) ; public replace ( location ) ; @ boundMethod @ queueMethod public async replace ( pathOrLocation , state ? ) { if ( typeof pathOrLocation === 'string' ) { this . browserHistory . replace ( pathOrLocation , state ) ; } else { this . browserHistory . replace ( pathOrLocation ) ; } await this . nextLoad ; } @ boundMethod @ queueMethod public async go ( n ) { this . browserHistory . go ( n ) ; await this . nextLoad ; } @ boundMethod @ queueMethod public async pop ( ) { this . browserHistory . goBack ( ) ; await this . nextLoad ; } @ boundMethod @ queueMethod public async goBack ( ) { this . browserHistory . goBack ( ) ; await this . nextLoad ; } @ boundMethod @ queueMethod public async goForward ( ) { this . browserHistory . goForward ( ) ; await this . nextLoad ; } @ boundMethod public block ( prompt ? ) { return this . browserHistory . block ( prompt ) ; } @ boundMethod public listen ( listener ) { const id = uniqueId ( 'resolver-listeners' ) ; this . locationObservers . set ( id , listener ) ; return ( ) => { this . locationObservers . delete ( id ) ; } ; } @ boundMethod public observeLoading ( listener ) { const id = uniqueId ( 'loading-subscriber' ) ; this . loadingObservers . set ( id , listener ) ; return ( ) => { this . loadingObservers . delete ( id ) ; } ; } @ boundMethod public registerResolver ( id , listener ) { this . activationObservers . set ( id , listener ) ; return ( ) => { this . activationObservers . delete ( id ) ; } ; } @ boundMethod public createHref ( location ) { return this . browserHistory . createHref ( location ) ; } @ boundMethod @ queueMethod public async updateTitle ( title ) { document . title = typeof title === 'string' ? title : title . text ; } private async initialNavigation ( ) { const matchingRoute = await matchRoute ( this . matchers , stringifyLocation ( location ) ) ; if ( matchingRoute ) { await this . activateRoute ( matchingRoute , this . browserHistory . location . state ) ; this . _action = this . browserHistory . action ; this . _location = this . browserHistory . location ; this . locationObservers . forEach ( listener => { listener ( this . browserHistory . location , this . browserHistory . action ) ; } ) ; } } private async activateRoute ( matchingRoute , state ) { this . setLoading ( true ) ; const activatedRoute = await this . resolveRouteDetails ( matchingRoute , state ) ; const observer = this . activationObservers . get ( matchingRoute . id ) ; observer ?. ( activatedRoute ) ; const allObserver = this . activationObservers . get ( 'all' ) ; allObserver ?. ( activatedRoute ) ; const title = typeof matchingRoute . title === 'function' ? await matchingRoute . title ( activatedRoute ) : matchingRoute . title ; if ( title ) { document . title = title ; } this . setLoading ( false ) ; } private observeNavigation ( ) { this . browserHistory . listen ( async ( location , action ) => { const unblock = this . browserHistory . block ( true ) ; const matchingRoute = await matchRoute ( this . matchers , stringifyLocation ( location ) ) ; if ( matchingRoute ) { await this . activateRoute ( matchingRoute , location . state ) ; this . _action = action ; this . _location = location ; this . locationObservers . forEach ( listener => { listener ( location , action ) ; } ) ; window . scrollTo ( 0 , 0 ) ; this . setLoading ( false ) ; unblock ( ) ; } else { unblock ( ) ; } } ) ; } private setLoading ( loading ) { this . loadingObservers . forEach ( callback => callback ( loading ) ) ; } private async resolveRouteDetails ( matchingRoute , state ) { const resolvedData = await promisedEntries ( resolveRoute ( matchingRoute ) ) ; return { data : { ... resolvedData , ... ( typeof state === 'object' ? state : { } ) } , query : matchingRoute . query , params : matchingRoute . params , path : matchingRoute . matchedPath , loading : true } ; } }