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