@brust/runtime-dom 2.0.0-alpha.27 → 2.0.0-alpha.29
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/package.json +1 -1
- package/src/nav/nav.ts +18 -14
- package/src/nav/swap.ts +7 -0
package/package.json
CHANGED
package/src/nav/nav.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPA navigation (design): a qualifying link click, `navigate()` or
|
|
2
2
|
// back/forward fetches the real HTML document and swaps it in place, so the page shows exactly what a full load of
|
|
3
3
|
// that URL shows (§2). Anything that cannot be swapped is a full load. Events: `brust:nav:click|load|success|error`.
|
|
4
|
-
import { applyHead, classicScripts, planHead, runScripts, swapBody } from './swap'
|
|
4
|
+
import { applyHead, classicScripts, incomingImages, planHead, runScripts, swapBody } from './swap'
|
|
5
5
|
|
|
6
6
|
type Trigger = 'link' | 'navigate' | 'popstate'
|
|
7
7
|
export interface NavBridge { navigate(url: string, opts?: { replace?: boolean }): Promise<void>; clearCache(): void }
|
|
@@ -71,7 +71,8 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// ---- the progress bar (§10.2): only for a navigation still loading 150 ms after `load`; outside <body>, so no swap
|
|
74
|
-
// removes it, and its <style> is runtime-added, so the head reconcile keeps it
|
|
74
|
+
// removes it, and its <style> is runtime-added, so the head reconcile keeps it. The <style> goes first in the head
|
|
75
|
+
// and its rules are one attribute selector each, so an app rule on div[data-brust-bar] wins ----
|
|
75
76
|
let bar: Element | undefined
|
|
76
77
|
let css: HTMLStyleElement | undefined
|
|
77
78
|
let wait: ReturnType<typeof setTimeout> | undefined
|
|
@@ -81,12 +82,12 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
|
|
|
81
82
|
if (!bar && html.getAttribute('data-brust-progress') !== 'off') wait = setTimeout(() => {
|
|
82
83
|
if (!css) {
|
|
83
84
|
;(css = document.createElement('style')).textContent =
|
|
84
|
-
`div[${BAR}]{position:fixed;z-index:2147483647;top:0;left:0;width:100%;height:
|
|
85
|
-
`div[${BAR}=done]{animation:none;transform:none;opacity:0;transition:transform .2s,opacity .3s .2s}@keyframes brust-bar{to{transform:scaleX(.
|
|
86
|
-
`@media (prefers-reduced-motion:reduce){div[${BAR}]{animation:none;transform:scaleX(.
|
|
85
|
+
`div[${BAR}]{position:fixed;z-index:2147483647;top:0;left:0;width:100%;height:var(--brust-progress-height,2px);pointer-events:none;background:var(--brust-progress-color,#2f7cf6);transform-origin:0;transform:scaleX(0);animation:brust-bar 8s cubic-bezier(.1,.8,.2,1) forwards}` +
|
|
86
|
+
`div[${BAR}=done]{animation:none;transform:none;opacity:0;transition:transform .2s,opacity .3s .2s}@keyframes brust-bar{to{transform:scaleX(.8)}}` +
|
|
87
|
+
`@media (prefers-reduced-motion:reduce){div[${BAR}]{animation:none;transform:scaleX(.8)}div[${BAR}=done]{transition:none}}`
|
|
87
88
|
css.setAttribute(BAR, '')
|
|
88
89
|
}
|
|
89
|
-
if (!css.isConnected) document.head.
|
|
90
|
+
if (!css.isConnected) document.head.prepend(css)
|
|
90
91
|
html.append((bar = document.createElement('div')))
|
|
91
92
|
bar.setAttribute(BAR, '')
|
|
92
93
|
}, 150)
|
|
@@ -149,12 +150,14 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
|
|
|
149
150
|
const doc = /html/.test(res.type) ? new DOMParser().parseFromString(res.text, 'text/html') : null
|
|
150
151
|
if (doc?.querySelector(NAV)?.getAttribute('data-brust-build') !== build) throw new Error(`not a brust page of this build`)
|
|
151
152
|
|
|
152
|
-
// §3 step 4: everything the new page needs before it is shown. The store tags first,
|
|
153
|
-
// store seeds from the first tag of its name; a live store never re-reads one)
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// §3 step 4: everything the new page needs before it is shown. The store tags first, at the end of the body, after
|
|
154
|
+
// the old ones (a store seeds from the first tag of its name; a live store never re-reads one): an old-page island
|
|
155
|
+
// that hydrates while the modules load still finds its own page's tag, and a store new to this page, first read
|
|
156
|
+
// by one of its modules, finds the new one. The swap removes them with the old body tail. Then the module scripts
|
|
157
|
+
// in document order (one at a time: the order they register in is the order the server wrote), then the
|
|
158
|
+
// stylesheets the head plan inserted, with `media="not all"`: loaded and parsed, applied to nothing until the swap.
|
|
156
159
|
const stores = Array.from(doc.querySelectorAll('script[data-brust-store]'), (s) => document.importNode(s, true))
|
|
157
|
-
document.
|
|
160
|
+
document.body.append(...stores)
|
|
158
161
|
added.push(...stores)
|
|
159
162
|
for (const s of doc.querySelectorAll('script[type=module][src]')) {
|
|
160
163
|
if (!s.matches(NAV)) await load(new URL(s.getAttribute('src')!, url).href)
|
|
@@ -186,8 +189,9 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
|
|
|
186
189
|
shown = location.href
|
|
187
190
|
land(url, pos.get(pop!))
|
|
188
191
|
}
|
|
189
|
-
// §10.3: the swap inside a view transition (a root cross-fade). Everything slow is done by now. The first 8 images
|
|
190
|
-
// the
|
|
192
|
+
// §10.3: the swap inside a view transition (a root cross-fade). Everything slow is done by now. The first 8 images the
|
|
193
|
+
// swap puts in (the changed level's, so a kept layout's never take a slot) that are not lazy and not on screen
|
|
194
|
+
// already are preloaded (the same candidate: srcset, sizes,
|
|
191
195
|
// crossorigin and referrerpolicy copied) and get 100 ms in all to decode, so none pops in after the fade. `swap`
|
|
192
196
|
// runs once: a throw from it rejects updateCallbackDone and takes the error path; a throw from startViewTransition
|
|
193
197
|
// itself is a direct swap.
|
|
@@ -198,7 +202,7 @@ export function boot(load: Load = (u) => import(/* @vite-ignore */ u)): () => vo
|
|
|
198
202
|
const onScreen = new Set([...document.querySelectorAll('img')].flatMap((i) => [i.src, i.currentSrc]).filter(Boolean))
|
|
199
203
|
await Promise.race([
|
|
200
204
|
new Promise((r) => setTimeout(r, 100)),
|
|
201
|
-
Promise.all(
|
|
205
|
+
Promise.all(incomingImages(doc)
|
|
202
206
|
.filter((i) => i.getAttribute('loading') !== 'lazy' && (i.hasAttribute('src') || i.hasAttribute('srcset')) && !onScreen.has(abs(i.getAttribute('src'))))
|
|
203
207
|
.slice(0, 8)
|
|
204
208
|
.map((i) => {
|
package/src/nav/swap.ts
CHANGED
|
@@ -95,6 +95,13 @@ function tail(body: HTMLElement): ChildNode[] | null {
|
|
|
95
95
|
return tag && out
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** The images `swapBody(doc)` will put in, before it runs: the changed level's, or the whole body's. */
|
|
99
|
+
export function incomingImages(doc: Document): HTMLImageElement[] {
|
|
100
|
+
const b = readChain(doc)
|
|
101
|
+
const i = swapLevel(readChain(document), b)
|
|
102
|
+
return all(i && tail(document.body) && tail(doc.body) ? between(b![i]!) : [...doc.body.childNodes], 'img') as HTMLImageElement[]
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
/**
|
|
99
106
|
* Swap the body to `doc`'s (§5.9): from the first level that changed, or the whole body (with the attributes of
|
|
100
107
|
* `<html>` and `<body>`) when the root changed or a chain cannot be trusted. Returns the classic scripts of the new
|