react-router
Version:
Declarative routing for React
373 lines (372 loc) • 11.6 kB
JavaScript
/**
* react-router v8.0.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
import { PROTOCOL_RELATIVE_URL_REGEX } from "./url.js";
//#region lib/router/history.ts
/**
* Actions represent the type of change to a location value.
*/
let Action = /* @__PURE__ */ function(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.
*/
Action["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.
*/
Action["Push"] = "PUSH";
/**
* A REPLACE indicates the entry at the current index in the history stack
* being replaced by a new one.
*/
Action["Replace"] = "REPLACE";
return Action;
}({});
const PopStateEventType = "popstate";
function isLocation(obj) {
return typeof obj === "object" && obj != null && "pathname" in obj && "search" in obj && "hash" in obj && "state" in obj && "key" in obj;
}
/**
* Memory history stores the current location in memory. It is designed for use
* in stateful non-browser environments like tests and React Native.
*/
function createMemoryHistory(options = {}) {
let { initialEntries = ["/"], initialIndex, v5Compat = false } = options;
let entries;
entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : void 0, typeof entry === "string" ? void 0 : entry.mask));
let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);
let action = "POP";
let listener = null;
function clampIndex(n) {
return Math.min(Math.max(n, 0), entries.length - 1);
}
function getCurrentLocation() {
return entries[index];
}
function createMemoryLocation(to, state = null, key, mask) {
let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key, mask);
warning(location.pathname.charAt(0) === "/", `relative pathnames are not supported in memory history: ${JSON.stringify(to)}`);
return location;
}
function createHref(to) {
return typeof to === "string" ? to : createPath(to);
}
return {
get index() {
return index;
},
get action() {
return action;
},
get location() {
return getCurrentLocation();
},
createHref,
createURL(to) {
return new URL(createHref(to), "http://localhost");
},
encodeLocation(to) {
let path = typeof to === "string" ? parsePath(to) : to;
return {
pathname: path.pathname || "",
search: path.search || "",
hash: path.hash || ""
};
},
push(to, state) {
action = "PUSH";
let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state);
index += 1;
entries.splice(index, entries.length, nextLocation);
if (v5Compat && listener) listener({
action,
location: nextLocation,
delta: 1
});
},
replace(to, state) {
action = "REPLACE";
let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state);
entries[index] = nextLocation;
if (v5Compat && listener) listener({
action,
location: nextLocation,
delta: 0
});
},
go(delta) {
action = "POP";
let nextIndex = clampIndex(index + delta);
let nextLocation = entries[nextIndex];
index = nextIndex;
if (listener) listener({
action,
location: nextLocation,
delta
});
},
listen(fn) {
listener = fn;
return () => {
listener = null;
};
}
};
}
/**
* 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
*/
function createBrowserHistory(options = {}) {
function createBrowserLocation(window, globalHistory) {
let maskedLocation = globalHistory.state?.masked;
let { pathname, search, hash } = maskedLocation || window.location;
return createLocation("", {
pathname,
search,
hash
}, globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default", maskedLocation ? {
pathname: window.location.pathname,
search: window.location.search,
hash: window.location.hash
} : void 0);
}
function createBrowserHref(window, to) {
return typeof to === "string" ? to : createPath(to);
}
return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
}
/**
* 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
*/
function createHashHistory(options = {}) {
function createHashLocation(window, globalHistory) {
let { pathname = "/", search = "", hash = "" } = parsePath(window.location.hash.substring(1));
if (!pathname.startsWith("/") && !pathname.startsWith(".")) pathname = "/" + pathname;
return createLocation("", {
pathname,
search,
hash
}, globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
}
function createHashHref(window, 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, to) {
warning(location.pathname.charAt(0) === "/", `relative pathnames are not supported in hash history.push(${JSON.stringify(to)})`);
}
return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);
}
function invariant(value, message) {
if (value === false || value === null || typeof value === "undefined") throw new Error(message);
}
function warning(cond, message) {
if (!cond) {
if (typeof console !== "undefined") console.warn(message);
try {
throw new Error(message);
} catch (e) {}
}
}
function createKey() {
return Math.random().toString(36).substring(2, 10);
}
/**
* For browser-based histories, we combine the state and key into an object
*/
function getHistoryState(location, index) {
return {
usr: location.state,
key: location.key,
idx: index,
masked: location.mask ? {
pathname: location.pathname,
search: location.search,
hash: location.hash
} : void 0
};
}
/**
* Creates a Location object with a unique key from the given Path
*/
function createLocation(current, to, state = null, key, mask) {
return {
pathname: typeof current === "string" ? current : current.pathname,
search: "",
hash: "",
...typeof to === "string" ? parsePath(to) : to,
state,
key: to && to.key || key || createKey(),
mask
};
}
/**
* Creates a string URL path from the given pathname, search, and hash components.
*
* @category Utils
*/
function createPath({ pathname = "/", search = "", hash = "" }) {
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.
*
* @category Utils
*/
function parsePath(path) {
let parsedPath = {};
if (path) {
let hashIndex = path.indexOf("#");
if (hashIndex >= 0) {
parsedPath.hash = path.substring(hashIndex);
path = path.substring(0, hashIndex);
}
let searchIndex = path.indexOf("?");
if (searchIndex >= 0) {
parsedPath.search = path.substring(searchIndex);
path = path.substring(0, searchIndex);
}
if (path) parsedPath.pathname = path;
}
return parsedPath;
}
function getUrlBasedHistory(getLocation, createHref, validateLocation, options = {}) {
let { window = document.defaultView, v5Compat = false } = options;
let globalHistory = window.history;
let action = "POP";
let listener = null;
let index = getIndex();
if (index == null) {
index = 0;
globalHistory.replaceState({
...globalHistory.state,
idx: index
}, "");
}
function getIndex() {
return (globalHistory.state || { idx: null }).idx;
}
function handlePop() {
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, state) {
action = "PUSH";
let location = isLocation(to) ? to : createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);
index = getIndex() + 1;
let historyState = getHistoryState(location, index);
let url = history.createHref(location.mask || location);
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
if (error instanceof DOMException && error.name === "DataCloneError") throw error;
window.location.assign(url);
}
if (v5Compat && listener) listener({
action,
location: history.location,
delta: 1
});
}
function replace(to, state) {
action = "REPLACE";
let location = isLocation(to) ? to : createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);
index = getIndex();
let historyState = getHistoryState(location, index);
let url = history.createHref(location.mask || location);
globalHistory.replaceState(historyState, "", url);
if (v5Compat && listener) listener({
action,
location: history.location,
delta: 0
});
}
function createURL(to) {
return createBrowserURLImpl(window, to);
}
let history = {
get action() {
return action;
},
get location() {
return getLocation(window, globalHistory);
},
listen(fn) {
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) {
let url = createURL(to);
return {
pathname: url.pathname,
search: url.search,
hash: url.hash
};
},
push,
replace,
go(n) {
return globalHistory.go(n);
}
};
return history;
}
function createBrowserURLImpl(windowImpl, to, isAbsolute = false) {
let base = "http://localhost";
if (windowImpl) base = windowImpl.location.origin !== "null" ? windowImpl.location.origin : windowImpl.location.href;
invariant(base, "No window.location.(origin|href) available to create URL");
let href = typeof to === "string" ? to : createPath(to);
href = href.replace(/ $/, "%20");
if (!isAbsolute && PROTOCOL_RELATIVE_URL_REGEX.test(href)) href = base + href;
return new URL(href, base);
}
//#endregion
export { Action, createBrowserHistory, createBrowserURLImpl, createHashHistory, createLocation, createMemoryHistory, createPath, invariant, parsePath, warning };