UNPKG

react-router

Version:
373 lines (372 loc) • 11.6 kB
/** * 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 };