@ecopages/react-router 0.2.0-alpha.9 → 0.2.1-beta.0
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/README.md +1 -1
- package/package.json +5 -5
- package/src/adapter.js +1 -2
- package/src/head-morpher.d.ts +6 -2
- package/src/head-morpher.js +88 -9
- package/src/hydration-assets.d.ts +12 -0
- package/src/hydration-assets.js +17 -0
- package/src/navigation.d.ts +27 -2
- package/src/navigation.js +40 -7
- package/src/router.d.ts +3 -1
- package/src/router.js +249 -183
- package/src/scroll-persist.js +15 -7
- package/CHANGELOG.md +0 -19
- package/browser.ts +0 -17
- package/src/adapter.ts +0 -48
- package/src/context.ts +0 -25
- package/src/head-morpher.ts +0 -214
- package/src/index.ts +0 -21
- package/src/manage-scroll.ts +0 -47
- package/src/navigation.ts +0 -297
- package/src/props-script.ts +0 -19
- package/src/router.ts +0 -670
- package/src/scroll-persist.ts +0 -96
- package/src/types.ts +0 -64
- package/src/view-transition-manager.ts +0 -30
- package/src/view-transition-utils.ts +0 -95
package/src/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useEffect,
|
|
3
|
+
useEffectEvent,
|
|
3
4
|
useState,
|
|
4
5
|
useCallback,
|
|
5
6
|
useMemo,
|
|
@@ -14,6 +15,7 @@ import { RouterContext } from "./context.js";
|
|
|
14
15
|
import {
|
|
15
16
|
fetchPageDocument,
|
|
16
17
|
getInterceptDecision,
|
|
18
|
+
isSamePageHashNavigationHref,
|
|
17
19
|
loadPageModuleFromDocument,
|
|
18
20
|
shouldInterceptClick
|
|
19
21
|
} from "./navigation.js";
|
|
@@ -21,7 +23,9 @@ import { morphHead } from "./head-morpher.js";
|
|
|
21
23
|
import { applyViewTransitionNames } from "./view-transition-utils.js";
|
|
22
24
|
import { manageScroll } from "./manage-scroll.js";
|
|
23
25
|
import { saveScrollPositions, restoreScrollPositions } from "./scroll-persist.js";
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
getEcoNavigationRuntime
|
|
28
|
+
} from "@ecopages/core/router/navigation-coordinator";
|
|
25
29
|
import {
|
|
26
30
|
getAnchorFromNavigationEvent,
|
|
27
31
|
recoverPendingNavigationHref
|
|
@@ -52,6 +56,27 @@ function normalizeLayoutKey(value) {
|
|
|
52
56
|
return trimmed.split("#")[0]?.split("?")[0]?.replace(/\/$/, "") || "layout";
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
function hashString(value) {
|
|
60
|
+
let hash = 2166136261;
|
|
61
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
62
|
+
hash ^= value.charCodeAt(index);
|
|
63
|
+
hash = Math.imul(hash, 16777619);
|
|
64
|
+
}
|
|
65
|
+
return (hash >>> 0).toString(36);
|
|
66
|
+
}
|
|
67
|
+
function getLayoutSourceSignature(Layout) {
|
|
68
|
+
const source = Function.prototype.toString.call(Layout).replace(/\s+/g, " ").trim();
|
|
69
|
+
return hashString(source);
|
|
70
|
+
}
|
|
71
|
+
function getLayoutCacheKey(Layout) {
|
|
72
|
+
const layoutConfig = Layout.config;
|
|
73
|
+
const layoutMetaKey = layoutConfig?.__eco?.file || layoutConfig?.__eco?.id;
|
|
74
|
+
if (layoutMetaKey) {
|
|
75
|
+
return normalizeLayoutKey(layoutMetaKey);
|
|
76
|
+
}
|
|
77
|
+
const layoutNameKey = Layout.displayName || Layout.name || "layout";
|
|
78
|
+
return `${normalizeLayoutKey(layoutNameKey)}:${getLayoutSourceSignature(Layout)}`;
|
|
79
|
+
}
|
|
55
80
|
function clearLayoutCache() {
|
|
56
81
|
getLayoutCache().clear();
|
|
57
82
|
}
|
|
@@ -64,7 +89,7 @@ const PageContent = () => {
|
|
|
64
89
|
}
|
|
65
90
|
return null;
|
|
66
91
|
}
|
|
67
|
-
const { Component: Page, props } = pageContext;
|
|
92
|
+
const { Component: Page, props, refreshPersistedLayout } = pageContext;
|
|
68
93
|
const Layout = getLayoutFromPage(Page);
|
|
69
94
|
const pageElement = createElement(Page, props);
|
|
70
95
|
const layoutProps = props?.locals ? { locals: props.locals } : null;
|
|
@@ -73,10 +98,8 @@ const PageContent = () => {
|
|
|
73
98
|
}
|
|
74
99
|
if (persistLayouts) {
|
|
75
100
|
const layoutCache = getLayoutCache();
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const layoutKey = normalizeLayoutKey(layoutKeyRaw);
|
|
79
|
-
if (!layoutCache.has(layoutKey)) {
|
|
101
|
+
const layoutKey = getLayoutCacheKey(Layout);
|
|
102
|
+
if (!layoutCache.has(layoutKey) || refreshPersistedLayout && layoutCache.get(layoutKey) !== Layout) {
|
|
80
103
|
layoutCache.set(layoutKey, Layout);
|
|
81
104
|
}
|
|
82
105
|
const CachedLayout = layoutCache.get(layoutKey);
|
|
@@ -92,35 +115,39 @@ function createDeferred() {
|
|
|
92
115
|
return { promise, resolve };
|
|
93
116
|
}
|
|
94
117
|
function useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef) {
|
|
118
|
+
const handleCoordinatorNavigate = useEffectEvent(async (request) => {
|
|
119
|
+
await navigate(request.href, {
|
|
120
|
+
isPopState: request.direction === "back",
|
|
121
|
+
pushHistory: request.direction === "forward",
|
|
122
|
+
skipViewTransition: request.source === "browser-router"
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
const handleCoordinatorReload = useEffectEvent(async (request) => {
|
|
127
|
+
if (activeNavigationRef.current || isNavigatingRef.current) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (request?.clearCache) {
|
|
131
|
+
clearLayoutCache();
|
|
132
|
+
}
|
|
133
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
134
|
+
await navigate(currentUrl, { moduleUrlOverride: request?.moduleUrl });
|
|
135
|
+
});
|
|
136
|
+
const handleCleanupBeforeHandoff = useEffectEvent(async () => {
|
|
137
|
+
runtimeActiveRef.current = false;
|
|
138
|
+
window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
|
|
139
|
+
});
|
|
95
140
|
useEffect(() => {
|
|
96
|
-
if (typeof window === "undefined") return;
|
|
97
141
|
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
98
142
|
let unregisterRuntime = null;
|
|
99
143
|
const unregister = navigationRuntime.register({
|
|
100
144
|
owner: "react-router",
|
|
101
|
-
navigate:
|
|
102
|
-
|
|
103
|
-
isPopState: request.direction === "back",
|
|
104
|
-
pushHistory: request.direction === "forward",
|
|
105
|
-
skipViewTransition: request.source === "browser-router"
|
|
106
|
-
});
|
|
107
|
-
return true;
|
|
108
|
-
},
|
|
109
|
-
reloadCurrentPage: async (request) => {
|
|
110
|
-
if (activeNavigationRef.current || isNavigatingRef.current) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (request?.clearCache) {
|
|
114
|
-
clearLayoutCache();
|
|
115
|
-
}
|
|
116
|
-
const currentUrl = window.location.pathname + window.location.search;
|
|
117
|
-
await navigate(currentUrl);
|
|
118
|
-
},
|
|
145
|
+
navigate: handleCoordinatorNavigate,
|
|
146
|
+
reloadCurrentPage: handleCoordinatorReload,
|
|
119
147
|
cleanupBeforeHandoff: async () => {
|
|
120
|
-
runtimeActiveRef.current = false;
|
|
121
148
|
unregisterRuntime?.();
|
|
122
149
|
unregisterRuntime = null;
|
|
123
|
-
|
|
150
|
+
await handleCleanupBeforeHandoff();
|
|
124
151
|
}
|
|
125
152
|
});
|
|
126
153
|
unregisterRuntime = unregister;
|
|
@@ -132,11 +159,15 @@ function useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef
|
|
|
132
159
|
unregisterRuntime?.();
|
|
133
160
|
unregisterRuntime = null;
|
|
134
161
|
};
|
|
135
|
-
}, [
|
|
162
|
+
}, [runtimeActiveRef]);
|
|
136
163
|
}
|
|
137
164
|
const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
138
165
|
const options = useMemo(() => ({ ...DEFAULT_OPTIONS, ...userOptions }), [userOptions]);
|
|
139
|
-
const [currentPage, setCurrentPage] = useState({
|
|
166
|
+
const [currentPage, setCurrentPage] = useState({
|
|
167
|
+
Component: page,
|
|
168
|
+
props: pageProps,
|
|
169
|
+
refreshPersistedLayout: false
|
|
170
|
+
});
|
|
140
171
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
141
172
|
const pendingRenderRef = useRef(null);
|
|
142
173
|
const activeNavigationRef = useRef(null);
|
|
@@ -145,34 +176,24 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
145
176
|
const pendingPointerNavigationRef = useRef(null);
|
|
146
177
|
const pendingHoverNavigationRef = useRef(null);
|
|
147
178
|
const queuedNavigationHrefRef = useRef(null);
|
|
148
|
-
const
|
|
179
|
+
const committedPathRef = useRef(
|
|
180
|
+
typeof window !== "undefined" ? window.location.pathname + window.location.search : ""
|
|
181
|
+
);
|
|
149
182
|
const previousUrlRef = useRef(typeof window !== "undefined" ? window.location.href : "");
|
|
150
183
|
useEffect(() => {
|
|
151
184
|
isNavigatingRef.current = isNavigating;
|
|
152
185
|
}, [isNavigating]);
|
|
153
186
|
useEffect(() => {
|
|
154
|
-
setCurrentPage({ Component: page, props: pageProps });
|
|
187
|
+
setCurrentPage({ Component: page, props: pageProps, refreshPersistedLayout: true });
|
|
155
188
|
}, [page, pageProps]);
|
|
156
189
|
useEffect(() => {
|
|
190
|
+
committedPathRef.current = window.location.pathname + window.location.search;
|
|
157
191
|
applyViewTransitionNames();
|
|
158
|
-
}, [currentPage]);
|
|
159
|
-
useEffect(() => {
|
|
160
192
|
const pendingRender = pendingRenderRef.current;
|
|
161
193
|
if (pendingRender && currentPage.Component === pendingRender.page.Component && currentPage.props === pendingRender.page.props) {
|
|
162
194
|
pendingRender.resolve();
|
|
163
195
|
pendingRenderRef.current = null;
|
|
164
196
|
}
|
|
165
|
-
}, [currentPage]);
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
return () => {
|
|
168
|
-
activeNavigationRef.current?.cancel();
|
|
169
|
-
pendingRenderRef.current?.resolve();
|
|
170
|
-
pendingRenderRef.current = null;
|
|
171
|
-
queuedNavigationHrefRef.current = null;
|
|
172
|
-
};
|
|
173
|
-
}, []);
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
if (typeof window === "undefined") return;
|
|
176
197
|
const url = new URL(window.location.href);
|
|
177
198
|
const previousUrl = new URL(previousUrlRef.current);
|
|
178
199
|
if (url.href !== previousUrl.href) {
|
|
@@ -182,15 +203,23 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
182
203
|
});
|
|
183
204
|
previousUrlRef.current = url.href;
|
|
184
205
|
}
|
|
185
|
-
if (pendingScrollRestoreRef.current) {
|
|
186
|
-
const { url: targetUrl, isPopState } = pendingScrollRestoreRef.current;
|
|
187
|
-
restoreScrollPositions(targetUrl, isPopState);
|
|
188
|
-
pendingScrollRestoreRef.current = null;
|
|
189
|
-
}
|
|
190
206
|
}, [currentPage, options.scrollBehavior, options.smoothScroll]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
return () => {
|
|
209
|
+
activeNavigationRef.current?.cancel();
|
|
210
|
+
pendingRenderRef.current?.resolve();
|
|
211
|
+
pendingRenderRef.current = null;
|
|
212
|
+
queuedNavigationHrefRef.current = null;
|
|
213
|
+
};
|
|
214
|
+
}, []);
|
|
191
215
|
const navigate = useCallback(
|
|
192
216
|
async (url, navigationOptions = {}) => {
|
|
193
|
-
const {
|
|
217
|
+
const {
|
|
218
|
+
isPopState = false,
|
|
219
|
+
pushHistory = false,
|
|
220
|
+
skipViewTransition = false,
|
|
221
|
+
moduleUrlOverride
|
|
222
|
+
} = navigationOptions;
|
|
194
223
|
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
195
224
|
const navigation = navigationRuntime.beginNavigationTransaction();
|
|
196
225
|
activeNavigationRef.current = navigation;
|
|
@@ -203,6 +232,16 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
203
232
|
props
|
|
204
233
|
};
|
|
205
234
|
};
|
|
235
|
+
const preparePendingRender = (nextPage) => {
|
|
236
|
+
pendingRenderRef.current?.resolve();
|
|
237
|
+
const renderDfd = createDeferred();
|
|
238
|
+
pendingRenderRef.current = {
|
|
239
|
+
navigationId,
|
|
240
|
+
page: nextPage,
|
|
241
|
+
resolve: renderDfd.resolve
|
|
242
|
+
};
|
|
243
|
+
return renderDfd.promise;
|
|
244
|
+
};
|
|
206
245
|
let navigationCommitPromise = null;
|
|
207
246
|
try {
|
|
208
247
|
setIsNavigating(true);
|
|
@@ -212,32 +251,38 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
212
251
|
window.location.href = url;
|
|
213
252
|
return;
|
|
214
253
|
}
|
|
215
|
-
const result = await loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath
|
|
254
|
+
const result = await loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath, {
|
|
255
|
+
moduleUrlOverride
|
|
256
|
+
});
|
|
216
257
|
if (isStale()) return;
|
|
217
258
|
if (result) {
|
|
218
259
|
const { Component, props, doc, finalPath, moduleUrl } = result;
|
|
219
|
-
const nextPage = { Component, props };
|
|
220
|
-
const cleanupHead = await morphHead(doc);
|
|
260
|
+
const nextPage = { Component, props, refreshPersistedLayout: Boolean(moduleUrlOverride) };
|
|
261
|
+
const { cleanup: cleanupHead, flushRerunScripts } = await morphHead(doc);
|
|
262
|
+
const finalizeCommittedNavigation = () => {
|
|
263
|
+
committedPathRef.current = finalPath;
|
|
264
|
+
flushRerunScripts();
|
|
265
|
+
cleanupHead();
|
|
266
|
+
applyViewTransitionNames();
|
|
267
|
+
restoreScrollPositions(finalPath, isPopState);
|
|
268
|
+
};
|
|
269
|
+
const commitNextPage = () => {
|
|
270
|
+
commitPageData(moduleUrl, props);
|
|
271
|
+
setCurrentPage(nextPage);
|
|
272
|
+
};
|
|
221
273
|
if (isStale()) {
|
|
222
274
|
cleanupHead();
|
|
223
275
|
return;
|
|
224
276
|
}
|
|
225
277
|
applyViewTransitionNames();
|
|
278
|
+
saveScrollPositions();
|
|
226
279
|
if (pushHistory) {
|
|
227
280
|
window.history.pushState(null, "", finalPath);
|
|
228
281
|
} else if (finalPath !== url) {
|
|
229
282
|
window.history.replaceState(null, "", finalPath);
|
|
230
283
|
}
|
|
231
|
-
saveScrollPositions();
|
|
232
|
-
pendingScrollRestoreRef.current = { url, isPopState };
|
|
233
284
|
if (!skipViewTransition && options.viewTransitions && document.startViewTransition) {
|
|
234
|
-
|
|
235
|
-
const renderDfd = createDeferred();
|
|
236
|
-
pendingRenderRef.current = {
|
|
237
|
-
navigationId,
|
|
238
|
-
page: nextPage,
|
|
239
|
-
resolve: renderDfd.resolve
|
|
240
|
-
};
|
|
285
|
+
const renderPromise = preparePendingRender(nextPage);
|
|
241
286
|
navigationCommitPromise = new Promise((resolve) => {
|
|
242
287
|
document.startViewTransition(async () => {
|
|
243
288
|
try {
|
|
@@ -250,15 +295,13 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
250
295
|
return;
|
|
251
296
|
}
|
|
252
297
|
startTransition(() => {
|
|
253
|
-
|
|
254
|
-
setCurrentPage(nextPage);
|
|
298
|
+
commitNextPage();
|
|
255
299
|
});
|
|
256
|
-
await
|
|
300
|
+
await renderPromise;
|
|
257
301
|
if (isStale()) {
|
|
258
302
|
return;
|
|
259
303
|
}
|
|
260
|
-
|
|
261
|
-
applyViewTransitionNames();
|
|
304
|
+
finalizeCommittedNavigation();
|
|
262
305
|
} finally {
|
|
263
306
|
resolve();
|
|
264
307
|
}
|
|
@@ -266,10 +309,14 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
266
309
|
});
|
|
267
310
|
await navigationCommitPromise;
|
|
268
311
|
} else {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
312
|
+
const renderPromise = preparePendingRender(nextPage);
|
|
313
|
+
commitNextPage();
|
|
314
|
+
await renderPromise;
|
|
315
|
+
if (isStale()) {
|
|
316
|
+
cleanupHead();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
finalizeCommittedNavigation();
|
|
273
320
|
}
|
|
274
321
|
} else {
|
|
275
322
|
if (isStale()) return;
|
|
@@ -293,11 +340,12 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
293
340
|
} finally {
|
|
294
341
|
const shouldReplayQueuedNavigation = activeNavigationRef.current?.id === navigationId;
|
|
295
342
|
const queuedNavigationHref = shouldReplayQueuedNavigation ? queuedNavigationHrefRef.current : null;
|
|
343
|
+
const queuedNavigationPath = queuedNavigationHref ? new URL(queuedNavigationHref, window.location.origin).pathname + new URL(queuedNavigationHref, window.location.origin).search : null;
|
|
296
344
|
navigation.complete();
|
|
297
345
|
if (activeNavigationRef.current?.id === navigationId) {
|
|
298
346
|
activeNavigationRef.current = null;
|
|
299
347
|
}
|
|
300
|
-
if (queuedNavigationHref &&
|
|
348
|
+
if (queuedNavigationHref && queuedNavigationPath !== committedPathRef.current) {
|
|
301
349
|
queuedNavigationHrefRef.current = null;
|
|
302
350
|
if (runtimeActiveRef.current) {
|
|
303
351
|
void navigate(queuedNavigationHref, { pushHistory: true });
|
|
@@ -317,132 +365,150 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
317
365
|
},
|
|
318
366
|
[options.viewTransitions]
|
|
319
367
|
);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
const getLinkFromEvent = useEffectEvent(
|
|
369
|
+
(event) => getAnchorFromNavigationEvent(event, options.linkSelector)
|
|
370
|
+
);
|
|
371
|
+
const getRecoveredPointerHref = useEffectEvent(() => {
|
|
372
|
+
const href = recoverPendingNavigationHref(
|
|
373
|
+
pendingPointerNavigationRef.current,
|
|
374
|
+
!!activeNavigationRef.current || isNavigatingRef.current,
|
|
375
|
+
performance.now()
|
|
376
|
+
);
|
|
377
|
+
if (!href) {
|
|
378
|
+
pendingPointerNavigationRef.current = null;
|
|
379
|
+
}
|
|
380
|
+
return href;
|
|
381
|
+
});
|
|
382
|
+
const getRecoveredHoverHref = useEffectEvent(() => {
|
|
383
|
+
const href = recoverPendingNavigationHref(
|
|
384
|
+
pendingHoverNavigationRef.current,
|
|
385
|
+
!!activeNavigationRef.current || isNavigatingRef.current,
|
|
386
|
+
performance.now()
|
|
387
|
+
);
|
|
388
|
+
if (!href) {
|
|
389
|
+
pendingHoverNavigationRef.current = null;
|
|
390
|
+
}
|
|
391
|
+
return href;
|
|
392
|
+
});
|
|
393
|
+
const handleHoverIntent = useEffectEvent((event) => {
|
|
394
|
+
if (!runtimeActiveRef.current) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const link = getLinkFromEvent(event);
|
|
398
|
+
if (!link) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const decision = getInterceptDecision(event, link, options);
|
|
402
|
+
if (!decision.shouldIntercept) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
pendingHoverNavigationRef.current = {
|
|
406
|
+
href: link.getAttribute("href"),
|
|
407
|
+
timestamp: performance.now()
|
|
343
408
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
409
|
+
queuedNavigationHrefRef.current = link.getAttribute("href");
|
|
410
|
+
});
|
|
411
|
+
const handlePointerDown = useEffectEvent((event) => {
|
|
412
|
+
if (!runtimeActiveRef.current) {
|
|
413
|
+
pendingPointerNavigationRef.current = null;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const link = getLinkFromEvent(event);
|
|
417
|
+
if (!link) {
|
|
418
|
+
pendingPointerNavigationRef.current = null;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const decision = getInterceptDecision(event, link, options);
|
|
422
|
+
pendingPointerNavigationRef.current = decision.shouldIntercept ? {
|
|
423
|
+
href: link.getAttribute("href"),
|
|
424
|
+
timestamp: performance.now()
|
|
425
|
+
} : null;
|
|
426
|
+
if (decision.shouldIntercept && (activeNavigationRef.current || isNavigatingRef.current)) {
|
|
360
427
|
queuedNavigationHrefRef.current = link.getAttribute("href");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
timestamp: performance.now()
|
|
376
|
-
} : null;
|
|
377
|
-
if (decision.shouldIntercept && (activeNavigationRef.current || isNavigatingRef.current)) {
|
|
378
|
-
queuedNavigationHrefRef.current = link.getAttribute("href");
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
const handleClick = (event) => {
|
|
382
|
-
if (!runtimeActiveRef.current) {
|
|
383
|
-
pendingPointerNavigationRef.current = null;
|
|
384
|
-
pendingHoverNavigationRef.current = null;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
const handleClick = useEffectEvent((event) => {
|
|
431
|
+
if (!runtimeActiveRef.current) {
|
|
432
|
+
pendingPointerNavigationRef.current = null;
|
|
433
|
+
pendingHoverNavigationRef.current = null;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const link = getLinkFromEvent(event);
|
|
437
|
+
if (!link) {
|
|
438
|
+
const recoveredHref = getRecoveredPointerHref() ?? getRecoveredHoverHref();
|
|
439
|
+
pendingPointerNavigationRef.current = null;
|
|
440
|
+
pendingHoverNavigationRef.current = null;
|
|
441
|
+
if (!recoveredHref) {
|
|
385
442
|
return;
|
|
386
443
|
}
|
|
387
|
-
|
|
388
|
-
if (!link) {
|
|
389
|
-
const recoveredHref = getRecoveredPointerHref() ?? getRecoveredHoverHref();
|
|
390
|
-
pendingPointerNavigationRef.current = null;
|
|
391
|
-
pendingHoverNavigationRef.current = null;
|
|
392
|
-
if (!recoveredHref) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
event.preventDefault();
|
|
444
|
+
if (isSamePageHashNavigationHref(recoveredHref)) {
|
|
396
445
|
queuedNavigationHrefRef.current = null;
|
|
397
|
-
const recoveredUrl = new URL(recoveredHref, window.location.origin);
|
|
398
|
-
navigate(recoveredUrl.pathname + recoveredUrl.search, { pushHistory: true });
|
|
399
446
|
return;
|
|
400
447
|
}
|
|
401
|
-
if (!shouldInterceptClick(event, link, options)) {
|
|
402
|
-
if (options.debug) {
|
|
403
|
-
const decision = getInterceptDecision(event, link, options);
|
|
404
|
-
if (!decision.shouldIntercept) {
|
|
405
|
-
console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
pendingPointerNavigationRef.current = null;
|
|
409
|
-
pendingHoverNavigationRef.current = null;
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
pendingPointerNavigationRef.current = null;
|
|
413
|
-
pendingHoverNavigationRef.current = null;
|
|
414
448
|
event.preventDefault();
|
|
415
449
|
queuedNavigationHrefRef.current = null;
|
|
416
|
-
const
|
|
417
|
-
|
|
450
|
+
const recoveredUrl = new URL(recoveredHref, window.location.href);
|
|
451
|
+
navigate(recoveredUrl.pathname + recoveredUrl.search, { pushHistory: true });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (!shouldInterceptClick(event, link, options)) {
|
|
418
455
|
if (options.debug) {
|
|
419
|
-
|
|
456
|
+
const decision = getInterceptDecision(event, link, options);
|
|
457
|
+
if (!decision.shouldIntercept) {
|
|
458
|
+
console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
|
|
459
|
+
}
|
|
420
460
|
}
|
|
421
|
-
|
|
461
|
+
pendingPointerNavigationRef.current = null;
|
|
462
|
+
pendingHoverNavigationRef.current = null;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
pendingPointerNavigationRef.current = null;
|
|
466
|
+
pendingHoverNavigationRef.current = null;
|
|
467
|
+
event.preventDefault();
|
|
468
|
+
queuedNavigationHrefRef.current = null;
|
|
469
|
+
const href = link.getAttribute("href");
|
|
470
|
+
const url = new URL(href, window.location.origin);
|
|
471
|
+
if (options.debug) {
|
|
472
|
+
console.debug("[EcoRouter] Intercepting navigation:", url.pathname + url.search);
|
|
473
|
+
}
|
|
474
|
+
navigate(url.pathname + url.search, { pushHistory: true });
|
|
475
|
+
});
|
|
476
|
+
const handlePopState = useEffectEvent(() => {
|
|
477
|
+
if (!runtimeActiveRef.current) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
navigate(window.location.pathname + window.location.search, { isPopState: true });
|
|
481
|
+
});
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
const onHoverIntent = (event) => {
|
|
484
|
+
handleHoverIntent(event);
|
|
422
485
|
};
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
navigate(window.location.pathname + window.location.search, { isPopState: true });
|
|
486
|
+
const onPointerDown = (event) => {
|
|
487
|
+
handlePointerDown(event);
|
|
428
488
|
};
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
489
|
+
const onClick = (event) => {
|
|
490
|
+
handleClick(event);
|
|
491
|
+
};
|
|
492
|
+
const onPopState = () => {
|
|
493
|
+
handlePopState();
|
|
494
|
+
};
|
|
495
|
+
document.addEventListener("mouseover", onHoverIntent, true);
|
|
496
|
+
document.addEventListener("pointerover", onHoverIntent, true);
|
|
497
|
+
document.addEventListener("mousemove", onHoverIntent, true);
|
|
498
|
+
document.addEventListener("pointermove", onHoverIntent, true);
|
|
499
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
500
|
+
document.addEventListener("click", onClick, true);
|
|
501
|
+
window.addEventListener("popstate", onPopState);
|
|
436
502
|
return () => {
|
|
437
|
-
document.removeEventListener("mouseover",
|
|
438
|
-
document.removeEventListener("pointerover",
|
|
439
|
-
document.removeEventListener("mousemove",
|
|
440
|
-
document.removeEventListener("pointermove",
|
|
441
|
-
document.removeEventListener("pointerdown",
|
|
442
|
-
document.removeEventListener("click",
|
|
443
|
-
window.removeEventListener("popstate",
|
|
503
|
+
document.removeEventListener("mouseover", onHoverIntent, true);
|
|
504
|
+
document.removeEventListener("pointerover", onHoverIntent, true);
|
|
505
|
+
document.removeEventListener("mousemove", onHoverIntent, true);
|
|
506
|
+
document.removeEventListener("pointermove", onHoverIntent, true);
|
|
507
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
508
|
+
document.removeEventListener("click", onClick, true);
|
|
509
|
+
window.removeEventListener("popstate", onPopState);
|
|
444
510
|
};
|
|
445
|
-
}, [
|
|
511
|
+
}, []);
|
|
446
512
|
useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef);
|
|
447
513
|
return createElement(
|
|
448
514
|
RouterContext.Provider,
|
package/src/scroll-persist.js
CHANGED
|
@@ -28,19 +28,27 @@ function restoreScrollPositions(targetUrl, isPopState) {
|
|
|
28
28
|
currentScrollSnapshot = null;
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const restore = (remainingAttempts) => {
|
|
32
32
|
requestAnimationFrame(() => {
|
|
33
|
+
const restoredKeys = /* @__PURE__ */ new Set();
|
|
33
34
|
document.querySelectorAll(PERSIST_SELECTOR).forEach((el) => {
|
|
34
35
|
const key = getElementKey(el);
|
|
35
|
-
if (key
|
|
36
|
-
|
|
37
|
-
el.scrollTop = pos.top;
|
|
38
|
-
el.scrollLeft = pos.left;
|
|
36
|
+
if (!key || !positions.has(key)) {
|
|
37
|
+
return;
|
|
39
38
|
}
|
|
39
|
+
const pos = positions.get(key);
|
|
40
|
+
el.scrollTop = pos.top;
|
|
41
|
+
el.scrollLeft = pos.left;
|
|
42
|
+
restoredKeys.add(key);
|
|
40
43
|
});
|
|
41
|
-
|
|
44
|
+
if (restoredKeys.size === positions.size || remainingAttempts <= 1) {
|
|
45
|
+
currentScrollSnapshot = null;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setTimeout(() => restore(remainingAttempts - 1), 50);
|
|
42
49
|
});
|
|
43
|
-
}
|
|
50
|
+
};
|
|
51
|
+
restore(20);
|
|
44
52
|
}
|
|
45
53
|
function getScrollPositions(url) {
|
|
46
54
|
return urlScrollStore.get(url);
|
package/CHANGELOG.md
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@ecopages/react-router` are documented here.
|
|
4
|
-
|
|
5
|
-
> **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
|
|
6
|
-
|
|
7
|
-
## [UNRELEASED] — TBD
|
|
8
|
-
|
|
9
|
-
### Bug Fixes
|
|
10
|
-
|
|
11
|
-
- Fixed React-to-non-React handoffs to replay queued clicks through the next active runtime and reuse prefetched HTML documents instead of forcing a second fetch.
|
|
12
|
-
- Fixed stale handoff cleanup and fallback races so older React-router or browser-router navigations cannot overwrite a newer navigation.
|
|
13
|
-
- Standardized React route payload reads on `window.__ECO_PAGES__.page` and explicit document owner markers so mixed-router page ownership stays stable.
|
|
14
|
-
- Restored current-page HMR refreshes with persist layouts enabled by targeting the active React-router owner.
|
|
15
|
-
|
|
16
|
-
### Refactoring
|
|
17
|
-
|
|
18
|
-
- Routed browser handoff and current-page reloads through the shared navigation coordinator.
|
|
19
|
-
- Updated package metadata for the current core, esbuild adapter, and React peer dependency surface.
|
package/browser.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser entry point for @ecopages/react-router.
|
|
3
|
-
* This file exports only the client-side components needed for hydration.
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export { EcoRouter, PageContent } from './src/router.ts';
|
|
8
|
-
export type { EcoRouterProps } from './src/router.ts';
|
|
9
|
-
|
|
10
|
-
export { useRouter } from './src/context.ts';
|
|
11
|
-
export type { RouterContextValue } from './src/context.ts';
|
|
12
|
-
export { EcoPropsScript } from './src/props-script.ts';
|
|
13
|
-
export type { EcoPropsScriptProps } from './src/props-script.ts';
|
|
14
|
-
|
|
15
|
-
export { morphHead } from './src/head-morpher.ts';
|
|
16
|
-
|
|
17
|
-
export type { PageState } from './src/navigation.ts';
|