@tanstack/router-core
Version:
Modern and scalable routing for React applications
187 lines (186 loc) • 6.96 kB
JavaScript
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