@tanstack/router-core 1.171.14 → 1.171.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/router.cjs +1 -4
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +1 -1
- package/dist/cjs/scroll-restoration.cjs +27 -34
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/esm/router.d.ts +1 -1
- package/dist/esm/router.js +2 -4
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.js +27 -34
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +4 -8
- package/src/scroll-restoration.ts +40 -56
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { locationHistoryActions } from "./router.js";
|
|
2
1
|
import { isServer } from "@tanstack/router-core/isServer";
|
|
3
2
|
//#region src/scroll-restoration.ts
|
|
4
3
|
function getSafeSessionStorage() {
|
|
@@ -67,11 +66,11 @@ function getElement(selector) {
|
|
|
67
66
|
} catch {}
|
|
68
67
|
}
|
|
69
68
|
function getScrollToTopElements(scrollToTopSelectors) {
|
|
70
|
-
const elements =
|
|
69
|
+
const elements = /* @__PURE__ */ new Set();
|
|
71
70
|
for (const selector of scrollToTopSelectors) {
|
|
72
71
|
if (selector === windowScrollTarget) continue;
|
|
73
72
|
const element = getElement(selector);
|
|
74
|
-
if (element) elements.
|
|
73
|
+
if (element) elements.add(element);
|
|
75
74
|
}
|
|
76
75
|
return elements;
|
|
77
76
|
}
|
|
@@ -81,35 +80,29 @@ function setupScrollRestoration(router, force) {
|
|
|
81
80
|
if (shouldSetupScrollRestoration) scroll.restoring = true;
|
|
82
81
|
if (isServer ?? router.isServer) return;
|
|
83
82
|
const getKey = router.options.getScrollRestorationKey || defaultGetScrollRestorationKey;
|
|
84
|
-
const
|
|
85
|
-
const setTrackedScrollEntry = (target, scrollX, scrollY) => {
|
|
86
|
-
const entry = trackedScrollEntries.get(target) || {};
|
|
87
|
-
entry.scrollX = scrollX;
|
|
88
|
-
entry.scrollY = scrollY;
|
|
89
|
-
trackedScrollEntries.set(target, entry);
|
|
90
|
-
};
|
|
91
|
-
const onScroll = (event) => {
|
|
92
|
-
if (ignoreScroll || !scroll.restoring) return;
|
|
93
|
-
if (event.target === document) setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY);
|
|
94
|
-
else {
|
|
95
|
-
const target = event.target;
|
|
96
|
-
setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
83
|
+
const trackedScrollTargets = /* @__PURE__ */ new Set();
|
|
99
84
|
const snapshotCurrentScrollTargets = (restoreKey) => {
|
|
100
|
-
if (!scroll.restoring) return;
|
|
101
85
|
const keyEntry = scrollRestorationCache[restoreKey] ||= {};
|
|
102
|
-
for (const
|
|
103
|
-
|
|
86
|
+
for (const target of trackedScrollTargets) if (target === document) keyEntry[windowScrollTarget] = {
|
|
87
|
+
scrollX,
|
|
88
|
+
scrollY
|
|
89
|
+
};
|
|
90
|
+
else if (target.isConnected) keyEntry[getScrollRestorationSelector(target)] = {
|
|
91
|
+
scrollX: target.scrollLeft,
|
|
92
|
+
scrollY: target.scrollTop
|
|
93
|
+
};
|
|
104
94
|
};
|
|
105
95
|
if (shouldSetupScrollRestoration && !scroll.restoration) {
|
|
106
96
|
scroll.restoration = true;
|
|
107
97
|
ignoreScroll = false;
|
|
108
98
|
history.scrollRestoration = "manual";
|
|
109
|
-
document.addEventListener("scroll",
|
|
99
|
+
document.addEventListener("scroll", (event) => {
|
|
100
|
+
if (ignoreScroll) return;
|
|
101
|
+
trackedScrollTargets.add(event.target);
|
|
102
|
+
}, true);
|
|
110
103
|
router.subscribe("onBeforeLoad", (event) => {
|
|
111
104
|
if (event.fromLocation) snapshotCurrentScrollTargets(getKey(event.fromLocation));
|
|
112
|
-
|
|
105
|
+
trackedScrollTargets.clear();
|
|
113
106
|
});
|
|
114
107
|
addEventListener("pagehide", () => {
|
|
115
108
|
snapshotCurrentScrollTargets(getKey(router.stores.resolvedLocation.get() ?? router.stores.location.get()));
|
|
@@ -122,9 +115,11 @@ function setupScrollRestoration(router, force) {
|
|
|
122
115
|
const behavior = router.options.scrollRestorationBehavior;
|
|
123
116
|
const scrollToTopSelectors = router.options.scrollToTopSelectors;
|
|
124
117
|
const shouldResetScroll = scroll.next;
|
|
118
|
+
const hashNavigation = scroll.hash;
|
|
125
119
|
let scrollToTopElements;
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
trackedScrollTargets.clear();
|
|
121
|
+
scroll.next = true;
|
|
122
|
+
scroll.hash = false;
|
|
128
123
|
if (typeof router.options.scrollRestoration === "function" && !router.options.scrollRestoration({ location: router.latestLocation })) return;
|
|
129
124
|
const cacheKey = getKey(event.toLocation);
|
|
130
125
|
const fromCacheKey = event.fromLocation && getKey(event.fromLocation);
|
|
@@ -140,7 +135,7 @@ function setupScrollRestoration(router, force) {
|
|
|
140
135
|
if (!element) continue;
|
|
141
136
|
if (shouldResetScroll && scrollToTopSelectors) {
|
|
142
137
|
scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors);
|
|
143
|
-
if (scrollToTopElements.
|
|
138
|
+
if (scrollToTopElements.has(element)) continue;
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
141
|
if (!toElementEntries) toElementEntries = scrollRestorationCache[cacheKey] = {};
|
|
@@ -154,8 +149,8 @@ function setupScrollRestoration(router, force) {
|
|
|
154
149
|
const hashScrollIntoViewOptions = event.toLocation.state.__hashScrollIntoViewOptions ?? true;
|
|
155
150
|
let windowRestored = false;
|
|
156
151
|
if (shouldResetScroll) {
|
|
157
|
-
|
|
158
|
-
const skipWindowRestore = hash && hashScrollIntoViewOptions &&
|
|
152
|
+
if (!hash && scrollToTopSelectors) scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors);
|
|
153
|
+
const skipWindowRestore = hash && hashScrollIntoViewOptions && hashNavigation;
|
|
159
154
|
const elementEntries = scroll.restoring ? scrollRestorationCache[cacheKey] : void 0;
|
|
160
155
|
if (elementEntries) for (const elementSelector in elementEntries) {
|
|
161
156
|
const { scrollX, scrollY } = elementEntries[elementSelector];
|
|
@@ -172,20 +167,18 @@ function setupScrollRestoration(router, force) {
|
|
|
172
167
|
if (element) {
|
|
173
168
|
element.scrollLeft = scrollX;
|
|
174
169
|
element.scrollTop = scrollY;
|
|
170
|
+
scrollToTopElements?.delete(element);
|
|
175
171
|
}
|
|
176
172
|
}
|
|
177
173
|
}
|
|
178
|
-
if (!
|
|
174
|
+
if (!hash) {
|
|
179
175
|
const scrollOptions = {
|
|
180
176
|
top: 0,
|
|
181
177
|
left: 0,
|
|
182
178
|
behavior
|
|
183
179
|
};
|
|
184
|
-
scrollTo(scrollOptions);
|
|
185
|
-
if (
|
|
186
|
-
scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors);
|
|
187
|
-
for (const element of scrollToTopElements) element.scrollTo(scrollOptions);
|
|
188
|
-
}
|
|
180
|
+
if (!windowRestored) scrollTo(scrollOptions);
|
|
181
|
+
if (scrollToTopElements) for (const element of scrollToTopElements) element.scrollTo(scrollOptions);
|
|
189
182
|
}
|
|
190
183
|
}
|
|
191
184
|
if (!windowRestored && hash && hashScrollIntoViewOptions) document.getElementById(hash)?.scrollIntoView(hashScrollIntoViewOptions);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-restoration.js","names":[],"sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { locationHistoryActions } from './router'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\ntype ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\ntype ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n // Accessing sessionStorage itself can throw SecurityError in locked-down\n // contexts, e.g. sandboxed/opaque origins or blocked storage policies.\n return sessionStorage\n } catch {\n return\n }\n}\n\n// SessionStorage key used to store scroll positions across navigations.\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\nconst safeSessionStorage = getSafeSessionStorage()\n\nfunction createScrollRestorationCache() {\n try {\n return JSON.parse(\n safeSessionStorage?.getItem(storageKey) || '{}',\n ) as ScrollRestorationByKey\n } catch {\n // ignore invalid session storage payloads\n return {}\n }\n}\n\nfunction persistScrollRestorationCache() {\n try {\n safeSessionStorage?.setItem(\n storageKey,\n JSON.stringify(scrollRestorationCache),\n )\n } catch {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n }\n}\n\nconst scrollRestorationCache = /* @__PURE__ */ createScrollRestorationCache()\nconst scrollRestorationIdAttribute = 'data-scroll-restoration-id'\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\nfunction getScrollRestorationSelector(element: Element): string {\n const attrId = element.getAttribute(scrollRestorationIdAttribute)\n if (attrId) {\n return `[${scrollRestorationIdAttribute}=\"${attrId}\"]`\n }\n\n let selector = ''\n let el: any = element\n let parent: HTMLElement\n\n while ((parent = el.parentNode)) {\n let index = 1\n let sibling = el\n while ((sibling = sibling.previousElementSibling)) {\n index++\n }\n\n const part = `${el.localName}:nth-child(${index})`\n selector = selector ? `${part} > ${selector}` : part\n el = parent\n }\n\n return selector\n}\n\nexport function getElementScrollRestorationEntry(\n router: AnyRouter,\n options: (\n | {\n id: string\n getElement?: () => Window | Element | undefined | null\n }\n | {\n id?: string\n getElement: () => Window | Element | undefined | null\n }\n ) & {\n getKey?: (location: ParsedLocation) => string\n },\n): ScrollRestorationEntry | undefined {\n const getKey = options.getKey || defaultGetScrollRestorationKey\n const restoreKey = getKey(router.latestLocation)\n const entries = scrollRestorationCache[restoreKey]\n\n if (!entries) {\n return\n }\n\n if (options.id) {\n return entries[`[${scrollRestorationIdAttribute}=\"${options.id}\"]`]\n }\n\n const element = options.getElement?.()\n if (!element) {\n return\n }\n\n return entries[\n element === window\n ? windowScrollTarget\n : getScrollRestorationSelector(element as Element)\n ]\n}\n\nlet ignoreScroll = false\nconst windowScrollTarget = 'window'\ntype ScrollTarget = typeof windowScrollTarget | Element\n\nfunction getElement(selector: string | (() => Element | null | undefined)) {\n try {\n return typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n } catch {}\n return\n}\n\nfunction getScrollToTopElements(\n scrollToTopSelectors: NonNullable<\n AnyRouter['options']['scrollToTopSelectors']\n >,\n): Array<Element> {\n const elements: Array<Element> = []\n\n for (const selector of scrollToTopSelectors) {\n if (selector === windowScrollTarget) {\n continue\n }\n\n const element = getElement(selector)\n if (element) {\n elements.push(element)\n }\n }\n\n return elements\n}\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n // Keep hash/top scrolling active even when sessionStorage is unavailable.\n const shouldSetupScrollRestoration = force ?? router.options.scrollRestoration\n const scroll = router._scroll\n\n if (shouldSetupScrollRestoration) {\n scroll.restoring = true\n }\n\n if (isServer ?? router.isServer) {\n return\n }\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()\n const setTrackedScrollEntry = (\n target: ScrollTarget,\n scrollX: number,\n scrollY: number,\n ) => {\n const entry =\n trackedScrollEntries.get(target) || ({} as ScrollRestorationEntry)\n entry.scrollX = scrollX\n entry.scrollY = scrollY\n trackedScrollEntries.set(target, entry)\n }\n\n const onScroll = (event: Event) => {\n if (ignoreScroll || !scroll.restoring) {\n return\n }\n\n if (event.target === document) {\n setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY)\n } else {\n const target = event.target as Element\n setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop)\n }\n }\n\n // Snapshot the current page's tracked scroll targets before navigation or unload.\n const snapshotCurrentScrollTargets = (restoreKey: string) => {\n if (!scroll.restoring) {\n return\n }\n\n const keyEntry = (scrollRestorationCache[restoreKey] ||=\n {} as ScrollRestorationByElement)\n\n for (const [target, position] of trackedScrollEntries) {\n if (target === windowScrollTarget) {\n keyEntry[windowScrollTarget] = position\n } else if (target.isConnected) {\n keyEntry[getScrollRestorationSelector(target)] = position\n }\n }\n }\n\n if (shouldSetupScrollRestoration && !scroll.restoration) {\n scroll.restoration = true\n ignoreScroll = false\n\n history.scrollRestoration = 'manual'\n\n document.addEventListener('scroll', onScroll, true)\n router.subscribe('onBeforeLoad', (event) => {\n if (event.fromLocation) {\n snapshotCurrentScrollTargets(getKey(event.fromLocation))\n }\n trackedScrollEntries.clear()\n })\n addEventListener('pagehide', () => {\n snapshotCurrentScrollTargets(\n getKey(\n router.stores.resolvedLocation.get() ?? router.stores.location.get(),\n ),\n )\n persistScrollRestorationCache()\n })\n }\n\n if (scroll.reset) {\n return\n }\n\n scroll.reset = true\n\n // Restore destination scroll after the new route has rendered.\n router.subscribe('onRendered', (event) => {\n const behavior = router.options.scrollRestorationBehavior\n const scrollToTopSelectors = router.options.scrollToTopSelectors\n const shouldResetScroll = scroll.next\n let scrollToTopElements: Array<Element> | undefined\n trackedScrollEntries.clear()\n\n if (!shouldResetScroll) {\n scroll.next = true\n }\n\n if (\n typeof router.options.scrollRestoration === 'function' &&\n !router.options.scrollRestoration({ location: router.latestLocation })\n ) {\n return\n }\n\n const cacheKey = getKey(event.toLocation)\n const fromCacheKey = event.fromLocation && getKey(event.fromLocation)\n\n if (scroll.restoring && fromCacheKey && fromCacheKey !== cacheKey) {\n const fromElementEntries = scrollRestorationCache[fromCacheKey]\n\n if (fromElementEntries) {\n let toElementEntries = scrollRestorationCache[cacheKey]\n\n for (const elementSelector in fromElementEntries) {\n if (elementSelector === windowScrollTarget) {\n if (shouldResetScroll) {\n continue\n }\n } else {\n const element = getElement(elementSelector)\n if (!element) {\n continue\n }\n\n if (shouldResetScroll && scrollToTopSelectors) {\n scrollToTopElements ??=\n getScrollToTopElements(scrollToTopSelectors)\n if (scrollToTopElements.includes(element)) {\n continue\n }\n }\n }\n\n if (!toElementEntries) {\n toElementEntries = scrollRestorationCache[cacheKey] =\n {} as ScrollRestorationByElement\n }\n\n toElementEntries[elementSelector] ??=\n fromElementEntries[elementSelector]!\n }\n }\n }\n\n ignoreScroll = true\n\n try {\n const hash = event.toLocation.hash\n const hashScrollIntoViewOptions =\n event.toLocation.state.__hashScrollIntoViewOptions ?? true\n let windowRestored = false\n\n if (shouldResetScroll) {\n const action = locationHistoryActions.get(event.toLocation)\n const skipWindowRestore =\n hash &&\n hashScrollIntoViewOptions &&\n (action === 'PUSH' || action === 'REPLACE')\n\n const elementEntries = scroll.restoring\n ? scrollRestorationCache[cacheKey]\n : undefined\n\n if (elementEntries) {\n for (const elementSelector in elementEntries) {\n const { scrollX, scrollY } = elementEntries[elementSelector]!\n\n if (elementSelector === windowScrollTarget) {\n if (skipWindowRestore) {\n continue\n }\n\n scrollTo({\n top: scrollY,\n left: scrollX,\n behavior,\n })\n windowRestored = true\n } else {\n const element = getElement(elementSelector)\n if (element) {\n element.scrollLeft = scrollX\n element.scrollTop = scrollY\n }\n }\n }\n }\n\n if (!windowRestored && !hash) {\n const scrollOptions = {\n top: 0,\n left: 0,\n behavior,\n }\n\n scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)\n for (const element of scrollToTopElements) {\n element.scrollTo(scrollOptions)\n }\n }\n }\n }\n\n if (!windowRestored && hash && hashScrollIntoViewOptions) {\n document.getElementById(hash)?.scrollIntoView(hashScrollIntoViewOptions)\n }\n } finally {\n ignoreScroll = false\n }\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scroll-restoration.js","names":[],"sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\ntype ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\ntype ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n // Accessing sessionStorage itself can throw SecurityError in locked-down\n // contexts, e.g. sandboxed/opaque origins or blocked storage policies.\n return sessionStorage\n } catch {\n return\n }\n}\n\n// SessionStorage key used to store scroll positions across navigations.\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\nconst safeSessionStorage = getSafeSessionStorage()\n\nfunction createScrollRestorationCache() {\n try {\n return JSON.parse(\n safeSessionStorage?.getItem(storageKey) || '{}',\n ) as ScrollRestorationByKey\n } catch {\n // ignore invalid session storage payloads\n return {}\n }\n}\n\nfunction persistScrollRestorationCache() {\n try {\n safeSessionStorage?.setItem(\n storageKey,\n JSON.stringify(scrollRestorationCache),\n )\n } catch {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n }\n}\n\nconst scrollRestorationCache = /* @__PURE__ */ createScrollRestorationCache()\nconst scrollRestorationIdAttribute = 'data-scroll-restoration-id'\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\nfunction getScrollRestorationSelector(element: Element): string {\n const attrId = element.getAttribute(scrollRestorationIdAttribute)\n if (attrId) {\n return `[${scrollRestorationIdAttribute}=\"${attrId}\"]`\n }\n\n let selector = ''\n let el: any = element\n let parent: HTMLElement\n\n while ((parent = el.parentNode)) {\n let index = 1\n let sibling = el\n while ((sibling = sibling.previousElementSibling)) {\n index++\n }\n\n const part = `${el.localName}:nth-child(${index})`\n selector = selector ? `${part} > ${selector}` : part\n el = parent\n }\n\n return selector\n}\n\nexport function getElementScrollRestorationEntry(\n router: AnyRouter,\n options: (\n | {\n id: string\n getElement?: () => Window | Element | undefined | null\n }\n | {\n id?: string\n getElement: () => Window | Element | undefined | null\n }\n ) & {\n getKey?: (location: ParsedLocation) => string\n },\n): ScrollRestorationEntry | undefined {\n const getKey = options.getKey || defaultGetScrollRestorationKey\n const restoreKey = getKey(router.latestLocation)\n const entries = scrollRestorationCache[restoreKey]\n\n if (!entries) {\n return\n }\n\n if (options.id) {\n return entries[`[${scrollRestorationIdAttribute}=\"${options.id}\"]`]\n }\n\n const element = options.getElement?.()\n if (!element) {\n return\n }\n\n return entries[\n element === window\n ? windowScrollTarget\n : getScrollRestorationSelector(element as Element)\n ]\n}\n\nlet ignoreScroll = false\nconst windowScrollTarget = 'window'\n\nfunction getElement(selector: string | (() => Element | null | undefined)) {\n try {\n return typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n } catch {}\n return\n}\n\nfunction getScrollToTopElements(\n scrollToTopSelectors: NonNullable<\n AnyRouter['options']['scrollToTopSelectors']\n >,\n) {\n const elements = new Set<Element>()\n\n for (const selector of scrollToTopSelectors) {\n if (selector === windowScrollTarget) {\n continue\n }\n\n const element = getElement(selector)\n if (element) {\n elements.add(element)\n }\n }\n\n return elements\n}\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n // Keep hash/top scrolling active even when sessionStorage is unavailable.\n const shouldSetupScrollRestoration = force ?? router.options.scrollRestoration\n const scroll = router._scroll\n\n if (shouldSetupScrollRestoration) {\n scroll.restoring = true\n }\n\n if (isServer ?? router.isServer) {\n return\n }\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n const trackedScrollTargets = new Set<Document | Element>()\n\n // Snapshot the current page's tracked scroll targets before navigation or unload.\n const snapshotCurrentScrollTargets = (restoreKey: string) => {\n const keyEntry = (scrollRestorationCache[restoreKey] ||=\n {} as ScrollRestorationByElement)\n\n for (const target of trackedScrollTargets) {\n if (target === document) {\n keyEntry[windowScrollTarget] = { scrollX, scrollY }\n } else if ((target as Element).isConnected) {\n keyEntry[getScrollRestorationSelector(target as Element)] = {\n scrollX: (target as Element).scrollLeft,\n scrollY: (target as Element).scrollTop,\n }\n }\n }\n }\n\n if (shouldSetupScrollRestoration && !scroll.restoration) {\n scroll.restoration = true\n ignoreScroll = false\n\n history.scrollRestoration = 'manual'\n\n document.addEventListener(\n 'scroll',\n (event) => {\n if (ignoreScroll) {\n return\n }\n trackedScrollTargets.add(event.target as Document | Element)\n },\n true,\n )\n router.subscribe('onBeforeLoad', (event) => {\n if (event.fromLocation) {\n snapshotCurrentScrollTargets(getKey(event.fromLocation))\n }\n trackedScrollTargets.clear()\n })\n addEventListener('pagehide', () => {\n snapshotCurrentScrollTargets(\n getKey(\n router.stores.resolvedLocation.get() ?? router.stores.location.get(),\n ),\n )\n persistScrollRestorationCache()\n })\n }\n\n if (scroll.reset) {\n return\n }\n\n scroll.reset = true\n\n // Restore destination scroll after the new route has rendered.\n router.subscribe('onRendered', (event) => {\n const behavior = router.options.scrollRestorationBehavior\n const scrollToTopSelectors = router.options.scrollToTopSelectors\n const shouldResetScroll = scroll.next\n const hashNavigation = scroll.hash\n let scrollToTopElements: Set<Element> | undefined\n trackedScrollTargets.clear()\n scroll.next = true\n scroll.hash = false\n\n if (\n typeof router.options.scrollRestoration === 'function' &&\n !router.options.scrollRestoration({ location: router.latestLocation })\n ) {\n return\n }\n\n const cacheKey = getKey(event.toLocation)\n const fromCacheKey = event.fromLocation && getKey(event.fromLocation)\n\n if (scroll.restoring && fromCacheKey && fromCacheKey !== cacheKey) {\n const fromElementEntries = scrollRestorationCache[fromCacheKey]\n\n if (fromElementEntries) {\n let toElementEntries = scrollRestorationCache[cacheKey]\n\n for (const elementSelector in fromElementEntries) {\n if (elementSelector === windowScrollTarget) {\n if (shouldResetScroll) {\n continue\n }\n } else {\n const element = getElement(elementSelector)\n if (!element) {\n continue\n }\n\n if (shouldResetScroll && scrollToTopSelectors) {\n scrollToTopElements ??=\n getScrollToTopElements(scrollToTopSelectors)\n if (scrollToTopElements.has(element)) {\n continue\n }\n }\n }\n\n if (!toElementEntries) {\n toElementEntries = scrollRestorationCache[cacheKey] =\n {} as ScrollRestorationByElement\n }\n\n toElementEntries[elementSelector] ??=\n fromElementEntries[elementSelector]!\n }\n }\n }\n\n ignoreScroll = true\n\n try {\n const hash = event.toLocation.hash\n const hashScrollIntoViewOptions =\n event.toLocation.state.__hashScrollIntoViewOptions ?? true\n let windowRestored = false\n\n if (shouldResetScroll) {\n if (!hash && scrollToTopSelectors) {\n scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)\n }\n\n const skipWindowRestore =\n hash && hashScrollIntoViewOptions && hashNavigation\n\n const elementEntries = scroll.restoring\n ? scrollRestorationCache[cacheKey]\n : undefined\n\n if (elementEntries) {\n for (const elementSelector in elementEntries) {\n const { scrollX, scrollY } = elementEntries[elementSelector]!\n\n if (elementSelector === windowScrollTarget) {\n if (skipWindowRestore) {\n continue\n }\n\n scrollTo({\n top: scrollY,\n left: scrollX,\n behavior,\n })\n windowRestored = true\n } else {\n const element = getElement(elementSelector)\n if (element) {\n element.scrollLeft = scrollX\n element.scrollTop = scrollY\n scrollToTopElements?.delete(element)\n }\n }\n }\n }\n\n if (!hash) {\n const scrollOptions = {\n top: 0,\n left: 0,\n behavior,\n }\n\n if (!windowRestored) {\n scrollTo(scrollOptions)\n }\n if (scrollToTopElements) {\n for (const element of scrollToTopElements) {\n element.scrollTo(scrollOptions)\n }\n }\n }\n }\n\n if (!windowRestored && hash && hashScrollIntoViewOptions) {\n document.getElementById(hash)?.scrollIntoView(hashScrollIntoViewOptions)\n }\n } finally {\n ignoreScroll = false\n }\n })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/router.ts
CHANGED
|
@@ -904,11 +904,6 @@ export function getLocationChangeInfo(
|
|
|
904
904
|
return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
-
export const locationHistoryActions = new WeakMap<
|
|
908
|
-
ParsedLocation,
|
|
909
|
-
HistoryAction
|
|
910
|
-
>()
|
|
911
|
-
|
|
912
907
|
type LightweightRouteMatchResult = {
|
|
913
908
|
matchedRoutes: ReadonlyArray<AnyRoute>
|
|
914
909
|
fullPath: string
|
|
@@ -978,6 +973,8 @@ export class RouterCore<
|
|
|
978
973
|
)}`
|
|
979
974
|
_scroll: {
|
|
980
975
|
next: boolean
|
|
976
|
+
// True until the current PUSH/REPLACE renders, so its hash owns window scroll.
|
|
977
|
+
hash?: boolean
|
|
981
978
|
restoring?: boolean
|
|
982
979
|
restoration?: boolean
|
|
983
980
|
reset?: boolean
|
|
@@ -2475,9 +2472,8 @@ export class RouterCore<
|
|
|
2475
2472
|
try {
|
|
2476
2473
|
this.beforeLoad()
|
|
2477
2474
|
if (historyAction) {
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
locationHistoryActions.delete(this.latestLocation)
|
|
2475
|
+
this._scroll.hash =
|
|
2476
|
+
historyAction === 'PUSH' || historyAction === 'REPLACE'
|
|
2481
2477
|
}
|
|
2482
2478
|
const next = this.latestLocation
|
|
2483
2479
|
const prevLocation = this.stores.resolvedLocation.get()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isServer } from '@tanstack/router-core/isServer'
|
|
2
|
-
import { locationHistoryActions } from './router'
|
|
3
2
|
import type { AnyRouter } from './router'
|
|
4
3
|
import type { ParsedLocation } from './location'
|
|
5
4
|
|
|
@@ -133,7 +132,6 @@ export function getElementScrollRestorationEntry(
|
|
|
133
132
|
|
|
134
133
|
let ignoreScroll = false
|
|
135
134
|
const windowScrollTarget = 'window'
|
|
136
|
-
type ScrollTarget = typeof windowScrollTarget | Element
|
|
137
135
|
|
|
138
136
|
function getElement(selector: string | (() => Element | null | undefined)) {
|
|
139
137
|
try {
|
|
@@ -148,8 +146,8 @@ function getScrollToTopElements(
|
|
|
148
146
|
scrollToTopSelectors: NonNullable<
|
|
149
147
|
AnyRouter['options']['scrollToTopSelectors']
|
|
150
148
|
>,
|
|
151
|
-
)
|
|
152
|
-
const elements
|
|
149
|
+
) {
|
|
150
|
+
const elements = new Set<Element>()
|
|
153
151
|
|
|
154
152
|
for (const selector of scrollToTopSelectors) {
|
|
155
153
|
if (selector === windowScrollTarget) {
|
|
@@ -158,7 +156,7 @@ function getScrollToTopElements(
|
|
|
158
156
|
|
|
159
157
|
const element = getElement(selector)
|
|
160
158
|
if (element) {
|
|
161
|
-
elements.
|
|
159
|
+
elements.add(element)
|
|
162
160
|
}
|
|
163
161
|
}
|
|
164
162
|
|
|
@@ -180,46 +178,21 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
180
178
|
|
|
181
179
|
const getKey =
|
|
182
180
|
router.options.getScrollRestorationKey || defaultGetScrollRestorationKey
|
|
183
|
-
const
|
|
184
|
-
const setTrackedScrollEntry = (
|
|
185
|
-
target: ScrollTarget,
|
|
186
|
-
scrollX: number,
|
|
187
|
-
scrollY: number,
|
|
188
|
-
) => {
|
|
189
|
-
const entry =
|
|
190
|
-
trackedScrollEntries.get(target) || ({} as ScrollRestorationEntry)
|
|
191
|
-
entry.scrollX = scrollX
|
|
192
|
-
entry.scrollY = scrollY
|
|
193
|
-
trackedScrollEntries.set(target, entry)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const onScroll = (event: Event) => {
|
|
197
|
-
if (ignoreScroll || !scroll.restoring) {
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (event.target === document) {
|
|
202
|
-
setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY)
|
|
203
|
-
} else {
|
|
204
|
-
const target = event.target as Element
|
|
205
|
-
setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
181
|
+
const trackedScrollTargets = new Set<Document | Element>()
|
|
208
182
|
|
|
209
183
|
// Snapshot the current page's tracked scroll targets before navigation or unload.
|
|
210
184
|
const snapshotCurrentScrollTargets = (restoreKey: string) => {
|
|
211
|
-
if (!scroll.restoring) {
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
185
|
const keyEntry = (scrollRestorationCache[restoreKey] ||=
|
|
216
186
|
{} as ScrollRestorationByElement)
|
|
217
187
|
|
|
218
|
-
for (const
|
|
219
|
-
if (target ===
|
|
220
|
-
keyEntry[windowScrollTarget] =
|
|
221
|
-
} else if (target.isConnected) {
|
|
222
|
-
keyEntry[getScrollRestorationSelector(target)] =
|
|
188
|
+
for (const target of trackedScrollTargets) {
|
|
189
|
+
if (target === document) {
|
|
190
|
+
keyEntry[windowScrollTarget] = { scrollX, scrollY }
|
|
191
|
+
} else if ((target as Element).isConnected) {
|
|
192
|
+
keyEntry[getScrollRestorationSelector(target as Element)] = {
|
|
193
|
+
scrollX: (target as Element).scrollLeft,
|
|
194
|
+
scrollY: (target as Element).scrollTop,
|
|
195
|
+
}
|
|
223
196
|
}
|
|
224
197
|
}
|
|
225
198
|
}
|
|
@@ -230,12 +203,21 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
230
203
|
|
|
231
204
|
history.scrollRestoration = 'manual'
|
|
232
205
|
|
|
233
|
-
document.addEventListener(
|
|
206
|
+
document.addEventListener(
|
|
207
|
+
'scroll',
|
|
208
|
+
(event) => {
|
|
209
|
+
if (ignoreScroll) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
trackedScrollTargets.add(event.target as Document | Element)
|
|
213
|
+
},
|
|
214
|
+
true,
|
|
215
|
+
)
|
|
234
216
|
router.subscribe('onBeforeLoad', (event) => {
|
|
235
217
|
if (event.fromLocation) {
|
|
236
218
|
snapshotCurrentScrollTargets(getKey(event.fromLocation))
|
|
237
219
|
}
|
|
238
|
-
|
|
220
|
+
trackedScrollTargets.clear()
|
|
239
221
|
})
|
|
240
222
|
addEventListener('pagehide', () => {
|
|
241
223
|
snapshotCurrentScrollTargets(
|
|
@@ -258,12 +240,11 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
258
240
|
const behavior = router.options.scrollRestorationBehavior
|
|
259
241
|
const scrollToTopSelectors = router.options.scrollToTopSelectors
|
|
260
242
|
const shouldResetScroll = scroll.next
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
243
|
+
const hashNavigation = scroll.hash
|
|
244
|
+
let scrollToTopElements: Set<Element> | undefined
|
|
245
|
+
trackedScrollTargets.clear()
|
|
246
|
+
scroll.next = true
|
|
247
|
+
scroll.hash = false
|
|
267
248
|
|
|
268
249
|
if (
|
|
269
250
|
typeof router.options.scrollRestoration === 'function' &&
|
|
@@ -295,7 +276,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
295
276
|
if (shouldResetScroll && scrollToTopSelectors) {
|
|
296
277
|
scrollToTopElements ??=
|
|
297
278
|
getScrollToTopElements(scrollToTopSelectors)
|
|
298
|
-
if (scrollToTopElements.
|
|
279
|
+
if (scrollToTopElements.has(element)) {
|
|
299
280
|
continue
|
|
300
281
|
}
|
|
301
282
|
}
|
|
@@ -321,11 +302,12 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
321
302
|
let windowRestored = false
|
|
322
303
|
|
|
323
304
|
if (shouldResetScroll) {
|
|
324
|
-
|
|
305
|
+
if (!hash && scrollToTopSelectors) {
|
|
306
|
+
scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)
|
|
307
|
+
}
|
|
308
|
+
|
|
325
309
|
const skipWindowRestore =
|
|
326
|
-
hash &&
|
|
327
|
-
hashScrollIntoViewOptions &&
|
|
328
|
-
(action === 'PUSH' || action === 'REPLACE')
|
|
310
|
+
hash && hashScrollIntoViewOptions && hashNavigation
|
|
329
311
|
|
|
330
312
|
const elementEntries = scroll.restoring
|
|
331
313
|
? scrollRestorationCache[cacheKey]
|
|
@@ -351,21 +333,23 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
|
|
|
351
333
|
if (element) {
|
|
352
334
|
element.scrollLeft = scrollX
|
|
353
335
|
element.scrollTop = scrollY
|
|
336
|
+
scrollToTopElements?.delete(element)
|
|
354
337
|
}
|
|
355
338
|
}
|
|
356
339
|
}
|
|
357
340
|
}
|
|
358
341
|
|
|
359
|
-
if (!
|
|
342
|
+
if (!hash) {
|
|
360
343
|
const scrollOptions = {
|
|
361
344
|
top: 0,
|
|
362
345
|
left: 0,
|
|
363
346
|
behavior,
|
|
364
347
|
}
|
|
365
348
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
349
|
+
if (!windowRestored) {
|
|
350
|
+
scrollTo(scrollOptions)
|
|
351
|
+
}
|
|
352
|
+
if (scrollToTopElements) {
|
|
369
353
|
for (const element of scrollToTopElements) {
|
|
370
354
|
element.scrollTo(scrollOptions)
|
|
371
355
|
}
|