747 lines
21 KiB
TypeScript
747 lines
21 KiB
TypeScript
|
////////////////////////////////////////////////////////////////////////////////
|
||
|
//#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;
|
||
|
}
|
||
|
|
||
|
// TODO: (v7) Change the Location generic default from `any` to `unknown` and
|
||
|
// remove Remix `useLocation` wrapper.
|
||
|
|
||
|
/**
|
||
|
* 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<State = any> extends Path {
|
||
|
/**
|
||
|
* A value of arbitrary data associated with this location.
|
||
|
*/
|
||
|
state: State;
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
|
||
|
/**
|
||
|
* The delta between this location and the former location in the history stack
|
||
|
*/
|
||
|
delta: number | null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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`. This 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;
|
||
|
|
||
|
/**
|
||
|
* Returns a URL for the given `to` value
|
||
|
*
|
||
|
* @param to - The destination URL
|
||
|
*/
|
||
|
createURL(to: To): URL;
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
idx: number;
|
||
|
};
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
function createHref(to: To) {
|
||
|
return typeof to === "string" ? to : createPath(to);
|
||
|
}
|
||
|
|
||
|
let history: MemoryHistory = {
|
||
|
get index() {
|
||
|
return index;
|
||
|
},
|
||
|
get action() {
|
||
|
return action;
|
||
|
},
|
||
|
get location() {
|
||
|
return getCurrentLocation();
|
||
|
},
|
||
|
createHref,
|
||
|
createURL(to) {
|
||
|
return new URL(createHref(to), "http://localhost");
|
||
|
},
|
||
|
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, delta: 1 });
|
||
|
}
|
||
|
},
|
||
|
replace(to, state) {
|
||
|
action = Action.Replace;
|
||
|
let nextLocation = createMemoryLocation(to, state);
|
||
|
entries[index] = nextLocation;
|
||
|
if (v5Compat && listener) {
|
||
|
listener({ action, location: nextLocation, delta: 0 });
|
||
|
}
|
||
|
},
|
||
|
go(delta) {
|
||
|
action = Action.Pop;
|
||
|
let nextIndex = clampIndex(index + delta);
|
||
|
let nextLocation = entries[nextIndex];
|
||
|
index = nextIndex;
|
||
|
if (listener) {
|
||
|
listener({ action, location: nextLocation, delta });
|
||
|
}
|
||
|
},
|
||
|
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));
|
||
|
|
||
|
// Hash URL should always have a leading / just like window.location.pathname
|
||
|
// does, so if an app ends up at a route like /#something then we add a
|
||
|
// leading slash so all of our path-matching behaves the same as if it would
|
||
|
// in a browser router. This is particularly important when there exists a
|
||
|
// root splat route (<Route path="*">) since that matches internally against
|
||
|
// "/*" and we'd expect /#something to 404 in a hash router app.
|
||
|
if (!pathname.startsWith("/") && !pathname.startsWith(".")) {
|
||
|
pathname = "/" + pathname;
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export 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, index: number): HistoryState {
|
||
|
return {
|
||
|
usr: location.state,
|
||
|
key: location.key,
|
||
|
idx: index,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 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;
|
||
|
|
||
|
let index = getIndex()!;
|
||
|
// Index should only be null when we initialize. If not, it's because the
|
||
|
// user called history.pushState or history.replaceState directly, in which
|
||
|
// case we should log a warning as it will result in bugs.
|
||
|
if (index == null) {
|
||
|
index = 0;
|
||
|
globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
|
||
|
}
|
||
|
|
||
|
function getIndex(): number {
|
||
|
let state = globalHistory.state || { idx: null };
|
||
|
return state.idx;
|
||
|
}
|
||
|
|
||
|
function handlePop() {
|
||
|
action = Action.Pop;
|
||
|
let nextIndex = getIndex();
|
||
|
let delta = nextIndex == null ? null : nextIndex - index;
|
||
|
index = nextIndex;
|
||
|
if (listener) {
|
||
|
listener({ action, location: history.location, delta });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function push(to: To, state?: any) {
|
||
|
action = Action.Push;
|
||
|
let location = createLocation(history.location, to, state);
|
||
|
if (validateLocation) validateLocation(location, to);
|
||
|
|
||
|
index = getIndex() + 1;
|
||
|
let historyState = getHistoryState(location, index);
|
||
|
let url = history.createHref(location);
|
||
|
|
||
|
// try...catch because iOS limits us to 100 pushState calls :/
|
||
|
try {
|
||
|
globalHistory.pushState(historyState, "", url);
|
||
|
} catch (error) {
|
||
|
// If the exception is because `state` can't be serialized, let that throw
|
||
|
// outwards just like a replace call would so the dev knows the cause
|
||
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
|
||
|
// https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
|
||
|
if (error instanceof DOMException && error.name === "DataCloneError") {
|
||
|
throw 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, delta: 1 });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function replace(to: To, state?: any) {
|
||
|
action = Action.Replace;
|
||
|
let location = createLocation(history.location, to, state);
|
||
|
if (validateLocation) validateLocation(location, to);
|
||
|
|
||
|
index = getIndex();
|
||
|
let historyState = getHistoryState(location, index);
|
||
|
let url = history.createHref(location);
|
||
|
globalHistory.replaceState(historyState, "", url);
|
||
|
|
||
|
if (v5Compat && listener) {
|
||
|
listener({ action, location: history.location, delta: 0 });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createURL(to: To): 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 =
|
||
|
window.location.origin !== "null"
|
||
|
? window.location.origin
|
||
|
: window.location.href;
|
||
|
|
||
|
let href = typeof to === "string" ? to : createPath(to);
|
||
|
// Treating this as a full URL will strip any trailing spaces so we need to
|
||
|
// pre-encode them since they might be part of a matching splat param from
|
||
|
// an ancestor route
|
||
|
href = href.replace(/ $/, "%20");
|
||
|
invariant(
|
||
|
base,
|
||
|
`No window.location.(origin|href) available to create URL for href: ${href}`
|
||
|
);
|
||
|
return new URL(href, base);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
},
|
||
|
createURL,
|
||
|
encodeLocation(to) {
|
||
|
// Encode a Location the same way window.location would
|
||
|
let url = createURL(to);
|
||
|
return {
|
||
|
pathname: url.pathname,
|
||
|
search: url.search,
|
||
|
hash: url.hash,
|
||
|
};
|
||
|
},
|
||
|
push,
|
||
|
replace,
|
||
|
go(n) {
|
||
|
return globalHistory.go(n);
|
||
|
},
|
||
|
};
|
||
|
|
||
|
return history;
|
||
|
}
|
||
|
|
||
|
//#endregion
|