@ecopages/browser-router 0.2.0-alpha.5 → 0.2.0-alpha.6
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 +9 -7
- package/README.md +36 -36
- package/package.json +2 -2
- package/src/client/eco-router.d.ts +35 -1
- package/src/client/eco-router.js +335 -75
- package/src/client/eco-router.ts +430 -100
- package/src/client/services/dom-swapper.d.ts +23 -0
- package/src/client/services/dom-swapper.js +210 -38
- package/src/client/services/dom-swapper.ts +296 -45
- package/src/client/services/view-transition-manager.d.ts +7 -1
- package/src/client/services/view-transition-manager.js +10 -5
- package/src/client/services/view-transition-manager.ts +13 -7
- package/src/client/types.d.ts +3 -0
- package/src/client/types.ts +4 -0
package/src/client/eco-router.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { getEcoNavigationRuntime } from "@ecopages/core/router/navigation-coordinator";
|
|
2
|
+
import {
|
|
3
|
+
getAnchorFromNavigationEvent,
|
|
4
|
+
recoverPendingNavigationHref
|
|
5
|
+
} from "@ecopages/core/router/link-intent";
|
|
1
6
|
import { DEFAULT_OPTIONS } from "./types.js";
|
|
2
7
|
import { DomSwapper, ScrollManager, ViewTransitionManager, PrefetchManager } from "./services/index.js";
|
|
3
8
|
class EcoRouter {
|
|
4
9
|
options;
|
|
5
|
-
|
|
10
|
+
unregisterNavigationRuntime = null;
|
|
11
|
+
started = false;
|
|
12
|
+
pendingNavigations = 0;
|
|
13
|
+
pendingPointerNavigation = null;
|
|
14
|
+
pendingHoverNavigation = null;
|
|
15
|
+
queuedNavigationHref = null;
|
|
6
16
|
domSwapper;
|
|
7
17
|
scrollManager;
|
|
8
18
|
viewTransitionManager;
|
|
@@ -19,8 +29,162 @@ class EcoRouter {
|
|
|
19
29
|
});
|
|
20
30
|
}
|
|
21
31
|
this.handleClick = this.handleClick.bind(this);
|
|
32
|
+
this.handleHoverIntent = this.handleHoverIntent.bind(this);
|
|
33
|
+
this.handlePointerDown = this.handlePointerDown.bind(this);
|
|
22
34
|
this.handlePopState = this.handlePopState.bind(this);
|
|
23
35
|
}
|
|
36
|
+
getLinkFromEvent(event) {
|
|
37
|
+
return getAnchorFromNavigationEvent(event, this.options.linkSelector);
|
|
38
|
+
}
|
|
39
|
+
canInterceptLink(event, link) {
|
|
40
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return null;
|
|
41
|
+
if (event.button !== 0) return null;
|
|
42
|
+
const target = link.getAttribute("target");
|
|
43
|
+
if (target && target !== "_self") return null;
|
|
44
|
+
if (link.hasAttribute(this.options.reloadAttribute)) return null;
|
|
45
|
+
if (link.hasAttribute("download")) return null;
|
|
46
|
+
const href = link.getAttribute("href");
|
|
47
|
+
if (!href) return null;
|
|
48
|
+
if (href.startsWith("#")) return null;
|
|
49
|
+
if (href.startsWith("javascript:")) return null;
|
|
50
|
+
const url = new URL(href, window.location.origin);
|
|
51
|
+
if (!this.isSameOrigin(url)) return null;
|
|
52
|
+
return href;
|
|
53
|
+
}
|
|
54
|
+
getRecoveredPointerHref() {
|
|
55
|
+
const href = recoverPendingNavigationHref(
|
|
56
|
+
this.pendingPointerNavigation,
|
|
57
|
+
this.pendingNavigations > 0,
|
|
58
|
+
performance.now()
|
|
59
|
+
);
|
|
60
|
+
if (!href) {
|
|
61
|
+
this.pendingPointerNavigation = null;
|
|
62
|
+
}
|
|
63
|
+
return href;
|
|
64
|
+
}
|
|
65
|
+
getRecoveredHoverHref() {
|
|
66
|
+
const href = recoverPendingNavigationHref(
|
|
67
|
+
this.pendingHoverNavigation,
|
|
68
|
+
this.pendingNavigations > 0,
|
|
69
|
+
performance.now()
|
|
70
|
+
);
|
|
71
|
+
if (!href) {
|
|
72
|
+
this.pendingHoverNavigation = null;
|
|
73
|
+
}
|
|
74
|
+
return href;
|
|
75
|
+
}
|
|
76
|
+
isAnotherNavigationRuntimeActive() {
|
|
77
|
+
const ownerState = getEcoNavigationRuntime(window).getOwnerState();
|
|
78
|
+
return ownerState.owner !== "none" && ownerState.owner !== "browser-router" && ownerState.canHandleSpaNavigation;
|
|
79
|
+
}
|
|
80
|
+
getDocumentOwner(doc) {
|
|
81
|
+
return getEcoNavigationRuntime(window).resolveDocumentOwner(doc, "browser-router");
|
|
82
|
+
}
|
|
83
|
+
adoptDocumentOwner(doc) {
|
|
84
|
+
getEcoNavigationRuntime(window).adoptDocumentOwner(doc, "browser-router");
|
|
85
|
+
}
|
|
86
|
+
syncDocumentElementAttributes(newDocument) {
|
|
87
|
+
const currentHtml = document.documentElement;
|
|
88
|
+
const nextHtml = newDocument.documentElement;
|
|
89
|
+
for (const attribute of Array.from(currentHtml.attributes)) {
|
|
90
|
+
if (!nextHtml.hasAttribute(attribute.name)) {
|
|
91
|
+
currentHtml.removeAttribute(attribute.name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const attribute of Array.from(nextHtml.attributes)) {
|
|
95
|
+
if (currentHtml.getAttribute(attribute.name) !== attribute.value) {
|
|
96
|
+
currentHtml.setAttribute(attribute.name, attribute.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
reloadDocument(url) {
|
|
101
|
+
window.location.assign(url.href);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Commits a fully fetched document into the live page.
|
|
105
|
+
*
|
|
106
|
+
* When browser-router accepts a handoff from another runtime, it delays source
|
|
107
|
+
* runtime cleanup until the incoming document has been prepared and is ready to
|
|
108
|
+
* commit. That ordering avoids the blank-page window we previously hit when a
|
|
109
|
+
* delegated navigation went stale after the source runtime had already torn
|
|
110
|
+
* itself down.
|
|
111
|
+
*/
|
|
112
|
+
async commitDocumentNavigation(url, direction, newDocument, options = {}) {
|
|
113
|
+
const previousUrl = new URL(window.location.href);
|
|
114
|
+
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
115
|
+
const isStaleNavigation = options.isStaleNavigation ?? (() => false);
|
|
116
|
+
const currentDocumentOwner = navigationRuntime.resolveDocumentOwner(document, "browser-router");
|
|
117
|
+
const newDocumentOwner = navigationRuntime.resolveDocumentOwner(newDocument, "browser-router");
|
|
118
|
+
const activeOwner = navigationRuntime.getOwnerState().owner;
|
|
119
|
+
const shouldCleanupCurrentOwner = currentDocumentOwner !== newDocumentOwner && currentDocumentOwner !== "browser-router" && activeOwner === currentDocumentOwner;
|
|
120
|
+
let shouldReload = false;
|
|
121
|
+
const beforeSwapEvent = {
|
|
122
|
+
url,
|
|
123
|
+
direction,
|
|
124
|
+
newDocument,
|
|
125
|
+
reload: () => {
|
|
126
|
+
shouldReload = true;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
document.dispatchEvent(new CustomEvent("eco:before-swap", { detail: beforeSwapEvent }));
|
|
130
|
+
if (isStaleNavigation()) return;
|
|
131
|
+
if (shouldReload) {
|
|
132
|
+
if (shouldCleanupCurrentOwner) {
|
|
133
|
+
await navigationRuntime.cleanupOwner(currentDocumentOwner);
|
|
134
|
+
}
|
|
135
|
+
if (isStaleNavigation()) return;
|
|
136
|
+
this.reloadDocument(url);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const useViewTransitions = this.options.viewTransitions;
|
|
140
|
+
await this.domSwapper.preloadStylesheets(newDocument);
|
|
141
|
+
if (isStaleNavigation()) return;
|
|
142
|
+
if (shouldCleanupCurrentOwner) {
|
|
143
|
+
await navigationRuntime.cleanupOwner(currentDocumentOwner);
|
|
144
|
+
}
|
|
145
|
+
if (isStaleNavigation()) return;
|
|
146
|
+
const commitSwap = () => {
|
|
147
|
+
if (isStaleNavigation()) return;
|
|
148
|
+
if (this.options.updateHistory && direction === "forward") {
|
|
149
|
+
window.history.pushState({}, "", url.href);
|
|
150
|
+
} else if (direction === "replace") {
|
|
151
|
+
window.history.replaceState({}, "", url.href);
|
|
152
|
+
}
|
|
153
|
+
this.syncDocumentElementAttributes(newDocument);
|
|
154
|
+
this.domSwapper.morphHead(newDocument);
|
|
155
|
+
if (useViewTransitions && !this.domSwapper.shouldReplaceBodyForRerunScripts()) {
|
|
156
|
+
this.domSwapper.morphBody(newDocument);
|
|
157
|
+
} else {
|
|
158
|
+
this.domSwapper.replaceBody(newDocument);
|
|
159
|
+
}
|
|
160
|
+
this.domSwapper.flushRerunScripts();
|
|
161
|
+
this.scrollManager.handleScroll(url, previousUrl);
|
|
162
|
+
};
|
|
163
|
+
if (useViewTransitions) {
|
|
164
|
+
await this.viewTransitionManager.transition(commitSwap);
|
|
165
|
+
} else {
|
|
166
|
+
commitSwap();
|
|
167
|
+
}
|
|
168
|
+
if (isStaleNavigation()) return;
|
|
169
|
+
navigationRuntime.adoptDocumentOwner(newDocument, "browser-router");
|
|
170
|
+
const afterSwapEvent = {
|
|
171
|
+
url,
|
|
172
|
+
direction
|
|
173
|
+
};
|
|
174
|
+
document.dispatchEvent(new CustomEvent("eco:after-swap", { detail: afterSwapEvent }));
|
|
175
|
+
this.prefetchManager?.observeNewLinks();
|
|
176
|
+
if (options.html) {
|
|
177
|
+
this.prefetchManager?.cacheVisitedPage(url.href, options.html);
|
|
178
|
+
}
|
|
179
|
+
requestAnimationFrame(() => {
|
|
180
|
+
if (isStaleNavigation()) return;
|
|
181
|
+
document.dispatchEvent(
|
|
182
|
+
new CustomEvent("eco:page-load", {
|
|
183
|
+
detail: { url, direction }
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
24
188
|
/**
|
|
25
189
|
* Starts the router and begins intercepting navigation.
|
|
26
190
|
*
|
|
@@ -28,30 +192,87 @@ class EcoRouter {
|
|
|
28
192
|
* back/forward buttons. Also starts the prefetch manager if configured.
|
|
29
193
|
*/
|
|
30
194
|
start() {
|
|
31
|
-
|
|
195
|
+
if (this.started) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
199
|
+
document.addEventListener("mouseover", this.handleHoverIntent, true);
|
|
200
|
+
document.addEventListener("pointerover", this.handleHoverIntent, true);
|
|
201
|
+
document.addEventListener("mousemove", this.handleHoverIntent, true);
|
|
202
|
+
document.addEventListener("pointermove", this.handleHoverIntent, true);
|
|
203
|
+
document.addEventListener("pointerdown", this.handlePointerDown, true);
|
|
204
|
+
document.addEventListener("click", this.handleClick, true);
|
|
32
205
|
window.addEventListener("popstate", this.handlePopState);
|
|
33
206
|
this.prefetchManager?.start();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
207
|
+
this.unregisterNavigationRuntime?.();
|
|
208
|
+
this.unregisterNavigationRuntime = navigationRuntime.register({
|
|
209
|
+
owner: "browser-router",
|
|
210
|
+
navigate: async (request) => {
|
|
211
|
+
await this.performNavigation(
|
|
212
|
+
new URL(request.href, window.location.origin),
|
|
213
|
+
request.direction ?? "forward"
|
|
214
|
+
);
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
handoffNavigation: async (request) => {
|
|
218
|
+
const { isStaleNavigation, complete } = this.beginNavigationTransaction();
|
|
219
|
+
if (isStaleNavigation()) return true;
|
|
220
|
+
try {
|
|
221
|
+
await this.commitDocumentNavigation(
|
|
222
|
+
new URL(request.finalHref ?? request.href, window.location.origin),
|
|
223
|
+
request.direction ?? "forward",
|
|
224
|
+
request.document,
|
|
225
|
+
{ html: request.html, isStaleNavigation }
|
|
226
|
+
);
|
|
227
|
+
} finally {
|
|
228
|
+
complete();
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
},
|
|
232
|
+
reloadCurrentPage: async (request) => {
|
|
233
|
+
if (this.pendingNavigations > 0) return;
|
|
234
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
235
|
+
if (request?.clearCache) {
|
|
236
|
+
this.prefetchManager?.invalidate(currentUrl);
|
|
237
|
+
}
|
|
238
|
+
await this.performNavigation(new URL(currentUrl, window.location.origin), "replace");
|
|
239
|
+
},
|
|
240
|
+
cleanupBeforeHandoff: async () => {
|
|
241
|
+
this.cancelNavigationTransaction();
|
|
39
242
|
}
|
|
40
|
-
|
|
41
|
-
|
|
243
|
+
});
|
|
244
|
+
this.adoptDocumentOwner(document);
|
|
42
245
|
const initialHtml = document.documentElement.outerHTML;
|
|
43
246
|
this.prefetchManager?.cacheVisitedPage(window.location.href, initialHtml);
|
|
247
|
+
this.started = true;
|
|
44
248
|
}
|
|
45
249
|
/**
|
|
46
250
|
* Stops the router and cleans up all event listeners.
|
|
47
251
|
* After calling this, navigation will fall back to full page reloads.
|
|
48
252
|
*/
|
|
49
253
|
stop() {
|
|
50
|
-
|
|
254
|
+
if (!this.started) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.cancelNavigationTransaction();
|
|
258
|
+
document.removeEventListener("mouseover", this.handleHoverIntent, true);
|
|
259
|
+
document.removeEventListener("pointerover", this.handleHoverIntent, true);
|
|
260
|
+
document.removeEventListener("mousemove", this.handleHoverIntent, true);
|
|
261
|
+
document.removeEventListener("pointermove", this.handleHoverIntent, true);
|
|
262
|
+
document.removeEventListener("pointerdown", this.handlePointerDown, true);
|
|
263
|
+
document.removeEventListener("click", this.handleClick, true);
|
|
51
264
|
window.removeEventListener("popstate", this.handlePopState);
|
|
52
265
|
this.prefetchManager?.stop();
|
|
53
|
-
|
|
54
|
-
|
|
266
|
+
this.unregisterNavigationRuntime?.();
|
|
267
|
+
this.unregisterNavigationRuntime = null;
|
|
268
|
+
this.started = false;
|
|
269
|
+
this.pendingHoverNavigation = null;
|
|
270
|
+
this.pendingPointerNavigation = null;
|
|
271
|
+
this.queuedNavigationHref = null;
|
|
272
|
+
const win = window;
|
|
273
|
+
if (win[ACTIVE_ROUTER_KEY] === this) {
|
|
274
|
+
delete win[ACTIVE_ROUTER_KEY];
|
|
275
|
+
}
|
|
55
276
|
}
|
|
56
277
|
/**
|
|
57
278
|
* Programmatic navigation.
|
|
@@ -88,24 +309,63 @@ class EcoRouter {
|
|
|
88
309
|
* Uses `event.composedPath()` to correctly detect clicks on anchors inside
|
|
89
310
|
* Shadow DOM boundaries (Web Components).
|
|
90
311
|
*/
|
|
312
|
+
handlePointerDown(event) {
|
|
313
|
+
const link = this.getLinkFromEvent(event);
|
|
314
|
+
if (!link) {
|
|
315
|
+
this.pendingPointerNavigation = null;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const href = this.canInterceptLink(event, link);
|
|
319
|
+
this.pendingPointerNavigation = href ? {
|
|
320
|
+
href,
|
|
321
|
+
timestamp: performance.now()
|
|
322
|
+
} : null;
|
|
323
|
+
if (href && this.pendingNavigations > 0) {
|
|
324
|
+
this.queuedNavigationHref = href;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
handleHoverIntent(event) {
|
|
328
|
+
const link = this.getLinkFromEvent(event);
|
|
329
|
+
if (!link) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const href = this.canInterceptLink(event, link);
|
|
333
|
+
if (!href) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.pendingHoverNavigation = {
|
|
337
|
+
href,
|
|
338
|
+
timestamp: performance.now()
|
|
339
|
+
};
|
|
340
|
+
if (this.pendingNavigations > 0) {
|
|
341
|
+
this.queuedNavigationHref = href;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
91
344
|
handleClick(event) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (event.button !== 0) return;
|
|
98
|
-
const target = link.getAttribute("target");
|
|
99
|
-
if (target && target !== "_self") return;
|
|
100
|
-
if (link.hasAttribute(this.options.reloadAttribute)) return;
|
|
101
|
-
if (link.hasAttribute("download")) return;
|
|
102
|
-
const href = link.getAttribute("href");
|
|
345
|
+
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
346
|
+
const link = this.getLinkFromEvent(event);
|
|
347
|
+
const href = link ? this.canInterceptLink(event, link) : this.getRecoveredPointerHref() ?? this.getRecoveredHoverHref();
|
|
348
|
+
this.pendingPointerNavigation = null;
|
|
349
|
+
this.pendingHoverNavigation = null;
|
|
103
350
|
if (!href) return;
|
|
104
|
-
|
|
105
|
-
if (
|
|
351
|
+
this.queuedNavigationHref = null;
|
|
352
|
+
if (this.isAnotherNavigationRuntimeActive()) {
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
event.stopImmediatePropagation();
|
|
355
|
+
void navigationRuntime.requestNavigation({
|
|
356
|
+
href,
|
|
357
|
+
direction: "forward",
|
|
358
|
+
source: "browser-router"
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
106
362
|
const url = new URL(href, window.location.origin);
|
|
107
|
-
if (!this.isSameOrigin(url)) return;
|
|
108
363
|
event.preventDefault();
|
|
364
|
+
if (this.pendingNavigations > 0) {
|
|
365
|
+
this.queuedNavigationHref = href;
|
|
366
|
+
this.cancelNavigationTransaction();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
109
369
|
this.performNavigation(url, "forward");
|
|
110
370
|
}
|
|
111
371
|
/**
|
|
@@ -113,6 +373,7 @@ class EcoRouter {
|
|
|
113
373
|
* Triggered by the History API's popstate event.
|
|
114
374
|
*/
|
|
115
375
|
handlePopState(_event) {
|
|
376
|
+
if (this.isAnotherNavigationRuntimeActive()) return;
|
|
116
377
|
const url = new URL(window.location.href);
|
|
117
378
|
this.performNavigation(url, "back");
|
|
118
379
|
}
|
|
@@ -123,6 +384,17 @@ class EcoRouter {
|
|
|
123
384
|
isSameOrigin(url) {
|
|
124
385
|
return url.origin === window.location.origin;
|
|
125
386
|
}
|
|
387
|
+
cancelNavigationTransaction() {
|
|
388
|
+
getEcoNavigationRuntime(window).cancelCurrentNavigationTransaction();
|
|
389
|
+
}
|
|
390
|
+
beginNavigationTransaction() {
|
|
391
|
+
const transaction = getEcoNavigationRuntime(window).beginNavigationTransaction();
|
|
392
|
+
return {
|
|
393
|
+
isStaleNavigation: () => !transaction.isCurrent(),
|
|
394
|
+
signal: transaction.signal,
|
|
395
|
+
complete: () => transaction.complete()
|
|
396
|
+
};
|
|
397
|
+
}
|
|
126
398
|
/**
|
|
127
399
|
* Executes the core navigation flow.
|
|
128
400
|
*
|
|
@@ -142,64 +414,45 @@ class EcoRouter {
|
|
|
142
414
|
* @param direction - Navigation direction ('forward', 'back', or 'replace')
|
|
143
415
|
*/
|
|
144
416
|
async performNavigation(url, direction) {
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
|
|
417
|
+
this.pendingNavigations++;
|
|
418
|
+
const { isStaleNavigation, signal, complete } = this.beginNavigationTransaction();
|
|
419
|
+
let queuedNavigationHref = null;
|
|
148
420
|
try {
|
|
149
|
-
const html = await this.fetchPage(url,
|
|
421
|
+
const html = await this.fetchPage(url, signal);
|
|
422
|
+
if (isStaleNavigation()) return;
|
|
150
423
|
const newDocument = this.domSwapper.parseHTML(html, url);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
newDocument,
|
|
156
|
-
reload: () => {
|
|
157
|
-
shouldReload = true;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
document.dispatchEvent(new CustomEvent("eco:before-swap", { detail: beforeSwapEvent }));
|
|
161
|
-
if (shouldReload) {
|
|
162
|
-
window.location.href = url.href;
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (this.options.updateHistory && direction === "forward") {
|
|
166
|
-
window.history.pushState({}, "", url.href);
|
|
167
|
-
} else if (direction === "replace") {
|
|
168
|
-
window.history.replaceState({}, "", url.href);
|
|
169
|
-
}
|
|
170
|
-
const useViewTransitions = this.options.viewTransitions;
|
|
171
|
-
await this.domSwapper.preloadStylesheets(newDocument);
|
|
172
|
-
if (useViewTransitions) {
|
|
173
|
-
await this.viewTransitionManager.transition(() => {
|
|
174
|
-
this.domSwapper.morphHead(newDocument);
|
|
175
|
-
this.domSwapper.morphBody(newDocument);
|
|
176
|
-
this.scrollManager.handleScroll(url, previousUrl);
|
|
177
|
-
});
|
|
178
|
-
} else {
|
|
179
|
-
this.domSwapper.morphHead(newDocument);
|
|
180
|
-
this.domSwapper.replaceBody(newDocument);
|
|
181
|
-
this.scrollManager.handleScroll(url, previousUrl);
|
|
182
|
-
}
|
|
183
|
-
const afterSwapEvent = {
|
|
184
|
-
url,
|
|
185
|
-
direction
|
|
186
|
-
};
|
|
187
|
-
document.dispatchEvent(new CustomEvent("eco:after-swap", { detail: afterSwapEvent }));
|
|
188
|
-
this.prefetchManager?.observeNewLinks();
|
|
189
|
-
this.prefetchManager?.cacheVisitedPage(url.href, html);
|
|
190
|
-
requestAnimationFrame(() => {
|
|
191
|
-
document.dispatchEvent(
|
|
192
|
-
new CustomEvent("eco:page-load", {
|
|
193
|
-
detail: { url, direction }
|
|
194
|
-
})
|
|
195
|
-
);
|
|
424
|
+
if (isStaleNavigation()) return;
|
|
425
|
+
await this.commitDocumentNavigation(url, direction, newDocument, {
|
|
426
|
+
html,
|
|
427
|
+
isStaleNavigation
|
|
196
428
|
});
|
|
197
429
|
} catch (error) {
|
|
430
|
+
if (isStaleNavigation()) return;
|
|
198
431
|
if (error instanceof Error && error.name === "AbortError") {
|
|
199
432
|
return;
|
|
200
433
|
}
|
|
201
434
|
console.error("[ecopages] Navigation failed:", error);
|
|
202
435
|
window.location.href = url.href;
|
|
436
|
+
} finally {
|
|
437
|
+
complete();
|
|
438
|
+
this.pendingNavigations--;
|
|
439
|
+
const navigationRuntime = getEcoNavigationRuntime(window);
|
|
440
|
+
if (!navigationRuntime.hasPendingNavigationTransaction()) {
|
|
441
|
+
queuedNavigationHref = this.queuedNavigationHref;
|
|
442
|
+
this.queuedNavigationHref = null;
|
|
443
|
+
}
|
|
444
|
+
if (queuedNavigationHref && queuedNavigationHref !== window.location.pathname + window.location.search) {
|
|
445
|
+
const ownerState = navigationRuntime.getOwnerState();
|
|
446
|
+
if (ownerState.owner !== "none" && ownerState.owner !== "browser-router" && ownerState.canHandleSpaNavigation) {
|
|
447
|
+
void navigationRuntime.requestNavigation({
|
|
448
|
+
href: queuedNavigationHref,
|
|
449
|
+
direction: "forward",
|
|
450
|
+
source: "browser-router"
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
void this.performNavigation(new URL(queuedNavigationHref, window.location.origin), "forward");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
203
456
|
}
|
|
204
457
|
}
|
|
205
458
|
/**
|
|
@@ -227,8 +480,15 @@ class EcoRouter {
|
|
|
227
480
|
return response.text();
|
|
228
481
|
}
|
|
229
482
|
}
|
|
483
|
+
const ACTIVE_ROUTER_KEY = "__ecopages_browser_router__";
|
|
230
484
|
function createRouter(options) {
|
|
485
|
+
const win = window;
|
|
486
|
+
const existingRouter = win[ACTIVE_ROUTER_KEY];
|
|
487
|
+
if (existingRouter) {
|
|
488
|
+
return existingRouter;
|
|
489
|
+
}
|
|
231
490
|
const router = new EcoRouter(options);
|
|
491
|
+
win[ACTIVE_ROUTER_KEY] = router;
|
|
232
492
|
router.start();
|
|
233
493
|
return router;
|
|
234
494
|
}
|