UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

187 lines (186 loc) 6.96 kB
import { functionalUpdate, isPlainObject } from "./utils.js"; import { isServer } from "@tanstack/router-core/isServer"; //#region src/scroll-restoration.ts function getSafeSessionStorage() { try { return typeof window !== "undefined" && typeof window.sessionStorage === "object" ? window.sessionStorage : void 0; } catch { return; } } var storageKey = "tsr-scroll-restoration-v1_3"; function createScrollRestorationCache() { const safeSessionStorage = getSafeSessionStorage(); if (!safeSessionStorage) return null; let state = {}; try { const parsed = JSON.parse(safeSessionStorage.getItem("tsr-scroll-restoration-v1_3") || "{}"); if (isPlainObject(parsed)) state = parsed; } catch {} const persist = () => { try { safeSessionStorage.setItem(storageKey, JSON.stringify(state)); } catch { if (process.env.NODE_ENV !== "production") console.warn("[ts-router] Could not persist scroll restoration state to sessionStorage."); } }; return { get state() { return state; }, set: (updater) => { state = functionalUpdate(updater, state) || state; }, persist }; } var scrollRestorationCache = createScrollRestorationCache(); /** * The default `getKey` function for `useScrollRestoration`. * It returns the `key` from the location state or the `href` of the location. * * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render. */ var defaultGetScrollRestorationKey = (location) => { return location.state.__TSR_key || location.href; }; function getCssSelector(el) { const path = []; let parent; while (parent = el.parentNode) { path.push(`${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`); el = parent; } return `${path.reverse().join(" > ")}`.toLowerCase(); } function getElementScrollRestorationEntry(router, options) { const restoreKey = (options.getKey || defaultGetScrollRestorationKey)(router.latestLocation); if (options.id) return scrollRestorationCache?.state[restoreKey]?.[`[${scrollRestorationIdAttribute}="${options.id}"]`]; const element = options.getElement?.(); if (!element) return; return scrollRestorationCache?.state[restoreKey]?.[element instanceof Window ? windowScrollTarget : getCssSelector(element)]; } var ignoreScroll = false; var windowScrollTarget = "window"; var scrollRestorationIdAttribute = "data-scroll-restoration-id"; function setupScrollRestoration(router, force) { if (!scrollRestorationCache && !(isServer ?? router.isServer)) return; const cache = scrollRestorationCache; if (force ?? router.options.scrollRestoration ?? false) router.isScrollRestoring = true; if ((isServer ?? router.isServer) || router.isScrollRestorationSetup || !cache) return; router.isScrollRestorationSetup = true; ignoreScroll = false; const getKey = router.options.getScrollRestorationKey || defaultGetScrollRestorationKey; const trackedScrollEntries = /* @__PURE__ */ new Map(); window.history.scrollRestoration = "manual"; const onScroll = (event) => { if (ignoreScroll || !router.isScrollRestoring) return; if (event.target === document || event.target === window) trackedScrollEntries.set(windowScrollTarget, { scrollX: window.scrollX || 0, scrollY: window.scrollY || 0 }); else { const target = event.target; trackedScrollEntries.set(target, { scrollX: target.scrollLeft || 0, scrollY: target.scrollTop || 0 }); } }; const snapshotCurrentScrollTargets = (restoreKey) => { if (!router.isScrollRestoring || !restoreKey || trackedScrollEntries.size === 0 || !cache) return; const keyEntry = cache.state[restoreKey] ||= {}; for (const [target, position] of trackedScrollEntries) { let selector; if (target === windowScrollTarget) selector = windowScrollTarget; else if (target.isConnected) { const attrId = target.getAttribute(scrollRestorationIdAttribute); selector = attrId ? `[${scrollRestorationIdAttribute}="${attrId}"]` : getCssSelector(target); } if (!selector) continue; keyEntry[selector] = position; } }; document.addEventListener("scroll", onScroll, true); router.subscribe("onBeforeLoad", (event) => { snapshotCurrentScrollTargets(event.fromLocation ? getKey(event.fromLocation) : void 0); trackedScrollEntries.clear(); }); window.addEventListener("pagehide", () => { snapshotCurrentScrollTargets(getKey(router.stores.resolvedLocation.get() ?? router.stores.location.get())); cache.persist(); }); router.subscribe("onRendered", (event) => { const cacheKey = getKey(event.toLocation); const behavior = router.options.scrollRestorationBehavior; const scrollToTopSelectors = router.options.scrollToTopSelectors; trackedScrollEntries.clear(); if (!router.resetNextScroll) { router.resetNextScroll = true; return; } if (typeof router.options.scrollRestoration === "function" && !router.options.scrollRestoration({ location: router.latestLocation })) return; ignoreScroll = true; try { const elementEntries = router.isScrollRestoring ? cache.state[cacheKey] : void 0; let restored = false; if (elementEntries) for (const elementSelector in elementEntries) { const entry = elementEntries[elementSelector]; if (!isPlainObject(entry)) continue; const { scrollX, scrollY } = entry; if (!Number.isFinite(scrollX) || !Number.isFinite(scrollY)) continue; if (elementSelector === windowScrollTarget) { window.scrollTo({ top: scrollY, left: scrollX, behavior }); restored = true; } else if (elementSelector) { let element; try { element = document.querySelector(elementSelector); } catch { continue; } if (element) { element.scrollLeft = scrollX; element.scrollTop = scrollY; restored = true; } } } if (!restored) { const hash = router.history.location.hash.slice(1); if (hash) { const hashScrollIntoViewOptions = window.history.state?.__hashScrollIntoViewOptions ?? true; if (hashScrollIntoViewOptions) { const el = document.getElementById(hash); if (el) el.scrollIntoView(hashScrollIntoViewOptions); } } else { const scrollOptions = { top: 0, left: 0, behavior }; window.scrollTo(scrollOptions); if (scrollToTopSelectors) for (const selector of scrollToTopSelectors) { if (selector === windowScrollTarget) continue; const element = typeof selector === "function" ? selector() : document.querySelector(selector); if (element) element.scrollTo(scrollOptions); } } } } finally { ignoreScroll = false; } if (router.isScrollRestoring) cache.set((state) => { state[cacheKey] ||= {}; return state; }); }); } //#endregion export { defaultGetScrollRestorationKey, getElementScrollRestorationEntry, scrollRestorationCache, setupScrollRestoration, storageKey }; //# sourceMappingURL=scroll-restoration.js.map