You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					684 lines
				
				19 KiB
			
		
		
			
		
	
	
					684 lines
				
				19 KiB
			| 
											3 years ago
										 | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //#region Types and Constants
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Actions represent the type of change to a location value. | ||
|  |  */ | ||
|  | export enum Action { | ||
|  |   /** | ||
|  |    * A POP indicates a change to an arbitrary index in the history stack, such | ||
|  |    * as a back or forward navigation. It does not describe the direction of the | ||
|  |    * navigation, only that the current index changed. | ||
|  |    * | ||
|  |    * Note: This is the default action for newly created history objects. | ||
|  |    */ | ||
|  |   Pop = "POP", | ||
|  | 
 | ||
|  |   /** | ||
|  |    * A PUSH indicates a new entry being added to the history stack, such as when | ||
|  |    * a link is clicked and a new page loads. When this happens, all subsequent | ||
|  |    * entries in the stack are lost. | ||
|  |    */ | ||
|  |   Push = "PUSH", | ||
|  | 
 | ||
|  |   /** | ||
|  |    * A REPLACE indicates the entry at the current index in the history stack | ||
|  |    * being replaced by a new one. | ||
|  |    */ | ||
|  |   Replace = "REPLACE", | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * The pathname, search, and hash values of a URL. | ||
|  |  */ | ||
|  | export interface Path { | ||
|  |   /** | ||
|  |    * A URL pathname, beginning with a /. | ||
|  |    */ | ||
|  |   pathname: string; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * A URL search string, beginning with a ?. | ||
|  |    */ | ||
|  |   search: string; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * A URL fragment identifier, beginning with a #. | ||
|  |    */ | ||
|  |   hash: string; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * An entry in a history stack. A location contains information about the | ||
|  |  * URL path, as well as possibly some arbitrary state and a key. | ||
|  |  */ | ||
|  | export interface Location extends Path { | ||
|  |   /** | ||
|  |    * A value of arbitrary data associated with this location. | ||
|  |    */ | ||
|  |   state: any; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * A unique string associated with this location. May be used to safely store | ||
|  |    * and retrieve data in some other storage API, like `localStorage`. | ||
|  |    * | ||
|  |    * Note: This value is always "default" on the initial location. | ||
|  |    */ | ||
|  |   key: string; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * A change to the current location. | ||
|  |  */ | ||
|  | export interface Update { | ||
|  |   /** | ||
|  |    * The action that triggered the change. | ||
|  |    */ | ||
|  |   action: Action; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * The new location. | ||
|  |    */ | ||
|  |   location: Location; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * A function that receives notifications about location changes. | ||
|  |  */ | ||
|  | export interface Listener { | ||
|  |   (update: Update): void; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Describes a location that is the destination of some navigation, either via | ||
|  |  * `history.push` or `history.replace`. May be either a URL or the pieces of a | ||
|  |  * URL path. | ||
|  |  */ | ||
|  | export type To = string | Partial<Path>; | ||
|  | 
 | ||
|  | /** | ||
|  |  * A history is an interface to the navigation stack. The history serves as the | ||
|  |  * source of truth for the current location, as well as provides a set of | ||
|  |  * methods that may be used to change it. | ||
|  |  * | ||
|  |  * It is similar to the DOM's `window.history` object, but with a smaller, more | ||
|  |  * focused API. | ||
|  |  */ | ||
|  | export interface History { | ||
|  |   /** | ||
|  |    * The last action that modified the current location. This will always be | ||
|  |    * Action.Pop when a history instance is first created. This value is mutable. | ||
|  |    */ | ||
|  |   readonly action: Action; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * The current location. This value is mutable. | ||
|  |    */ | ||
|  |   readonly location: Location; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Returns a valid href for the given `to` value that may be used as | ||
|  |    * the value of an <a href> attribute. | ||
|  |    * | ||
|  |    * @param to - The destination URL | ||
|  |    */ | ||
|  |   createHref(to: To): string; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Encode a location the same way window.history would do (no-op for memory | ||
|  |    * history) so we ensure our PUSH/REPLACE navigations for data routers | ||
|  |    * behave the same as POP | ||
|  |    * | ||
|  |    * @param to Unencoded path | ||
|  |    */ | ||
|  |   encodeLocation(to: To): Path; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Pushes a new location onto the history stack, increasing its length by one. | ||
|  |    * If there were any entries in the stack after the current one, they are | ||
|  |    * lost. | ||
|  |    * | ||
|  |    * @param to - The new URL | ||
|  |    * @param state - Data to associate with the new location | ||
|  |    */ | ||
|  |   push(to: To, state?: any): void; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Replaces the current location in the history stack with a new one.  The | ||
|  |    * location that was replaced will no longer be available. | ||
|  |    * | ||
|  |    * @param to - The new URL | ||
|  |    * @param state - Data to associate with the new location | ||
|  |    */ | ||
|  |   replace(to: To, state?: any): void; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Navigates `n` entries backward/forward in the history stack relative to the | ||
|  |    * current index. For example, a "back" navigation would use go(-1). | ||
|  |    * | ||
|  |    * @param delta - The delta in the stack index | ||
|  |    */ | ||
|  |   go(delta: number): void; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Sets up a listener that will be called whenever the current location | ||
|  |    * changes. | ||
|  |    * | ||
|  |    * @param listener - A function that will be called when the location changes | ||
|  |    * @returns unlisten - A function that may be used to stop listening | ||
|  |    */ | ||
|  |   listen(listener: Listener): () => void; | ||
|  | } | ||
|  | 
 | ||
|  | type HistoryState = { | ||
|  |   usr: any; | ||
|  |   key?: string; | ||
|  | }; | ||
|  | 
 | ||
|  | const PopStateEventType = "popstate"; | ||
|  | //#endregion
 | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //#region Memory History
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * A user-supplied object that describes a location. Used when providing | ||
|  |  * entries to `createMemoryHistory` via its `initialEntries` option. | ||
|  |  */ | ||
|  | export type InitialEntry = string | Partial<Location>; | ||
|  | 
 | ||
|  | export type MemoryHistoryOptions = { | ||
|  |   initialEntries?: InitialEntry[]; | ||
|  |   initialIndex?: number; | ||
|  |   v5Compat?: boolean; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * A memory history stores locations in memory. This is useful in stateful | ||
|  |  * environments where there is no web browser, such as node tests or React | ||
|  |  * Native. | ||
|  |  */ | ||
|  | export interface MemoryHistory extends History { | ||
|  |   /** | ||
|  |    * The current index in the history stack. | ||
|  |    */ | ||
|  |   readonly index: number; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Memory history stores the current location in memory. It is designed for use | ||
|  |  * in stateful non-browser environments like tests and React Native. | ||
|  |  */ | ||
|  | export function createMemoryHistory( | ||
|  |   options: MemoryHistoryOptions = {} | ||
|  | ): MemoryHistory { | ||
|  |   let { initialEntries = ["/"], initialIndex, v5Compat = false } = options; | ||
|  |   let entries: Location[]; // Declare so we can access from createMemoryLocation
 | ||
|  |   entries = initialEntries.map((entry, index) => | ||
|  |     createMemoryLocation( | ||
|  |       entry, | ||
|  |       typeof entry === "string" ? null : entry.state, | ||
|  |       index === 0 ? "default" : undefined | ||
|  |     ) | ||
|  |   ); | ||
|  |   let index = clampIndex( | ||
|  |     initialIndex == null ? entries.length - 1 : initialIndex | ||
|  |   ); | ||
|  |   let action = Action.Pop; | ||
|  |   let listener: Listener | null = null; | ||
|  | 
 | ||
|  |   function clampIndex(n: number): number { | ||
|  |     return Math.min(Math.max(n, 0), entries.length - 1); | ||
|  |   } | ||
|  |   function getCurrentLocation(): Location { | ||
|  |     return entries[index]; | ||
|  |   } | ||
|  |   function createMemoryLocation( | ||
|  |     to: To, | ||
|  |     state: any = null, | ||
|  |     key?: string | ||
|  |   ): Location { | ||
|  |     let location = createLocation( | ||
|  |       entries ? getCurrentLocation().pathname : "/", | ||
|  |       to, | ||
|  |       state, | ||
|  |       key | ||
|  |     ); | ||
|  |     warning( | ||
|  |       location.pathname.charAt(0) === "/", | ||
|  |       `relative pathnames are not supported in memory history: ${JSON.stringify( | ||
|  |         to | ||
|  |       )}`
 | ||
|  |     ); | ||
|  |     return location; | ||
|  |   } | ||
|  | 
 | ||
|  |   let history: MemoryHistory = { | ||
|  |     get index() { | ||
|  |       return index; | ||
|  |     }, | ||
|  |     get action() { | ||
|  |       return action; | ||
|  |     }, | ||
|  |     get location() { | ||
|  |       return getCurrentLocation(); | ||
|  |     }, | ||
|  |     createHref(to) { | ||
|  |       return typeof to === "string" ? to : createPath(to); | ||
|  |     }, | ||
|  |     encodeLocation(to: To) { | ||
|  |       let path = typeof to === "string" ? parsePath(to) : to; | ||
|  |       return { | ||
|  |         pathname: path.pathname || "", | ||
|  |         search: path.search || "", | ||
|  |         hash: path.hash || "", | ||
|  |       }; | ||
|  |     }, | ||
|  |     push(to, state) { | ||
|  |       action = Action.Push; | ||
|  |       let nextLocation = createMemoryLocation(to, state); | ||
|  |       index += 1; | ||
|  |       entries.splice(index, entries.length, nextLocation); | ||
|  |       if (v5Compat && listener) { | ||
|  |         listener({ action, location: nextLocation }); | ||
|  |       } | ||
|  |     }, | ||
|  |     replace(to, state) { | ||
|  |       action = Action.Replace; | ||
|  |       let nextLocation = createMemoryLocation(to, state); | ||
|  |       entries[index] = nextLocation; | ||
|  |       if (v5Compat && listener) { | ||
|  |         listener({ action, location: nextLocation }); | ||
|  |       } | ||
|  |     }, | ||
|  |     go(delta) { | ||
|  |       action = Action.Pop; | ||
|  |       index = clampIndex(index + delta); | ||
|  |       if (listener) { | ||
|  |         listener({ action, location: getCurrentLocation() }); | ||
|  |       } | ||
|  |     }, | ||
|  |     listen(fn: Listener) { | ||
|  |       listener = fn; | ||
|  |       return () => { | ||
|  |         listener = null; | ||
|  |       }; | ||
|  |     }, | ||
|  |   }; | ||
|  | 
 | ||
|  |   return history; | ||
|  | } | ||
|  | //#endregion
 | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //#region Browser History
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * A browser history stores the current location in regular URLs in a web | ||
|  |  * browser environment. This is the standard for most web apps and provides the | ||
|  |  * cleanest URLs the browser's address bar. | ||
|  |  * | ||
|  |  * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory
 | ||
|  |  */ | ||
|  | export interface BrowserHistory extends UrlHistory {} | ||
|  | 
 | ||
|  | export type BrowserHistoryOptions = UrlHistoryOptions; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Browser history stores the location in regular URLs. This is the standard for | ||
|  |  * most web apps, but it requires some configuration on the server to ensure you | ||
|  |  * serve the same app at multiple URLs. | ||
|  |  * | ||
|  |  * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
 | ||
|  |  */ | ||
|  | export function createBrowserHistory( | ||
|  |   options: BrowserHistoryOptions = {} | ||
|  | ): BrowserHistory { | ||
|  |   function createBrowserLocation( | ||
|  |     window: Window, | ||
|  |     globalHistory: Window["history"] | ||
|  |   ) { | ||
|  |     let { pathname, search, hash } = window.location; | ||
|  |     return createLocation( | ||
|  |       "", | ||
|  |       { pathname, search, hash }, | ||
|  |       // state defaults to `null` because `window.history.state` does
 | ||
|  |       (globalHistory.state && globalHistory.state.usr) || null, | ||
|  |       (globalHistory.state && globalHistory.state.key) || "default" | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   function createBrowserHref(window: Window, to: To) { | ||
|  |     return typeof to === "string" ? to : createPath(to); | ||
|  |   } | ||
|  | 
 | ||
|  |   return getUrlBasedHistory( | ||
|  |     createBrowserLocation, | ||
|  |     createBrowserHref, | ||
|  |     null, | ||
|  |     options | ||
|  |   ); | ||
|  | } | ||
|  | //#endregion
 | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //#region Hash History
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * A hash history stores the current location in the fragment identifier portion | ||
|  |  * of the URL in a web browser environment. | ||
|  |  * | ||
|  |  * This is ideal for apps that do not control the server for some reason | ||
|  |  * (because the fragment identifier is never sent to the server), including some | ||
|  |  * shared hosting environments that do not provide fine-grained controls over | ||
|  |  * which pages are served at which URLs. | ||
|  |  * | ||
|  |  * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory
 | ||
|  |  */ | ||
|  | export interface HashHistory extends UrlHistory {} | ||
|  | 
 | ||
|  | export type HashHistoryOptions = UrlHistoryOptions; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Hash history stores the location in window.location.hash. This makes it ideal | ||
|  |  * for situations where you don't want to send the location to the server for | ||
|  |  * some reason, either because you do cannot configure it or the URL space is | ||
|  |  * reserved for something else. | ||
|  |  * | ||
|  |  * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
 | ||
|  |  */ | ||
|  | export function createHashHistory( | ||
|  |   options: HashHistoryOptions = {} | ||
|  | ): HashHistory { | ||
|  |   function createHashLocation( | ||
|  |     window: Window, | ||
|  |     globalHistory: Window["history"] | ||
|  |   ) { | ||
|  |     let { | ||
|  |       pathname = "/", | ||
|  |       search = "", | ||
|  |       hash = "", | ||
|  |     } = parsePath(window.location.hash.substr(1)); | ||
|  |     return createLocation( | ||
|  |       "", | ||
|  |       { pathname, search, hash }, | ||
|  |       // state defaults to `null` because `window.history.state` does
 | ||
|  |       (globalHistory.state && globalHistory.state.usr) || null, | ||
|  |       (globalHistory.state && globalHistory.state.key) || "default" | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   function createHashHref(window: Window, to: To) { | ||
|  |     let base = window.document.querySelector("base"); | ||
|  |     let href = ""; | ||
|  | 
 | ||
|  |     if (base && base.getAttribute("href")) { | ||
|  |       let url = window.location.href; | ||
|  |       let hashIndex = url.indexOf("#"); | ||
|  |       href = hashIndex === -1 ? url : url.slice(0, hashIndex); | ||
|  |     } | ||
|  | 
 | ||
|  |     return href + "#" + (typeof to === "string" ? to : createPath(to)); | ||
|  |   } | ||
|  | 
 | ||
|  |   function validateHashLocation(location: Location, to: To) { | ||
|  |     warning( | ||
|  |       location.pathname.charAt(0) === "/", | ||
|  |       `relative pathnames are not supported in hash history.push(${JSON.stringify( | ||
|  |         to | ||
|  |       )})`
 | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   return getUrlBasedHistory( | ||
|  |     createHashLocation, | ||
|  |     createHashHref, | ||
|  |     validateHashLocation, | ||
|  |     options | ||
|  |   ); | ||
|  | } | ||
|  | //#endregion
 | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //#region UTILS
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * @private | ||
|  |  */ | ||
|  | export function invariant(value: boolean, message?: string): asserts value; | ||
|  | export function invariant<T>( | ||
|  |   value: T | null | undefined, | ||
|  |   message?: string | ||
|  | ): asserts value is T; | ||
|  | export function invariant(value: any, message?: string) { | ||
|  |   if (value === false || value === null || typeof value === "undefined") { | ||
|  |     throw new Error(message); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function warning(cond: any, message: string) { | ||
|  |   if (!cond) { | ||
|  |     // eslint-disable-next-line no-console
 | ||
|  |     if (typeof console !== "undefined") console.warn(message); | ||
|  | 
 | ||
|  |     try { | ||
|  |       // Welcome to debugging history!
 | ||
|  |       //
 | ||
|  |       // This error is thrown as a convenience so you can more easily
 | ||
|  |       // find the source for a warning that appears in the console by
 | ||
|  |       // enabling "pause on exceptions" in your JavaScript debugger.
 | ||
|  |       throw new Error(message); | ||
|  |       // eslint-disable-next-line no-empty
 | ||
|  |     } catch (e) {} | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function createKey() { | ||
|  |   return Math.random().toString(36).substr(2, 8); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * For browser-based histories, we combine the state and key into an object | ||
|  |  */ | ||
|  | function getHistoryState(location: Location): HistoryState { | ||
|  |   return { | ||
|  |     usr: location.state, | ||
|  |     key: location.key, | ||
|  |   }; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Creates a Location object with a unique key from the given Path | ||
|  |  */ | ||
|  | export function createLocation( | ||
|  |   current: string | Location, | ||
|  |   to: To, | ||
|  |   state: any = null, | ||
|  |   key?: string | ||
|  | ): Readonly<Location> { | ||
|  |   let location: Readonly<Location> = { | ||
|  |     pathname: typeof current === "string" ? current : current.pathname, | ||
|  |     search: "", | ||
|  |     hash: "", | ||
|  |     ...(typeof to === "string" ? parsePath(to) : to), | ||
|  |     state, | ||
|  |     // TODO: This could be cleaned up.  push/replace should probably just take
 | ||
|  |     // full Locations now and avoid the need to run through this flow at all
 | ||
|  |     // But that's a pretty big refactor to the current test suite so going to
 | ||
|  |     // keep as is for the time being and just let any incoming keys take precedence
 | ||
|  |     key: (to && (to as Location).key) || key || createKey(), | ||
|  |   }; | ||
|  |   return location; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Creates a string URL path from the given pathname, search, and hash components. | ||
|  |  */ | ||
|  | export function createPath({ | ||
|  |   pathname = "/", | ||
|  |   search = "", | ||
|  |   hash = "", | ||
|  | }: Partial<Path>) { | ||
|  |   if (search && search !== "?") | ||
|  |     pathname += search.charAt(0) === "?" ? search : "?" + search; | ||
|  |   if (hash && hash !== "#") | ||
|  |     pathname += hash.charAt(0) === "#" ? hash : "#" + hash; | ||
|  |   return pathname; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parses a string URL path into its separate pathname, search, and hash components. | ||
|  |  */ | ||
|  | export function parsePath(path: string): Partial<Path> { | ||
|  |   let parsedPath: Partial<Path> = {}; | ||
|  | 
 | ||
|  |   if (path) { | ||
|  |     let hashIndex = path.indexOf("#"); | ||
|  |     if (hashIndex >= 0) { | ||
|  |       parsedPath.hash = path.substr(hashIndex); | ||
|  |       path = path.substr(0, hashIndex); | ||
|  |     } | ||
|  | 
 | ||
|  |     let searchIndex = path.indexOf("?"); | ||
|  |     if (searchIndex >= 0) { | ||
|  |       parsedPath.search = path.substr(searchIndex); | ||
|  |       path = path.substr(0, searchIndex); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (path) { | ||
|  |       parsedPath.pathname = path; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return parsedPath; | ||
|  | } | ||
|  | 
 | ||
|  | export function createClientSideURL(location: Location | string): URL { | ||
|  |   // window.location.origin is "null" (the literal string value) in Firefox
 | ||
|  |   // under certain conditions, notably when serving from a local HTML file
 | ||
|  |   // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
 | ||
|  |   let base = | ||
|  |     typeof window !== "undefined" && | ||
|  |     typeof window.location !== "undefined" && | ||
|  |     window.location.origin !== "null" | ||
|  |       ? window.location.origin | ||
|  |       : window.location.href; | ||
|  |   let href = typeof location === "string" ? location : createPath(location); | ||
|  |   invariant( | ||
|  |     base, | ||
|  |     `No window.location.(origin|href) available to create URL for href: ${href}` | ||
|  |   ); | ||
|  |   return new URL(href, base); | ||
|  | } | ||
|  | 
 | ||
|  | export interface UrlHistory extends History {} | ||
|  | 
 | ||
|  | export type UrlHistoryOptions = { | ||
|  |   window?: Window; | ||
|  |   v5Compat?: boolean; | ||
|  | }; | ||
|  | 
 | ||
|  | function getUrlBasedHistory( | ||
|  |   getLocation: (window: Window, globalHistory: Window["history"]) => Location, | ||
|  |   createHref: (window: Window, to: To) => string, | ||
|  |   validateLocation: ((location: Location, to: To) => void) | null, | ||
|  |   options: UrlHistoryOptions = {} | ||
|  | ): UrlHistory { | ||
|  |   let { window = document.defaultView!, v5Compat = false } = options; | ||
|  |   let globalHistory = window.history; | ||
|  |   let action = Action.Pop; | ||
|  |   let listener: Listener | null = null; | ||
|  | 
 | ||
|  |   function handlePop() { | ||
|  |     action = Action.Pop; | ||
|  |     if (listener) { | ||
|  |       listener({ action, location: history.location }); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function push(to: To, state?: any) { | ||
|  |     action = Action.Push; | ||
|  |     let location = createLocation(history.location, to, state); | ||
|  |     if (validateLocation) validateLocation(location, to); | ||
|  | 
 | ||
|  |     let historyState = getHistoryState(location); | ||
|  |     let url = history.createHref(location); | ||
|  | 
 | ||
|  |     // try...catch because iOS limits us to 100 pushState calls :/
 | ||
|  |     try { | ||
|  |       globalHistory.pushState(historyState, "", url); | ||
|  |     } catch (error) { | ||
|  |       // They are going to lose state here, but there is no real
 | ||
|  |       // way to warn them about it since the page will refresh...
 | ||
|  |       window.location.assign(url); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (v5Compat && listener) { | ||
|  |       listener({ action, location: history.location }); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function replace(to: To, state?: any) { | ||
|  |     action = Action.Replace; | ||
|  |     let location = createLocation(history.location, to, state); | ||
|  |     if (validateLocation) validateLocation(location, to); | ||
|  | 
 | ||
|  |     let historyState = getHistoryState(location); | ||
|  |     let url = history.createHref(location); | ||
|  |     globalHistory.replaceState(historyState, "", url); | ||
|  | 
 | ||
|  |     if (v5Compat && listener) { | ||
|  |       listener({ action, location: history.location }); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   let history: History = { | ||
|  |     get action() { | ||
|  |       return action; | ||
|  |     }, | ||
|  |     get location() { | ||
|  |       return getLocation(window, globalHistory); | ||
|  |     }, | ||
|  |     listen(fn: Listener) { | ||
|  |       if (listener) { | ||
|  |         throw new Error("A history only accepts one active listener"); | ||
|  |       } | ||
|  |       window.addEventListener(PopStateEventType, handlePop); | ||
|  |       listener = fn; | ||
|  | 
 | ||
|  |       return () => { | ||
|  |         window.removeEventListener(PopStateEventType, handlePop); | ||
|  |         listener = null; | ||
|  |       }; | ||
|  |     }, | ||
|  |     createHref(to) { | ||
|  |       return createHref(window, to); | ||
|  |     }, | ||
|  |     encodeLocation(to) { | ||
|  |       // Encode a Location the same way window.location would
 | ||
|  |       let url = createClientSideURL( | ||
|  |         typeof to === "string" ? to : createPath(to) | ||
|  |       ); | ||
|  |       return { | ||
|  |         pathname: url.pathname, | ||
|  |         search: url.search, | ||
|  |         hash: url.hash, | ||
|  |       }; | ||
|  |     }, | ||
|  |     push, | ||
|  |     replace, | ||
|  |     go(n) { | ||
|  |       return globalHistory.go(n); | ||
|  |     }, | ||
|  |   }; | ||
|  | 
 | ||
|  |   return history; | ||
|  | } | ||
|  | 
 | ||
|  | //#endregion
 |