@ecopages/react-router 0.2.0-alpha.5 → 0.2.0-alpha.51
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 +45 -119
- package/package.json +5 -4
- package/src/adapter.js +1 -2
- package/src/head-morpher.d.ts +6 -2
- package/src/head-morpher.js +125 -10
- package/src/hydration-assets.d.ts +12 -0
- package/src/hydration-assets.js +17 -0
- package/src/navigation.d.ts +47 -9
- package/src/navigation.js +94 -35
- package/src/props-script.d.ts +1 -1
- package/src/router.d.ts +5 -1
- package/src/router.js +373 -90
- package/src/scroll-persist.js +15 -7
- package/CHANGELOG.md +0 -12
- package/browser.ts +0 -17
- package/src/adapter.ts +0 -48
- package/src/context.ts +0 -25
- package/src/head-morpher.ts +0 -170
- package/src/index.ts +0 -21
- package/src/manage-scroll.ts +0 -47
- package/src/navigation.ts +0 -247
- package/src/props-script.ts +0 -19
- package/src/router.ts +0 -348
- 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,
|
|
@@ -11,11 +12,24 @@ import {
|
|
|
11
12
|
} from "react";
|
|
12
13
|
import { DEFAULT_OPTIONS } from "./types.js";
|
|
13
14
|
import { RouterContext } from "./context.js";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
fetchPageDocument,
|
|
17
|
+
getInterceptDecision,
|
|
18
|
+
isSamePageHashNavigationHref,
|
|
19
|
+
loadPageModuleFromDocument,
|
|
20
|
+
shouldInterceptClick
|
|
21
|
+
} from "./navigation.js";
|
|
15
22
|
import { morphHead } from "./head-morpher.js";
|
|
16
23
|
import { applyViewTransitionNames } from "./view-transition-utils.js";
|
|
17
24
|
import { manageScroll } from "./manage-scroll.js";
|
|
18
25
|
import { saveScrollPositions, restoreScrollPositions } from "./scroll-persist.js";
|
|
26
|
+
import {
|
|
27
|
+
getEcoNavigationRuntime
|
|
28
|
+
} from "@ecopages/core/router/navigation-coordinator";
|
|
29
|
+
import {
|
|
30
|
+
getAnchorFromNavigationEvent,
|
|
31
|
+
recoverPendingNavigationHref
|
|
32
|
+
} from "@ecopages/core/router/link-intent";
|
|
19
33
|
const PageContext = createContext(null);
|
|
20
34
|
const PersistLayoutsContext = createContext(false);
|
|
21
35
|
function getLayoutFromPage(Page) {
|
|
@@ -54,9 +68,10 @@ const PageContent = () => {
|
|
|
54
68
|
}
|
|
55
69
|
return null;
|
|
56
70
|
}
|
|
57
|
-
const { Component: Page, props } = pageContext;
|
|
71
|
+
const { Component: Page, props, refreshPersistedLayout } = pageContext;
|
|
58
72
|
const Layout = getLayoutFromPage(Page);
|
|
59
73
|
const pageElement = createElement(Page, props);
|
|
74
|
+
const layoutProps = props?.locals ? { locals: props.locals } : null;
|
|
60
75
|
if (!Layout) {
|
|
61
76
|
return pageElement;
|
|
62
77
|
}
|
|
@@ -65,13 +80,13 @@ const PageContent = () => {
|
|
|
65
80
|
const layoutConfig = Layout.config;
|
|
66
81
|
const layoutKeyRaw = layoutConfig?.__eco?.id || Layout.displayName || Layout.name || "layout";
|
|
67
82
|
const layoutKey = normalizeLayoutKey(layoutKeyRaw);
|
|
68
|
-
if (!layoutCache.has(layoutKey)) {
|
|
83
|
+
if (!layoutCache.has(layoutKey) || refreshPersistedLayout && layoutCache.get(layoutKey) !== Layout) {
|
|
69
84
|
layoutCache.set(layoutKey, Layout);
|
|
70
85
|
}
|
|
71
86
|
const CachedLayout = layoutCache.get(layoutKey);
|
|
72
|
-
return createElement(CachedLayout, { key: layoutKey }, pageElement);
|
|
87
|
+
return createElement(CachedLayout, { key: layoutKey, ...layoutProps ?? {} }, pageElement);
|
|
73
88
|
}
|
|
74
|
-
return createElement(Layout,
|
|
89
|
+
return createElement(Layout, layoutProps, pageElement);
|
|
75
90
|
};
|
|
76
91
|
function createDeferred() {
|
|
77
92
|
let resolve;
|
|
@@ -80,46 +95,86 @@ function createDeferred() {
|
|
|
80
95
|
});
|
|
81
96
|
return { promise, resolve };
|
|
82
97
|
}
|
|
83
|
-
function
|
|
98
|
+
function useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef) {
|
|
99
|
+
const handleCoordinatorNavigate = useEffectEvent(async (request) => {
|
|
100
|
+
await navigate(request.href, {
|
|
101
|
+
isPopState: request.direction === "back",
|
|
102
|
+
pushHistory: request.direction === "forward",
|
|
103
|
+
skipViewTransition: request.source === "browser-router"
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
const handleCoordinatorReload = useEffectEvent(async (request) => {
|
|
108
|
+
if (activeNavigationRef.current || isNavigatingRef.current) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (request?.clearCache) {
|
|
112
|
+
clearLayoutCache();
|
|
113
|
+
}
|
|
114
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
115
|
+
await navigate(currentUrl, { moduleUrlOverride: request?.moduleUrl });
|
|
116
|
+
});
|
|
117
|
+
const handleCleanupBeforeHandoff = useEffectEvent(async () => {
|
|
118
|
+
runtimeActiveRef.current = false;
|
|
119
|
+
window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
|
|
120
|
+
});
|
|
84
121
|
useEffect(() => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
123
|
+
let unregisterRuntime = null;
|
|
124
|
+
const unregister = navigationRuntime.register({
|
|
125
|
+
owner: "react-router",
|
|
126
|
+
navigate: handleCoordinatorNavigate,
|
|
127
|
+
reloadCurrentPage: handleCoordinatorReload,
|
|
128
|
+
cleanupBeforeHandoff: async () => {
|
|
129
|
+
unregisterRuntime?.();
|
|
130
|
+
unregisterRuntime = null;
|
|
131
|
+
await handleCleanupBeforeHandoff();
|
|
91
132
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
133
|
+
});
|
|
134
|
+
unregisterRuntime = unregister;
|
|
135
|
+
navigationRuntime.claimOwnership("react-router");
|
|
136
|
+
runtimeActiveRef.current = true;
|
|
95
137
|
return () => {
|
|
96
|
-
|
|
138
|
+
runtimeActiveRef.current = false;
|
|
139
|
+
navigationRuntime.releaseOwnership("react-router");
|
|
140
|
+
unregisterRuntime?.();
|
|
141
|
+
unregisterRuntime = null;
|
|
97
142
|
};
|
|
98
|
-
}, [
|
|
143
|
+
}, [runtimeActiveRef]);
|
|
99
144
|
}
|
|
100
145
|
const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
101
146
|
const options = useMemo(() => ({ ...DEFAULT_OPTIONS, ...userOptions }), [userOptions]);
|
|
102
|
-
const [currentPage, setCurrentPage] = useState({
|
|
147
|
+
const [currentPage, setCurrentPage] = useState({
|
|
148
|
+
Component: page,
|
|
149
|
+
props: pageProps,
|
|
150
|
+
refreshPersistedLayout: false
|
|
151
|
+
});
|
|
103
152
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
153
|
+
const pendingRenderRef = useRef(null);
|
|
154
|
+
const activeNavigationRef = useRef(null);
|
|
155
|
+
const isNavigatingRef = useRef(false);
|
|
156
|
+
const runtimeActiveRef = useRef(true);
|
|
157
|
+
const pendingPointerNavigationRef = useRef(null);
|
|
158
|
+
const pendingHoverNavigationRef = useRef(null);
|
|
159
|
+
const queuedNavigationHrefRef = useRef(null);
|
|
160
|
+
const committedPathRef = useRef(
|
|
161
|
+
typeof window !== "undefined" ? window.location.pathname + window.location.search : ""
|
|
162
|
+
);
|
|
107
163
|
const previousUrlRef = useRef(typeof window !== "undefined" ? window.location.href : "");
|
|
108
164
|
useEffect(() => {
|
|
109
|
-
|
|
165
|
+
isNavigatingRef.current = isNavigating;
|
|
166
|
+
}, [isNavigating]);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
setCurrentPage({ Component: page, props: pageProps, refreshPersistedLayout: true });
|
|
110
169
|
}, [page, pageProps]);
|
|
111
170
|
useEffect(() => {
|
|
171
|
+
committedPathRef.current = window.location.pathname + window.location.search;
|
|
112
172
|
applyViewTransitionNames();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
renderDfd.current = null;
|
|
118
|
-
setPendingPage(null);
|
|
173
|
+
const pendingRender = pendingRenderRef.current;
|
|
174
|
+
if (pendingRender && currentPage.Component === pendingRender.page.Component && currentPage.props === pendingRender.page.props) {
|
|
175
|
+
pendingRender.resolve();
|
|
176
|
+
pendingRenderRef.current = null;
|
|
119
177
|
}
|
|
120
|
-
}, [currentPage, pendingPage]);
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (typeof window === "undefined") return;
|
|
123
178
|
const url = new URL(window.location.href);
|
|
124
179
|
const previousUrl = new URL(previousUrlRef.current);
|
|
125
180
|
if (url.href !== previousUrl.href) {
|
|
@@ -129,85 +184,313 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
|
|
|
129
184
|
});
|
|
130
185
|
previousUrlRef.current = url.href;
|
|
131
186
|
}
|
|
132
|
-
if (pendingScrollRestoreRef.current) {
|
|
133
|
-
const { url: targetUrl, isPopState } = pendingScrollRestoreRef.current;
|
|
134
|
-
restoreScrollPositions(targetUrl, isPopState);
|
|
135
|
-
pendingScrollRestoreRef.current = null;
|
|
136
|
-
}
|
|
137
187
|
}, [currentPage, options.scrollBehavior, options.smoothScroll]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
return () => {
|
|
190
|
+
activeNavigationRef.current?.cancel();
|
|
191
|
+
pendingRenderRef.current?.resolve();
|
|
192
|
+
pendingRenderRef.current = null;
|
|
193
|
+
queuedNavigationHrefRef.current = null;
|
|
194
|
+
};
|
|
195
|
+
}, []);
|
|
138
196
|
const navigate = useCallback(
|
|
139
|
-
async (url,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
197
|
+
async (url, navigationOptions = {}) => {
|
|
198
|
+
const {
|
|
199
|
+
isPopState = false,
|
|
200
|
+
pushHistory = false,
|
|
201
|
+
skipViewTransition = false,
|
|
202
|
+
moduleUrlOverride
|
|
203
|
+
} = navigationOptions;
|
|
204
|
+
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
205
|
+
const navigation = navigationRuntime.beginNavigationTransaction();
|
|
206
|
+
activeNavigationRef.current = navigation;
|
|
207
|
+
const navigationId = navigation.id;
|
|
208
|
+
const isStale = () => !navigation.isCurrent();
|
|
209
|
+
const commitPageData = (moduleUrl, props) => {
|
|
210
|
+
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
211
|
+
window.__ECO_PAGES__.page = {
|
|
212
|
+
module: moduleUrl,
|
|
213
|
+
props
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const preparePendingRender = (nextPage) => {
|
|
217
|
+
pendingRenderRef.current?.resolve();
|
|
218
|
+
const renderDfd = createDeferred();
|
|
219
|
+
pendingRenderRef.current = {
|
|
220
|
+
navigationId,
|
|
221
|
+
page: nextPage,
|
|
222
|
+
resolve: renderDfd.resolve
|
|
223
|
+
};
|
|
224
|
+
return renderDfd.promise;
|
|
225
|
+
};
|
|
226
|
+
let navigationCommitPromise = null;
|
|
227
|
+
try {
|
|
228
|
+
setIsNavigating(true);
|
|
229
|
+
const fetchedPage = await fetchPageDocument(url, { signal: navigation.signal });
|
|
230
|
+
if (isStale()) return;
|
|
231
|
+
if (!fetchedPage) {
|
|
232
|
+
window.location.href = url;
|
|
233
|
+
return;
|
|
149
234
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
235
|
+
const result = await loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath, {
|
|
236
|
+
moduleUrlOverride
|
|
237
|
+
});
|
|
238
|
+
if (isStale()) return;
|
|
239
|
+
if (result) {
|
|
240
|
+
const { Component, props, doc, finalPath, moduleUrl } = result;
|
|
241
|
+
const nextPage = { Component, props, refreshPersistedLayout: Boolean(moduleUrlOverride) };
|
|
242
|
+
const { cleanup: cleanupHead, flushRerunScripts } = await morphHead(doc);
|
|
243
|
+
const finalizeCommittedNavigation = () => {
|
|
244
|
+
committedPathRef.current = finalPath;
|
|
245
|
+
flushRerunScripts();
|
|
160
246
|
cleanupHead();
|
|
161
247
|
applyViewTransitionNames();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
248
|
+
restoreScrollPositions(finalPath, isPopState);
|
|
249
|
+
};
|
|
250
|
+
const commitNextPage = () => {
|
|
251
|
+
commitPageData(moduleUrl, props);
|
|
252
|
+
setCurrentPage(nextPage);
|
|
253
|
+
};
|
|
254
|
+
if (isStale()) {
|
|
255
|
+
cleanupHead();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
166
258
|
applyViewTransitionNames();
|
|
259
|
+
saveScrollPositions();
|
|
260
|
+
if (pushHistory) {
|
|
261
|
+
window.history.pushState(null, "", finalPath);
|
|
262
|
+
} else if (finalPath !== url) {
|
|
263
|
+
window.history.replaceState(null, "", finalPath);
|
|
264
|
+
}
|
|
265
|
+
if (!skipViewTransition && options.viewTransitions && document.startViewTransition) {
|
|
266
|
+
const renderPromise = preparePendingRender(nextPage);
|
|
267
|
+
navigationCommitPromise = new Promise((resolve) => {
|
|
268
|
+
document.startViewTransition(async () => {
|
|
269
|
+
try {
|
|
270
|
+
if (isStale()) {
|
|
271
|
+
if (pendingRenderRef.current?.navigationId === navigationId) {
|
|
272
|
+
pendingRenderRef.current.resolve();
|
|
273
|
+
pendingRenderRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
cleanupHead();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
startTransition(() => {
|
|
279
|
+
commitNextPage();
|
|
280
|
+
});
|
|
281
|
+
await renderPromise;
|
|
282
|
+
if (isStale()) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
finalizeCommittedNavigation();
|
|
286
|
+
} finally {
|
|
287
|
+
resolve();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
await navigationCommitPromise;
|
|
292
|
+
} else {
|
|
293
|
+
const renderPromise = preparePendingRender(nextPage);
|
|
294
|
+
commitNextPage();
|
|
295
|
+
await renderPromise;
|
|
296
|
+
if (isStale()) {
|
|
297
|
+
cleanupHead();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
finalizeCommittedNavigation();
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
if (isStale()) return;
|
|
304
|
+
const handled = await navigationRuntime.requestHandoff({
|
|
305
|
+
href: url,
|
|
306
|
+
finalHref: fetchedPage.finalPath,
|
|
307
|
+
direction: isPopState ? "back" : pushHistory ? "forward" : "replace",
|
|
308
|
+
source: "react-router",
|
|
309
|
+
targetOwner: "browser-router",
|
|
310
|
+
document: fetchedPage.doc,
|
|
311
|
+
html: fetchedPage.html,
|
|
312
|
+
isStaleSourceNavigation: isStale
|
|
313
|
+
});
|
|
314
|
+
if (!handled) {
|
|
315
|
+
window.location.assign(fetchedPage.finalPath);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!isStale()) {
|
|
319
|
+
setIsNavigating(false);
|
|
167
320
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
321
|
+
} finally {
|
|
322
|
+
const shouldReplayQueuedNavigation = activeNavigationRef.current?.id === navigationId;
|
|
323
|
+
const queuedNavigationHref = shouldReplayQueuedNavigation ? queuedNavigationHrefRef.current : null;
|
|
324
|
+
const queuedNavigationPath = queuedNavigationHref ? new URL(queuedNavigationHref, window.location.origin).pathname + new URL(queuedNavigationHref, window.location.origin).search : null;
|
|
325
|
+
navigation.complete();
|
|
326
|
+
if (activeNavigationRef.current?.id === navigationId) {
|
|
327
|
+
activeNavigationRef.current = null;
|
|
328
|
+
}
|
|
329
|
+
if (queuedNavigationHref && queuedNavigationPath !== committedPathRef.current) {
|
|
330
|
+
queuedNavigationHrefRef.current = null;
|
|
331
|
+
if (runtimeActiveRef.current) {
|
|
332
|
+
void navigate(queuedNavigationHref, { pushHistory: true });
|
|
333
|
+
} else {
|
|
334
|
+
void navigationRuntime.requestNavigation({
|
|
335
|
+
href: queuedNavigationHref,
|
|
336
|
+
direction: "forward",
|
|
337
|
+
source: "react-router"
|
|
338
|
+
}).then((handled) => {
|
|
339
|
+
if (!handled) {
|
|
340
|
+
window.location.assign(queuedNavigationHref);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
171
344
|
}
|
|
172
|
-
window.location.href = url;
|
|
173
345
|
}
|
|
174
|
-
setIsNavigating(false);
|
|
175
346
|
},
|
|
176
|
-
[options.viewTransitions
|
|
347
|
+
[options.viewTransitions]
|
|
177
348
|
);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
349
|
+
const getLinkFromEvent = useEffectEvent(
|
|
350
|
+
(event) => getAnchorFromNavigationEvent(event, options.linkSelector)
|
|
351
|
+
);
|
|
352
|
+
const getRecoveredPointerHref = useEffectEvent(() => {
|
|
353
|
+
const href = recoverPendingNavigationHref(
|
|
354
|
+
pendingPointerNavigationRef.current,
|
|
355
|
+
!!activeNavigationRef.current || isNavigatingRef.current,
|
|
356
|
+
performance.now()
|
|
357
|
+
);
|
|
358
|
+
if (!href) {
|
|
359
|
+
pendingPointerNavigationRef.current = null;
|
|
360
|
+
}
|
|
361
|
+
return href;
|
|
362
|
+
});
|
|
363
|
+
const getRecoveredHoverHref = useEffectEvent(() => {
|
|
364
|
+
const href = recoverPendingNavigationHref(
|
|
365
|
+
pendingHoverNavigationRef.current,
|
|
366
|
+
!!activeNavigationRef.current || isNavigatingRef.current,
|
|
367
|
+
performance.now()
|
|
368
|
+
);
|
|
369
|
+
if (!href) {
|
|
370
|
+
pendingHoverNavigationRef.current = null;
|
|
371
|
+
}
|
|
372
|
+
return href;
|
|
373
|
+
});
|
|
374
|
+
const handleHoverIntent = useEffectEvent((event) => {
|
|
375
|
+
if (!runtimeActiveRef.current) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const link = getLinkFromEvent(event);
|
|
379
|
+
if (!link) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const decision = getInterceptDecision(event, link, options);
|
|
383
|
+
if (!decision.shouldIntercept) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
pendingHoverNavigationRef.current = {
|
|
387
|
+
href: link.getAttribute("href"),
|
|
388
|
+
timestamp: performance.now()
|
|
389
|
+
};
|
|
390
|
+
queuedNavigationHrefRef.current = link.getAttribute("href");
|
|
391
|
+
});
|
|
392
|
+
const handlePointerDown = useEffectEvent((event) => {
|
|
393
|
+
if (!runtimeActiveRef.current) {
|
|
394
|
+
pendingPointerNavigationRef.current = null;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const link = getLinkFromEvent(event);
|
|
398
|
+
if (!link) {
|
|
399
|
+
pendingPointerNavigationRef.current = null;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const decision = getInterceptDecision(event, link, options);
|
|
403
|
+
pendingPointerNavigationRef.current = decision.shouldIntercept ? {
|
|
404
|
+
href: link.getAttribute("href"),
|
|
405
|
+
timestamp: performance.now()
|
|
406
|
+
} : null;
|
|
407
|
+
if (decision.shouldIntercept && (activeNavigationRef.current || isNavigatingRef.current)) {
|
|
408
|
+
queuedNavigationHrefRef.current = link.getAttribute("href");
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
const handleClick = useEffectEvent((event) => {
|
|
412
|
+
if (!runtimeActiveRef.current) {
|
|
413
|
+
pendingPointerNavigationRef.current = null;
|
|
414
|
+
pendingHoverNavigationRef.current = null;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const link = getLinkFromEvent(event);
|
|
418
|
+
if (!link) {
|
|
419
|
+
const recoveredHref = getRecoveredPointerHref() ?? getRecoveredHoverHref();
|
|
420
|
+
pendingPointerNavigationRef.current = null;
|
|
421
|
+
pendingHoverNavigationRef.current = null;
|
|
422
|
+
if (!recoveredHref) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (isSamePageHashNavigationHref(recoveredHref)) {
|
|
426
|
+
queuedNavigationHrefRef.current = null;
|
|
189
427
|
return;
|
|
190
428
|
}
|
|
191
429
|
event.preventDefault();
|
|
192
|
-
|
|
193
|
-
const
|
|
430
|
+
queuedNavigationHrefRef.current = null;
|
|
431
|
+
const recoveredUrl = new URL(recoveredHref, window.location.href);
|
|
432
|
+
navigate(recoveredUrl.pathname + recoveredUrl.search, { pushHistory: true });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!shouldInterceptClick(event, link, options)) {
|
|
194
436
|
if (options.debug) {
|
|
195
|
-
|
|
437
|
+
const decision = getInterceptDecision(event, link, options);
|
|
438
|
+
if (!decision.shouldIntercept) {
|
|
439
|
+
console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
|
|
440
|
+
}
|
|
196
441
|
}
|
|
197
|
-
|
|
198
|
-
|
|
442
|
+
pendingPointerNavigationRef.current = null;
|
|
443
|
+
pendingHoverNavigationRef.current = null;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
pendingPointerNavigationRef.current = null;
|
|
447
|
+
pendingHoverNavigationRef.current = null;
|
|
448
|
+
event.preventDefault();
|
|
449
|
+
queuedNavigationHrefRef.current = null;
|
|
450
|
+
const href = link.getAttribute("href");
|
|
451
|
+
const url = new URL(href, window.location.origin);
|
|
452
|
+
if (options.debug) {
|
|
453
|
+
console.debug("[EcoRouter] Intercepting navigation:", url.pathname + url.search);
|
|
454
|
+
}
|
|
455
|
+
navigate(url.pathname + url.search, { pushHistory: true });
|
|
456
|
+
});
|
|
457
|
+
const handlePopState = useEffectEvent(() => {
|
|
458
|
+
if (!runtimeActiveRef.current) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
navigate(window.location.pathname + window.location.search, { isPopState: true });
|
|
462
|
+
});
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
const onHoverIntent = (event) => {
|
|
465
|
+
handleHoverIntent(event);
|
|
466
|
+
};
|
|
467
|
+
const onPointerDown = (event) => {
|
|
468
|
+
handlePointerDown(event);
|
|
469
|
+
};
|
|
470
|
+
const onClick = (event) => {
|
|
471
|
+
handleClick(event);
|
|
199
472
|
};
|
|
200
|
-
const
|
|
201
|
-
|
|
473
|
+
const onPopState = () => {
|
|
474
|
+
handlePopState();
|
|
202
475
|
};
|
|
203
|
-
document.addEventListener("
|
|
204
|
-
|
|
476
|
+
document.addEventListener("mouseover", onHoverIntent, true);
|
|
477
|
+
document.addEventListener("pointerover", onHoverIntent, true);
|
|
478
|
+
document.addEventListener("mousemove", onHoverIntent, true);
|
|
479
|
+
document.addEventListener("pointermove", onHoverIntent, true);
|
|
480
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
481
|
+
document.addEventListener("click", onClick, true);
|
|
482
|
+
window.addEventListener("popstate", onPopState);
|
|
205
483
|
return () => {
|
|
206
|
-
document.removeEventListener("
|
|
207
|
-
|
|
484
|
+
document.removeEventListener("mouseover", onHoverIntent, true);
|
|
485
|
+
document.removeEventListener("pointerover", onHoverIntent, true);
|
|
486
|
+
document.removeEventListener("mousemove", onHoverIntent, true);
|
|
487
|
+
document.removeEventListener("pointermove", onHoverIntent, true);
|
|
488
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
489
|
+
document.removeEventListener("click", onClick, true);
|
|
490
|
+
window.removeEventListener("popstate", onPopState);
|
|
208
491
|
};
|
|
209
|
-
}, [
|
|
210
|
-
|
|
492
|
+
}, []);
|
|
493
|
+
useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef);
|
|
211
494
|
return createElement(
|
|
212
495
|
RouterContext.Provider,
|
|
213
496
|
{ value: { navigate, isNavigating } },
|
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,12 +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
|
-
### Refactoring
|
|
10
|
-
|
|
11
|
-
- Updated `package.json` dependencies to align with the new core adapter and esbuild build adapter versions.
|
|
12
|
-
- Internal peer dependency declarations updated for React 18+ and the new `@ecopages/core` API 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';
|
package/src/adapter.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Router adapter for React integration.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ReactRouterAdapter } from '@ecopages/react/router-adapter';
|
|
7
|
-
import type { EcoRouterOptions } from './types.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Creates a ReactRouterAdapter for EcoPages React Router.
|
|
11
|
-
* Use this with the React plugin to enable SPA navigation.
|
|
12
|
-
*
|
|
13
|
-
* @param options - Router configuration options
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* import { reactPlugin } from '@ecopages/react';
|
|
17
|
-
* import { ecoRouter } from '@ecopages/react-router';
|
|
18
|
-
*
|
|
19
|
-
* export default {
|
|
20
|
-
* integrations: [reactPlugin({ router: ecoRouter() })],
|
|
21
|
-
* };
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* // Disable view transitions
|
|
27
|
-
* reactPlugin({ router: ecoRouter({ viewTransitions: false }) })
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export function ecoRouter(options?: EcoRouterOptions): ReactRouterAdapter {
|
|
31
|
-
return {
|
|
32
|
-
name: 'eco-router',
|
|
33
|
-
bundle: {
|
|
34
|
-
importPath: '@ecopages/react-router/browser.ts',
|
|
35
|
-
outputName: 'react-router-esm',
|
|
36
|
-
externals: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
37
|
-
},
|
|
38
|
-
importMapKey: '@ecopages/react-router',
|
|
39
|
-
components: {
|
|
40
|
-
router: 'EcoRouter',
|
|
41
|
-
pageContent: 'PageContent',
|
|
42
|
-
},
|
|
43
|
-
getRouterProps(page: string, props: string): string {
|
|
44
|
-
const optionsStr = options ? `, options: ${JSON.stringify(options)}` : '';
|
|
45
|
-
return `{ page: ${page}, pageProps: ${props}${optionsStr} }`;
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|