UNPKG

@tanstack/history

Version:

Modern and scalable routing for React applications

420 lines (419 loc) 12.9 kB
const stateIndexKey = "__TSR_index"; const popStateEvent = "popstate"; const beforeUnloadEvent = "beforeunload"; function createHistory(opts) { let location = opts.getLocation(); const subscribers = /* @__PURE__ */ new Set(); const notify = (action) => { location = opts.getLocation(); subscribers.forEach((subscriber) => subscriber({ location, action })); }; const handleIndexChange = (action) => { if (opts.notifyOnIndexChange ?? true) notify(action); else location = opts.getLocation(); }; const tryNavigation = async ({ task, navigateOpts, ...actionInfo }) => { var _a, _b; const ignoreBlocker = (navigateOpts == null ? void 0 : navigateOpts.ignoreBlocker) ?? false; if (ignoreBlocker) { task(); return; } const blockers = ((_a = opts.getBlockers) == null ? void 0 : _a.call(opts)) ?? []; const isPushOrReplace = actionInfo.type === "PUSH" || actionInfo.type === "REPLACE"; if (typeof document !== "undefined" && blockers.length && isPushOrReplace) { for (const blocker of blockers) { const nextLocation = parseHref(actionInfo.path, actionInfo.state); const isBlocked = await blocker.blockerFn({ currentLocation: location, nextLocation, action: actionInfo.type }); if (isBlocked) { (_b = opts.onBlocked) == null ? void 0 : _b.call(opts); return; } } } task(); }; return { get location() { return location; }, get length() { return opts.getLength(); }, subscribers, subscribe: (cb) => { subscribers.add(cb); return () => { subscribers.delete(cb); }; }, push: (path, state, navigateOpts) => { const currentIndex = location.state[stateIndexKey]; state = assignKeyAndIndex(currentIndex + 1, state); tryNavigation({ task: () => { opts.pushState(path, state); notify({ type: "PUSH" }); }, navigateOpts, type: "PUSH", path, state }); }, replace: (path, state, navigateOpts) => { const currentIndex = location.state[stateIndexKey]; state = assignKeyAndIndex(currentIndex, state); tryNavigation({ task: () => { opts.replaceState(path, state); notify({ type: "REPLACE" }); }, navigateOpts, type: "REPLACE", path, state }); }, go: (index, navigateOpts) => { tryNavigation({ task: () => { opts.go(index); handleIndexChange({ type: "GO", index }); }, navigateOpts, type: "GO" }); }, back: (navigateOpts) => { tryNavigation({ task: () => { opts.back((navigateOpts == null ? void 0 : navigateOpts.ignoreBlocker) ?? false); handleIndexChange({ type: "BACK" }); }, navigateOpts, type: "BACK" }); }, forward: (navigateOpts) => { tryNavigation({ task: () => { opts.forward((navigateOpts == null ? void 0 : navigateOpts.ignoreBlocker) ?? false); handleIndexChange({ type: "FORWARD" }); }, navigateOpts, type: "FORWARD" }); }, canGoBack: () => location.state[stateIndexKey] !== 0, createHref: (str) => opts.createHref(str), block: (blocker) => { var _a; if (!opts.setBlockers) return () => { }; const blockers = ((_a = opts.getBlockers) == null ? void 0 : _a.call(opts)) ?? []; opts.setBlockers([...blockers, blocker]); return () => { var _a2, _b; const blockers2 = ((_a2 = opts.getBlockers) == null ? void 0 : _a2.call(opts)) ?? []; (_b = opts.setBlockers) == null ? void 0 : _b.call(opts, blockers2.filter((b) => b !== blocker)); }; }, flush: () => { var _a; return (_a = opts.flush) == null ? void 0 : _a.call(opts); }, destroy: () => { var _a; return (_a = opts.destroy) == null ? void 0 : _a.call(opts); }, notify }; } function assignKeyAndIndex(index, state) { if (!state) { state = {}; } const key = createRandomKey(); return { ...state, key, // TODO: Remove in v2 - use __TSR_key instead __TSR_key: key, [stateIndexKey]: index }; } function createBrowserHistory(opts) { var _a, _b; const win = (opts == null ? void 0 : opts.window) ?? (typeof document !== "undefined" ? window : void 0); const originalPushState = win.history.pushState; const originalReplaceState = win.history.replaceState; let blockers = []; const _getBlockers = () => blockers; const _setBlockers = (newBlockers) => blockers = newBlockers; const createHref = (opts == null ? void 0 : opts.createHref) ?? ((path) => path); const parseLocation = (opts == null ? void 0 : opts.parseLocation) ?? (() => parseHref( `${win.location.pathname}${win.location.search}${win.location.hash}`, win.history.state )); if (!((_a = win.history.state) == null ? void 0 : _a.__TSR_key) && !((_b = win.history.state) == null ? void 0 : _b.key)) { const addedKey = createRandomKey(); win.history.replaceState( { [stateIndexKey]: 0, key: addedKey, // TODO: Remove in v2 - use __TSR_key instead __TSR_key: addedKey }, "" ); } let currentLocation = parseLocation(); let rollbackLocation; let nextPopIsGo = false; let ignoreNextPop = false; let skipBlockerNextPop = false; let ignoreNextBeforeUnload = false; const getLocation = () => currentLocation; let next; let scheduled; const flush = () => { if (!next) { return; } history._ignoreSubscribers = true; (next.isPush ? win.history.pushState : win.history.replaceState)( next.state, "", next.href ); history._ignoreSubscribers = false; next = void 0; scheduled = void 0; rollbackLocation = void 0; }; const queueHistoryAction = (type, destHref, state) => { const href = createHref(destHref); if (!scheduled) { rollbackLocation = currentLocation; } currentLocation = parseHref(destHref, state); next = { href, state, isPush: (next == null ? void 0 : next.isPush) || type === "push" }; if (!scheduled) { scheduled = Promise.resolve().then(() => flush()); } }; const onPushPop = (type) => { currentLocation = parseLocation(); history.notify({ type }); }; const onPushPopEvent = async () => { if (ignoreNextPop) { ignoreNextPop = false; return; } const nextLocation = parseLocation(); const delta = nextLocation.state[stateIndexKey] - currentLocation.state[stateIndexKey]; const isForward = delta === 1; const isBack = delta === -1; const isGo = !isForward && !isBack || nextPopIsGo; nextPopIsGo = false; const action = isGo ? "GO" : isBack ? "BACK" : "FORWARD"; const notify = isGo ? { type: "GO", index: delta } : { type: isBack ? "BACK" : "FORWARD" }; if (skipBlockerNextPop) { skipBlockerNextPop = false; } else { const blockers2 = _getBlockers(); if (typeof document !== "undefined" && blockers2.length) { for (const blocker of blockers2) { const isBlocked = await blocker.blockerFn({ currentLocation, nextLocation, action }); if (isBlocked) { ignoreNextPop = true; win.history.go(1); history.notify(notify); return; } } } } currentLocation = parseLocation(); history.notify(notify); }; const onBeforeUnload = (e) => { if (ignoreNextBeforeUnload) { ignoreNextBeforeUnload = false; return; } let shouldBlock = false; const blockers2 = _getBlockers(); if (typeof document !== "undefined" && blockers2.length) { for (const blocker of blockers2) { const shouldHaveBeforeUnload = blocker.enableBeforeUnload ?? true; if (shouldHaveBeforeUnload === true) { shouldBlock = true; break; } if (typeof shouldHaveBeforeUnload === "function" && shouldHaveBeforeUnload() === true) { shouldBlock = true; break; } } } if (shouldBlock) { e.preventDefault(); return e.returnValue = ""; } return; }; const history = createHistory({ getLocation, getLength: () => win.history.length, pushState: (href, state) => queueHistoryAction("push", href, state), replaceState: (href, state) => queueHistoryAction("replace", href, state), back: (ignoreBlocker) => { if (ignoreBlocker) skipBlockerNextPop = true; ignoreNextBeforeUnload = true; return win.history.back(); }, forward: (ignoreBlocker) => { if (ignoreBlocker) skipBlockerNextPop = true; ignoreNextBeforeUnload = true; win.history.forward(); }, go: (n) => { nextPopIsGo = true; win.history.go(n); }, createHref: (href) => createHref(href), flush, destroy: () => { win.history.pushState = originalPushState; win.history.replaceState = originalReplaceState; win.removeEventListener(beforeUnloadEvent, onBeforeUnload, { capture: true }); win.removeEventListener(popStateEvent, onPushPopEvent); }, onBlocked: () => { if (rollbackLocation && currentLocation !== rollbackLocation) { currentLocation = rollbackLocation; } }, getBlockers: _getBlockers, setBlockers: _setBlockers, notifyOnIndexChange: false }); win.addEventListener(beforeUnloadEvent, onBeforeUnload, { capture: true }); win.addEventListener(popStateEvent, onPushPopEvent); win.history.pushState = function(...args) { const res = originalPushState.apply(win.history, args); if (!history._ignoreSubscribers) onPushPop("PUSH"); return res; }; win.history.replaceState = function(...args) { const res = originalReplaceState.apply(win.history, args); if (!history._ignoreSubscribers) onPushPop("REPLACE"); return res; }; return history; } function createHashHistory(opts) { const win = (opts == null ? void 0 : opts.window) ?? (typeof document !== "undefined" ? window : void 0); return createBrowserHistory({ window: win, parseLocation: () => { const hashSplit = win.location.hash.split("#").slice(1); const pathPart = hashSplit[0] ?? "/"; const searchPart = win.location.search; const hashEntries = hashSplit.slice(1); const hashPart = hashEntries.length === 0 ? "" : `#${hashEntries.join("#")}`; const hashHref = `${pathPart}${searchPart}${hashPart}`; return parseHref(hashHref, win.history.state); }, createHref: (href) => `${win.location.pathname}${win.location.search}#${href}` }); } function createMemoryHistory(opts = { initialEntries: ["/"] }) { const entries = opts.initialEntries; let index = opts.initialIndex ? Math.min(Math.max(opts.initialIndex, 0), entries.length - 1) : entries.length - 1; const states = entries.map( (_entry, index2) => assignKeyAndIndex(index2, void 0) ); const getLocation = () => parseHref(entries[index], states[index]); return createHistory({ getLocation, getLength: () => entries.length, pushState: (path, state) => { if (index < entries.length - 1) { entries.splice(index + 1); states.splice(index + 1); } states.push(state); entries.push(path); index = Math.max(entries.length - 1, 0); }, replaceState: (path, state) => { states[index] = state; entries[index] = path; }, back: () => { index = Math.max(index - 1, 0); }, forward: () => { index = Math.min(index + 1, entries.length - 1); }, go: (n) => { index = Math.min(Math.max(index + n, 0), entries.length - 1); }, createHref: (path) => path }); } function parseHref(href, state) { const hashIndex = href.indexOf("#"); const searchIndex = href.indexOf("?"); const addedKey = createRandomKey(); return { href, pathname: href.substring( 0, hashIndex > 0 ? searchIndex > 0 ? Math.min(hashIndex, searchIndex) : hashIndex : searchIndex > 0 ? searchIndex : href.length ), hash: hashIndex > -1 ? href.substring(hashIndex) : "", search: searchIndex > -1 ? href.slice(searchIndex, hashIndex === -1 ? void 0 : hashIndex) : "", state: state || { [stateIndexKey]: 0, key: addedKey, __TSR_key: addedKey } }; } function createRandomKey() { return (Math.random() + 1).toString(36).substring(7); } export { createBrowserHistory, createHashHistory, createHistory, createMemoryHistory, parseHref }; //# sourceMappingURL=index.js.map