@markless/router 0.5.0 → 0.6.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/dist/create-server-entry-DaYssO9x.js +1331 -0
- package/dist/index-BjxpxRL7.d.ts +248 -0
- package/dist/index.d.ts +2 -233
- package/dist/index.js +64 -15
- package/dist/request-locals-z3rJTizs.js +90 -0
- package/dist/route-manifest-DMizh42B.js +456 -0
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/typescript-plugin.js +1 -1
- package/dist/vite/runtime/create-route-discovery.js +1 -1
- package/dist/vite/runtime/create-server-entry.d.ts +16 -0
- package/dist/vite/runtime/create-server-entry.js +2 -658
- package/dist/vite/runtime/mdx-route.d.ts +21 -2
- package/dist/vite/runtime/mdx-route.js +5 -2
- package/dist/vite.js +205 -37
- package/package.json +4 -4
- package/src/vite/entries/fragment-entry.ts +283 -29
- package/dist/fragment-navigation-CIiNnFFc.js +0 -323
- package/dist/link-attributes-nY1Qkmx2.js +0 -7
- package/dist/request-files-Ca8E0FOo.js +0 -233
- package/dist/request-locals-BFPF6Vdm.js +0 -28
- package/dist/route-manifest-Bw_nFohL.js +0 -189
|
@@ -3,6 +3,7 @@ import type { ModulePreloadInput } from '@markless/web/render-to-string';
|
|
|
3
3
|
|
|
4
4
|
export interface FragmentNavigationConfig {
|
|
5
5
|
readonly entry: string;
|
|
6
|
+
readonly entryImports: readonly string[];
|
|
6
7
|
readonly linkAttribute: string;
|
|
7
8
|
readonly prefetchAttribute: string;
|
|
8
9
|
readonly replaceAttribute: string;
|
|
@@ -11,9 +12,14 @@ export interface FragmentNavigationConfig {
|
|
|
11
12
|
readonly retireProperty: string;
|
|
12
13
|
readonly streamExecutorGlobal: string;
|
|
13
14
|
readonly streamedArmSelector: string;
|
|
14
|
-
readonly
|
|
15
|
+
readonly requestPrefix: string;
|
|
15
16
|
readonly regionEndMarker: string;
|
|
16
17
|
readonly appendMarker: string;
|
|
18
|
+
// Close and open the page's nodes when the document's own element is the container; what is outside is the document's.
|
|
19
|
+
readonly pageEndComment: string;
|
|
20
|
+
readonly pageStartComment: string;
|
|
21
|
+
// The container's own attributes, never carried from the leaving page's element to the next one.
|
|
22
|
+
readonly containerAttributes: readonly [container: string, paths: string];
|
|
17
23
|
// 'document' apps swap the whole body: their shell renders from the request URL.
|
|
18
24
|
readonly documentLoads: boolean;
|
|
19
25
|
// A mouse fetches after resting this long on a link; press, focus and touch fetch at once.
|
|
@@ -23,7 +29,7 @@ export interface FragmentNavigationConfig {
|
|
|
23
29
|
readonly prefetchConnections: number;
|
|
24
30
|
// Paths the server answers with endpoints, not pages: never fetched before a click or as a fragment.
|
|
25
31
|
readonly endpointPrefixes: readonly string[];
|
|
26
|
-
// At most this many visible-link fragments are fetched at idle on fast connections,
|
|
32
|
+
// At most this many visible-link fragments are fetched at idle on fast connections, in document order; 0 keeps HTML to intent only.
|
|
27
33
|
readonly idleFragments: number;
|
|
28
34
|
readonly idleWithoutConnectionInfo: boolean;
|
|
29
35
|
// false: nothing is fetched before a click.
|
|
@@ -64,9 +70,11 @@ export type FragmentNavigationState = {
|
|
|
64
70
|
readonly prefetch: (url: URL) => void;
|
|
65
71
|
readonly parse: (url: string, text: string) => FragmentFetched;
|
|
66
72
|
readonly idleDownloads: boolean;
|
|
67
|
-
// Schedules the idle tier: at idle,
|
|
73
|
+
// Schedules the idle tier: at idle, fetch a few page fragments of the links in view; their code waits for intent.
|
|
68
74
|
readonly rescan: () => void;
|
|
69
75
|
readonly scrolled: Set<Element>;
|
|
76
|
+
// Code the intent started preloading, by absolute href, settling once each file has arrived.
|
|
77
|
+
readonly arriving: Map<string, Promise<unknown>>;
|
|
70
78
|
};
|
|
71
79
|
|
|
72
80
|
export type FragmentScroll =
|
|
@@ -167,12 +175,15 @@ function regionOf(state: FragmentNavigationState, doc: Document): Element | null
|
|
|
167
175
|
(
|
|
168
176
|
doc.querySelector(`script[type="${state.config.routeScriptType}"]`) ??
|
|
169
177
|
doc.querySelector('script[data-markless-router-link-resumer]')
|
|
170
|
-
)?.closest(
|
|
178
|
+
)?.closest(`[${state.config.containerAttributes[0]}]`) ?? null
|
|
171
179
|
);
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
function retire(state: FragmentNavigationState, root: Element): void {
|
|
175
|
-
for (const container of [
|
|
183
|
+
for (const container of [
|
|
184
|
+
root,
|
|
185
|
+
...root.querySelectorAll(`[${state.config.containerAttributes[0]}]`),
|
|
186
|
+
]) {
|
|
176
187
|
const retireContainer = (container as unknown as Record<string, unknown>)[
|
|
177
188
|
state.config.retireProperty
|
|
178
189
|
];
|
|
@@ -191,13 +202,157 @@ function reportAsync(error: unknown): void {
|
|
|
191
202
|
});
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
// The JavaScript MIME types of the HTML spec, which a browser runs as classic scripts.
|
|
206
|
+
const JAVASCRIPT_TYPE =
|
|
207
|
+
/^(?:|(?:text|application)\/(?:x-)?(?:java|ecma)script|text\/(?:javascript1\.[0-5]|jscript|livescript))$/;
|
|
208
|
+
|
|
209
|
+
function scriptKind(script: HTMLScriptElement): 'classic' | 'module' | undefined {
|
|
210
|
+
const type = script.getAttribute('type');
|
|
211
|
+
const language = script.getAttribute('language');
|
|
212
|
+
const essence = (type ?? (language ? `text/${language}` : ''))
|
|
213
|
+
.split(';')[0]!
|
|
214
|
+
.trim()
|
|
215
|
+
.toLowerCase();
|
|
216
|
+
if (essence === 'module') return 'module';
|
|
217
|
+
if (!JAVASCRIPT_TYPE.test(essence) || script.hasAttribute('nomodule')) return undefined;
|
|
218
|
+
return 'classic';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A parsed or adopted script never runs; a fresh element with its attributes and text does.
|
|
222
|
+
function runnable(d: Document, script: HTMLScriptElement): HTMLScriptElement {
|
|
197
223
|
const clone = d.createElement('script');
|
|
198
224
|
for (const attribute of script.attributes) clone.setAttribute(attribute.name, attribute.value);
|
|
199
225
|
clone.textContent = script.textContent;
|
|
200
|
-
|
|
226
|
+
return clone;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const settles = (script: HTMLScriptElement) =>
|
|
230
|
+
new Promise<unknown>((done) => {
|
|
231
|
+
script.addEventListener('load', done, { once: true });
|
|
232
|
+
script.addEventListener('error', done, { once: true });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// External files this document already ran: a page that includes one again keeps the loaded copy.
|
|
236
|
+
const ranFiles = new WeakMap<Document, Set<string>>();
|
|
237
|
+
|
|
238
|
+
function filesRan(d: Document): Set<string> {
|
|
239
|
+
let files = ranFiles.get(d);
|
|
240
|
+
if (!files) ranFiles.set(d, (files = new Set()));
|
|
241
|
+
for (const script of d.scripts) if (script.src && scriptKind(script)) files.add(script.src);
|
|
242
|
+
return files;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type LoadListener = {
|
|
246
|
+
readonly target: EventTarget;
|
|
247
|
+
readonly type: string;
|
|
248
|
+
readonly listener: EventListenerOrEventListenerObject;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// The document fired DOMContentLoaded and load already: listeners the page's scripts add are called once those scripts ran.
|
|
252
|
+
function captureLoadListeners(d: Document, w: Window, into: LoadListener[]): () => void {
|
|
253
|
+
const targets: Array<readonly [EventTarget, string]> = [];
|
|
254
|
+
if (d.readyState !== 'loading') targets.push([d, 'DOMContentLoaded']);
|
|
255
|
+
if (d.readyState === 'complete') targets.push([w, 'load']);
|
|
256
|
+
const restores = targets.map(([target, type]) => {
|
|
257
|
+
const own = Object.getOwnPropertyDescriptor(target, 'addEventListener');
|
|
258
|
+
const add = target.addEventListener;
|
|
259
|
+
target.addEventListener = function (
|
|
260
|
+
this: EventTarget,
|
|
261
|
+
kind: string,
|
|
262
|
+
listener: EventListenerOrEventListenerObject | null,
|
|
263
|
+
options?: boolean | AddEventListenerOptions,
|
|
264
|
+
) {
|
|
265
|
+
if (kind === type && listener && this === target) into.push({ target, type, listener });
|
|
266
|
+
return add.call(this, kind, listener, options);
|
|
267
|
+
};
|
|
268
|
+
return () => {
|
|
269
|
+
if (own) Object.defineProperty(target, 'addEventListener', own);
|
|
270
|
+
else delete (target as { addEventListener?: unknown }).addEventListener;
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
return () => {
|
|
274
|
+
for (const restore of restores) restore();
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function callLoadListeners(listeners: readonly LoadListener[], type: string): void {
|
|
279
|
+
const event = new Event(type);
|
|
280
|
+
for (const entry of listeners) {
|
|
281
|
+
if (entry.type !== type) continue;
|
|
282
|
+
try {
|
|
283
|
+
if (typeof entry.listener === 'function') entry.listener.call(entry.target, event);
|
|
284
|
+
else entry.listener.handleEvent(event);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
reportAsync(error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A landing's order: blocking classic scripts one by one, async as they arrive, then deferred and module scripts, then load listeners.
|
|
292
|
+
async function runScripts(
|
|
293
|
+
d: Document,
|
|
294
|
+
scripts: readonly HTMLScriptElement[],
|
|
295
|
+
ran: Set<string>,
|
|
296
|
+
current: () => boolean,
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
const w = d.defaultView!;
|
|
299
|
+
const listeners: LoadListener[] = [];
|
|
300
|
+
const release = captureLoadListeners(d, w, listeners);
|
|
301
|
+
try {
|
|
302
|
+
const deferred: HTMLScriptElement[] = [];
|
|
303
|
+
for (const script of scripts) {
|
|
304
|
+
const kind = scriptKind(script);
|
|
305
|
+
if (!kind || !script.isConnected) continue;
|
|
306
|
+
const src = script.hasAttribute('src') ? script.src : '';
|
|
307
|
+
if (src && ran.has(src)) continue;
|
|
308
|
+
if (src) ran.add(src);
|
|
309
|
+
const async = script.hasAttribute('async') && (src !== '' || kind === 'module');
|
|
310
|
+
if (!async && (kind === 'module' || (src && script.hasAttribute('defer')))) {
|
|
311
|
+
deferred.push(script);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const clone = runnable(d, script);
|
|
315
|
+
if (async || !src) {
|
|
316
|
+
script.replaceWith(clone);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
clone.async = false;
|
|
320
|
+
const done = settles(clone);
|
|
321
|
+
script.replaceWith(clone);
|
|
322
|
+
await done;
|
|
323
|
+
if (!current()) return;
|
|
324
|
+
}
|
|
325
|
+
let last: Promise<unknown> | undefined;
|
|
326
|
+
let tail: HTMLScriptElement | undefined;
|
|
327
|
+
for (const script of deferred) {
|
|
328
|
+
if (!script.isConnected) continue;
|
|
329
|
+
const clone = runnable(d, script);
|
|
330
|
+
clone.async = false;
|
|
331
|
+
last = clone.src ? settles(clone) : undefined;
|
|
332
|
+
tail = clone;
|
|
333
|
+
script.replaceWith(clone);
|
|
334
|
+
}
|
|
335
|
+
// An inline module fires no load event, so a marker queued after the last one says when it ran.
|
|
336
|
+
if (tail && !last) {
|
|
337
|
+
const key = `__marklessScriptsRan${Math.random().toString(36).slice(2)}`;
|
|
338
|
+
const host = w as unknown as Record<string, unknown>;
|
|
339
|
+
last = new Promise((done) => (host[key] = done));
|
|
340
|
+
const marker = d.createElement('script');
|
|
341
|
+
marker.type = 'module';
|
|
342
|
+
marker.async = false;
|
|
343
|
+
if (tail.nonce) marker.nonce = tail.nonce;
|
|
344
|
+
marker.textContent = `window.${key}();`;
|
|
345
|
+
d.head.append(marker);
|
|
346
|
+
await last;
|
|
347
|
+
delete host[key];
|
|
348
|
+
marker.remove();
|
|
349
|
+
} else await last;
|
|
350
|
+
if (!current()) return;
|
|
351
|
+
callLoadListeners(listeners, 'DOMContentLoaded');
|
|
352
|
+
callLoadListeners(listeners, 'load');
|
|
353
|
+
} finally {
|
|
354
|
+
release();
|
|
355
|
+
}
|
|
201
356
|
}
|
|
202
357
|
|
|
203
358
|
function loaded(w: Window, links: readonly HTMLLinkElement[]): Promise<unknown> {
|
|
@@ -235,9 +390,8 @@ async function mergeHead(d: Document, next: Document) {
|
|
|
235
390
|
else d.head.prepend(node);
|
|
236
391
|
if (node.localName === 'link' && (node as HTMLLinkElement).rel === 'stylesheet')
|
|
237
392
|
stylesheets.push(node as HTMLLinkElement);
|
|
238
|
-
// A kept
|
|
239
|
-
if (node.localName === 'script'
|
|
240
|
-
scripts.push(node as HTMLScriptElement);
|
|
393
|
+
// A kept head script already ran on this document; only one the destination adds runs.
|
|
394
|
+
if (node.localName === 'script') scripts.push(node as HTMLScriptElement);
|
|
241
395
|
}
|
|
242
396
|
cursor = node;
|
|
243
397
|
}
|
|
@@ -266,12 +420,62 @@ function syncHtmlAttributes(d: Document, next: Document): void {
|
|
|
266
420
|
serverHtmlAttributes.set(d, new Set(incoming.getAttributeNames()));
|
|
267
421
|
}
|
|
268
422
|
|
|
423
|
+
// Set from the page's config at the start of every swap.
|
|
424
|
+
let containerAttribute = '';
|
|
425
|
+
|
|
269
426
|
const isContainer = (node: Node): boolean =>
|
|
270
|
-
node.nodeType === 1 && (node as Element).hasAttribute(
|
|
427
|
+
node.nodeType === 1 && (node as Element).hasAttribute(containerAttribute);
|
|
428
|
+
|
|
429
|
+
function pageEnd(host: Node, data: string): Comment | undefined {
|
|
430
|
+
for (let node = host.lastChild; node; node = node.previousSibling)
|
|
431
|
+
if (node.nodeType === 8 && (node as Comment).data === data) return node as Comment;
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function pageStart(host: Node, data: string): Comment | undefined {
|
|
436
|
+
for (let node = host.firstChild; node; node = node.nextSibling)
|
|
437
|
+
if (node.nodeType === 8 && (node as Comment).data === data) return node as Comment;
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function nodesBefore(d: Document, node: Node): DocumentFragment {
|
|
442
|
+
const nodes = d.createDocumentFragment();
|
|
443
|
+
while (node.previousSibling) nodes.prepend(node.previousSibling);
|
|
444
|
+
return nodes;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function nodesAfter(d: Document, node: Node): DocumentFragment {
|
|
448
|
+
const nodes = d.createDocumentFragment();
|
|
449
|
+
while (node.nextSibling) nodes.append(node.nextSibling);
|
|
450
|
+
return nodes;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const scriptsIn = (element: Element): Element[] =>
|
|
454
|
+
element.localName === 'script' ? [element] : [...element.querySelectorAll('script')];
|
|
455
|
+
|
|
456
|
+
const serverHostAttributes = new WeakMap<Document, Set<string>>();
|
|
457
|
+
|
|
458
|
+
// The incoming container is a fresh element; attributes a script set on the outgoing one stay.
|
|
459
|
+
function carryHostAttributes(
|
|
460
|
+
d: Document,
|
|
461
|
+
from: Element,
|
|
462
|
+
to: Element,
|
|
463
|
+
own: readonly string[],
|
|
464
|
+
): void {
|
|
465
|
+
const server = serverHostAttributes.get(d);
|
|
466
|
+
serverHostAttributes.set(d, new Set(to.getAttributeNames()));
|
|
467
|
+
for (const attribute of from.attributes)
|
|
468
|
+
if (
|
|
469
|
+
!to.hasAttribute(attribute.name) &&
|
|
470
|
+
!server?.has(attribute.name) &&
|
|
471
|
+
!own.includes(attribute.name)
|
|
472
|
+
)
|
|
473
|
+
to.setAttribute(attribute.name, attribute.value);
|
|
474
|
+
}
|
|
271
475
|
|
|
272
476
|
// A document app's shell has no runtime, so it is morphed in place: unchanged chrome keeps its nodes.
|
|
273
477
|
// Containers are always replaced whole: their DOM is the new page's resume payload's to locate.
|
|
274
|
-
function morphChildren(d: Document, from:
|
|
478
|
+
function morphChildren(d: Document, from: Node, to: Node, fresh: Element[]): void {
|
|
275
479
|
let cursor = from.firstChild;
|
|
276
480
|
const skipContainers = () => {
|
|
277
481
|
while (cursor && isContainer(cursor)) {
|
|
@@ -409,7 +613,7 @@ async function streamAppends(
|
|
|
409
613
|
}
|
|
410
614
|
}
|
|
411
615
|
flush(true);
|
|
412
|
-
end.parentNode?.removeChild(end);
|
|
616
|
+
if ((end as Comment).data !== state.config.pageEndComment) end.parentNode?.removeChild(end);
|
|
413
617
|
return signal.aborted ? undefined : all;
|
|
414
618
|
}
|
|
415
619
|
|
|
@@ -422,6 +626,7 @@ export async function navigate(
|
|
|
422
626
|
const state = (d as FragmentHost).__marklessFragmentNav;
|
|
423
627
|
const w = d.defaultView;
|
|
424
628
|
if (!state || !w) return;
|
|
629
|
+
containerAttribute = state.config.containerAttributes[0];
|
|
425
630
|
const session = sessionFor(d);
|
|
426
631
|
const id = ++session.navigation;
|
|
427
632
|
session.streaming?.abort();
|
|
@@ -442,12 +647,24 @@ export async function navigate(
|
|
|
442
647
|
const documentLoads = state.config.documentLoads;
|
|
443
648
|
const incoming = documentLoads ? next.body : regionOf(state, next);
|
|
444
649
|
const outgoing = documentLoads ? d.body : regionOf(state, d);
|
|
445
|
-
const scope = documentLoads ? d.body : outgoing?.parentElement;
|
|
650
|
+
const scope = documentLoads && !isContainer(d.body) ? d.body : outgoing?.parentElement;
|
|
446
651
|
if (!incoming || !outgoing || !scope) {
|
|
447
652
|
fallback(fetched.url);
|
|
448
653
|
return;
|
|
449
654
|
}
|
|
655
|
+
// The destination shows once its own code has arrived, so its first interaction never waits on a fetch.
|
|
656
|
+
const code = [...next.head.querySelectorAll<HTMLLinkElement>('link[rel="modulepreload"]')].map(
|
|
657
|
+
(link) => state.arriving.get(new URL(link.getAttribute('href') ?? '', d.baseURI).href),
|
|
658
|
+
);
|
|
659
|
+
for (const item of state.routes[url.pathname] ?? [])
|
|
660
|
+
code.push(
|
|
661
|
+
state.arriving.get(
|
|
662
|
+
new URL(typeof item === 'string' ? item : item.href, d.baseURI).href,
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
const ranFiles = filesRan(d);
|
|
450
666
|
const head = await mergeHead(d, next);
|
|
667
|
+
await Promise.race([Promise.all(code), new Promise((resolve) => w.setTimeout(resolve, 3000))]);
|
|
451
668
|
if (id !== session.navigation) return taken.abort();
|
|
452
669
|
let restore = scroll;
|
|
453
670
|
if (mode === 'traverse') {
|
|
@@ -486,30 +703,66 @@ export async function navigate(
|
|
|
486
703
|
for (const element of d.querySelectorAll(state.config.streamedArmSelector)) element.remove();
|
|
487
704
|
state.shown = pageKey(new URL(w.location.href));
|
|
488
705
|
if (documentLoads) syncHtmlAttributes(d, next);
|
|
489
|
-
const fresh: Element[] = [];
|
|
490
706
|
let region: Element;
|
|
491
|
-
|
|
707
|
+
let end: Comment;
|
|
708
|
+
let scripts: Element[];
|
|
709
|
+
if (documentLoads && !isContainer(d.body)) {
|
|
710
|
+
const fresh: Element[] = [];
|
|
492
711
|
region = d.body;
|
|
493
712
|
morphChildren(d, region, incoming, fresh);
|
|
713
|
+
scripts = fresh.flatMap(scriptsIn);
|
|
714
|
+
region.append((end = d.createComment('')));
|
|
494
715
|
} else {
|
|
495
716
|
region = d.adoptNode(incoming);
|
|
496
|
-
outgoing.
|
|
497
|
-
|
|
717
|
+
const leavingEnd = pageEnd(outgoing, state.config.pageEndComment);
|
|
718
|
+
if (leavingEnd) {
|
|
719
|
+
carryHostAttributes(d, outgoing, region, state.config.containerAttributes);
|
|
720
|
+
end =
|
|
721
|
+
pageEnd(region, state.config.pageEndComment) ??
|
|
722
|
+
region.appendChild(d.createComment(state.config.pageEndComment));
|
|
723
|
+
// The document's own nodes after the page stay; a document app morphs them to its next shell.
|
|
724
|
+
const focused = d.activeElement as HTMLElement | null;
|
|
725
|
+
const kept = nodesAfter(d, leavingEnd);
|
|
726
|
+
const arriving = nodesAfter(d, end);
|
|
727
|
+
const fresh: Element[] = [];
|
|
728
|
+
if (documentLoads) morphChildren(d, kept, arriving, fresh);
|
|
729
|
+
const leavingStart = pageStart(outgoing, state.config.pageStartComment);
|
|
730
|
+
let start = pageStart(region, state.config.pageStartComment);
|
|
731
|
+
if (leavingStart || start) {
|
|
732
|
+
start ??= region.insertBefore(
|
|
733
|
+
d.createComment(state.config.pageStartComment),
|
|
734
|
+
region.firstChild,
|
|
735
|
+
);
|
|
736
|
+
const keptBefore = leavingStart
|
|
737
|
+
? nodesBefore(d, leavingStart)
|
|
738
|
+
: d.createDocumentFragment();
|
|
739
|
+
const arrivingBefore = nodesBefore(d, start);
|
|
740
|
+
if (documentLoads) morphChildren(d, keptBefore, arrivingBefore, fresh);
|
|
741
|
+
region.insertBefore(keptBefore, start);
|
|
742
|
+
}
|
|
743
|
+
scripts = [...scriptsIn(region), ...fresh.flatMap(scriptsIn)];
|
|
744
|
+
region.append(kept);
|
|
745
|
+
outgoing.replaceWith(region);
|
|
746
|
+
// The document's own nodes moved to the new element; focus inside them stays put.
|
|
747
|
+
if (focused && focused !== d.activeElement && region.contains(focused))
|
|
748
|
+
focused.focus({ preventScroll: true });
|
|
749
|
+
} else {
|
|
750
|
+
scripts = scriptsIn(region);
|
|
751
|
+
outgoing.replaceWith(region);
|
|
752
|
+
region.after((end = d.createComment('')));
|
|
753
|
+
}
|
|
498
754
|
}
|
|
499
755
|
// 'instant': a `scroll-behavior: smooth` page would otherwise animate every landing.
|
|
500
756
|
for (const box of offsets)
|
|
501
757
|
sameBox(scope, box)?.scrollTo({ top: box.top, left: box.left, behavior: 'instant' });
|
|
502
|
-
const end = d.createComment('');
|
|
503
|
-
if (documentLoads) region.append(end);
|
|
504
|
-
else region.after(end);
|
|
505
758
|
for (const element of head.stale) element.remove();
|
|
506
759
|
if (next.title && d.title !== next.title) d.title = next.title;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
760
|
+
const ran = runScripts(
|
|
761
|
+
d,
|
|
762
|
+
[...head.scripts, ...(scripts as HTMLScriptElement[])],
|
|
763
|
+
ranFiles,
|
|
764
|
+
() => id === session.navigation,
|
|
765
|
+
);
|
|
513
766
|
const active = d.activeElement as HTMLElement | null;
|
|
514
767
|
if (active && active !== d.body && !region.contains(active)) active.blur?.();
|
|
515
768
|
(region.querySelector('[autofocus]') as HTMLElement | null)?.focus();
|
|
@@ -540,4 +793,5 @@ export async function navigate(
|
|
|
540
793
|
if (w.requestIdleCallback) w.requestIdleCallback(idle, { timeout: 2000 });
|
|
541
794
|
else w.setTimeout(idle, 200);
|
|
542
795
|
}, reportAsync);
|
|
796
|
+
await ran;
|
|
543
797
|
}
|