@webjsdev/core 0.7.50 → 0.7.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/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -20
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/component/lifecycle.js +1481 -0
- package/src/component/reactive.js +171 -0
- package/src/component/server-element.js +248 -0
- package/src/component.js +2 -1896
- package/src/render-client/parts.js +2007 -0
- package/src/render-client/reconciler.js +388 -0
- package/src/render-client/template-compiler.js +525 -0
- package/src/render-client.js +1 -2938
- package/src/render-server/attrs.js +160 -0
- package/src/render-server/dsd.js +445 -0
- package/src/render-server/html-scan.js +402 -0
- package/src/render-server/slots.js +213 -0
- package/src/render-server/stream.js +163 -0
- package/src/render-server/template-renderer.js +923 -0
- package/src/render-server/text.js +163 -0
- package/src/render-server.js +6 -2405
- package/src/router-client/anchors.js +44 -0
- package/src/router-client/boundaries.js +221 -0
- package/src/router-client/constants.js +187 -0
- package/src/router-client/diagnostics.js +377 -0
- package/src/router-client/dom-differ.js +709 -0
- package/src/router-client/dom-parse.js +150 -0
- package/src/router-client/events.js +139 -0
- package/src/router-client/fetch-apply.js +387 -0
- package/src/router-client/form-encoder.js +135 -0
- package/src/router-client/frames.js +329 -0
- package/src/router-client/head-merge.js +344 -0
- package/src/router-client/nav-error.js +105 -0
- package/src/router-client/navigator.js +781 -0
- package/src/router-client/prefetch.js +824 -0
- package/src/router-client/scroll.js +259 -0
- package/src/router-client/snapshot-cache.js +97 -0
- package/src/router-client/state.js +92 -0
- package/src/router-client/stream.js +206 -0
- package/src/router-client/swap.js +451 -0
- package/src/router-client/upgrade.js +261 -0
- package/src/router-client/view-transition.js +223 -0
- package/src/router-client.d.ts +10 -1
- package/src/router-client.js +75 -5345
- package/src/slot/assignment.js +304 -0
- package/src/slot/interception.js +558 -0
- package/src/slot/polyfills.js +365 -0
- package/src/slot/project.js +432 -0
- package/src/slot/sensors.js +303 -0
- package/src/slot/state.js +245 -0
- package/src/slot/symbols.js +85 -0
- package/src/slot.js +7 -2199
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client router: upgrade.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of the pre-split `router-client.js`; that barrel holds
|
|
5
|
+
* the router's full contract and the public entry points.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { buildHaveHeader, collectBoundaries, planBoundarySwap } from './boundaries.js';
|
|
10
|
+
import { FALLBACK_MARKER_KEY, FRAME_TOP, LIVE_ATTRS } from './constants.js';
|
|
11
|
+
import { isPreBootNavigation } from './diagnostics.js';
|
|
12
|
+
import { applyOptimisticLoading, blurOutgoingFocus, diffElementInPlace, keyOf, reconcileChildren, restoreOptimistic } from './dom-differ.js';
|
|
13
|
+
import { parseHTML, resetParseProbe } from './dom-parse.js';
|
|
14
|
+
import { onPopState, onSubmit } from './events.js';
|
|
15
|
+
import { buildSubmitFormData, encodeSubmitBody, getSubmitAction, getSubmitEnctype, getSubmitMethod } from './form-encoder.js';
|
|
16
|
+
import { activeFrameId, clearFormBusy, clearFrameBusy, markFormBusy, markFrameBusy, resolveTargetFrameId } from './frames.js';
|
|
17
|
+
import { addNewHeadElements, cloneScriptWithCorrectNonce, mergeHead } from './head-merge.js';
|
|
18
|
+
|
|
19
|
+
import { eligibleAnchorHref, prefetch, prefetchAnchor, prefetchCache, prefetchHasHoverPointer, prefetchMode, prefetchSaysSaveData, prefetchSuppressed, prefetchTake } from './prefetch.js';
|
|
20
|
+
import { snapshotCache } from './snapshot-cache.js';
|
|
21
|
+
import { applyStreamedResolve, readStreamedShell, streamBoundariesProgressively, takeResolveUnit } from './stream.js';
|
|
22
|
+
import { applySwap } from './swap.js';
|
|
23
|
+
import { regraftPermanentElements, regraftPermanentInSlice, regraftedPermanents, runWithTransition, viewTransitionsEnabled } from './view-transition.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Global MutationObserver that upgrades any custom element inserted into
|
|
27
|
+
* the document. Safety net: if our diff / replaceChildren / View
|
|
28
|
+
* Transitions ever leave an un-upgraded element behind, this catches it.
|
|
29
|
+
*/
|
|
30
|
+
let upgradeObserver = null;
|
|
31
|
+
|
|
32
|
+
export function ensureUpgradeObserver() {
|
|
33
|
+
if (upgradeObserver || typeof MutationObserver === 'undefined' || typeof customElements === 'undefined') return;
|
|
34
|
+
upgradeObserver = new MutationObserver((mutations) => {
|
|
35
|
+
for (const m of mutations) {
|
|
36
|
+
for (const node of m.addedNodes) {
|
|
37
|
+
if (node.nodeType !== 1) continue;
|
|
38
|
+
const el = /** @type {Element} */ (node);
|
|
39
|
+
if (el.tagName?.includes('-')) customElements.upgrade(el);
|
|
40
|
+
for (const child of el.querySelectorAll('*')) {
|
|
41
|
+
if (child.tagName?.includes('-')) customElements.upgrade(child);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
upgradeObserver.observe(document.body, { childList: true, subtree: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Re-upgrade custom elements between a marker pair after a transitioned
|
|
51
|
+
* swap settles. The View Transitions API snapshots and replaces DOM, so
|
|
52
|
+
* elements can need a re-upgrade once the animation finishes.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ start: Comment, end: Comment | null } | undefined} range
|
|
55
|
+
*/
|
|
56
|
+
export function upgradeCustomElementsInRange(range) {
|
|
57
|
+
if (!range || !range.start) return;
|
|
58
|
+
for (let n = range.start.nextSibling; n && n !== range.end; n = n.nextSibling) {
|
|
59
|
+
if (n.nodeType === 1) upgradeCustomElements(/** @type {Element} */ (n));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {Element} container */
|
|
64
|
+
export function upgradeCustomElements(container) {
|
|
65
|
+
if (typeof customElements === 'undefined') return;
|
|
66
|
+
upgradeTree(container);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @param {Element | DocumentFragment} root */
|
|
70
|
+
export function upgradeTree(root) {
|
|
71
|
+
const els = root instanceof Element
|
|
72
|
+
? [root, ...root.querySelectorAll('*')]
|
|
73
|
+
: [...root.querySelectorAll('*')];
|
|
74
|
+
for (const el of els) {
|
|
75
|
+
if (el.tagName && el.tagName.includes('-')) {
|
|
76
|
+
customElements.upgrade(el);
|
|
77
|
+
if (el.shadowRoot) upgradeTree(el.shadowRoot);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Re-execute every `<script>` in `container`, INCLUDING `container` itself when
|
|
84
|
+
* it is one (#1102). A script parsed by `DOMParser` carries the spec's
|
|
85
|
+
* "already started" flag, so the node grafted into the live document is inert
|
|
86
|
+
* and only a fresh clone runs. `querySelectorAll` never matches the element it
|
|
87
|
+
* is called on, so a container-is-a-script was silently skipped: the two swap
|
|
88
|
+
* tiers hand this function each TOP-LEVEL node of the swapped range in turn, so
|
|
89
|
+
* a script emitted as a sibling of the content (a layout's progressive-
|
|
90
|
+
* enhancement script, the shape that surfaced this) never ran after a soft nav.
|
|
91
|
+
*
|
|
92
|
+
* Replacing the container DETACHES it, which is why both callers snapshot the
|
|
93
|
+
* range before iterating rather than walking live `nextSibling` links.
|
|
94
|
+
*
|
|
95
|
+
* `data-webjs-permanent` splits into two cases here, and the split is the whole
|
|
96
|
+
* rule (#1252):
|
|
97
|
+
*
|
|
98
|
+
* - The marked element IS a script: NEVER exempt, however the walk reaches
|
|
99
|
+
* it. This holds whether it arrives as the `container` or as a descendant
|
|
100
|
+
* of one, because the regraft selector has no tag filter and will happily
|
|
101
|
+
* preserve a `<script id data-webjs-permanent>` by identity. The exemption
|
|
102
|
+
* below is therefore STRICT containment, never reflexive. This must not be
|
|
103
|
+
* changed. The regrafts have a both-exist guard, so on the swap that first
|
|
104
|
+
* mounts a route there is no live node to preserve, the inert parsed copy
|
|
105
|
+
* is what lands, and exempting it would leave a script that runs on a cold
|
|
106
|
+
* load and never on a soft navigation. That is precisely the #1102 failure,
|
|
107
|
+
* reintroduced under the banner of fixing it. A script's only state is that
|
|
108
|
+
* it ran, and re-running is the contract for everything in a swapped range.
|
|
109
|
+
* - A script INSIDE a marked element that was ACTUALLY preserved: exempt.
|
|
110
|
+
* The attribute means the subtree survives as the same live node, which is
|
|
111
|
+
* what `diffElementInPlace` already implements by returning early rather
|
|
112
|
+
* than recursing into it. Re-emitting an init script against a widget
|
|
113
|
+
* instance the author deliberately kept alive is a double-initialization,
|
|
114
|
+
* not a refresh.
|
|
115
|
+
*
|
|
116
|
+
* The filter keys on `regraftedPermanents` (actual preservation by identity),
|
|
117
|
+
* never on the attribute alone. A permanent element arriving for the FIRST time
|
|
118
|
+
* was never preserved, so its scripts have never run and must run now; an
|
|
119
|
+
* attribute-only filter would leave them never running on any path.
|
|
120
|
+
*
|
|
121
|
+
* @param {Element} container
|
|
122
|
+
* @returns {Element} `container`, or its replacement when it was a script that
|
|
123
|
+
* was re-emitted. The replacement sits wherever `container` was; when
|
|
124
|
+
* `container` was already detached, `replaceWith` is a spec no-op and the
|
|
125
|
+
* returned clone is detached too, so callers must not assume it is connected.
|
|
126
|
+
*/
|
|
127
|
+
export function reactivateScripts(container) {
|
|
128
|
+
if (container.tagName === 'SCRIPT') {
|
|
129
|
+
const fresh = cloneScriptWithCorrectNonce(/** @type {HTMLScriptElement} */ (container));
|
|
130
|
+
// A no-op when the node has no parent. That happens when an EARLIER
|
|
131
|
+
// script's reactivation ran code that removed this node from the range
|
|
132
|
+
// (reactivation executes synchronously), so a stale snapshot entry cannot
|
|
133
|
+
// resurrect itself into the document.
|
|
134
|
+
container.replaceWith(fresh);
|
|
135
|
+
return fresh;
|
|
136
|
+
}
|
|
137
|
+
// Roots whose subtrees survived this swap by identity. Collected from the
|
|
138
|
+
// container DOWNWARD so the exemption is bounded to the swapped range by
|
|
139
|
+
// construction; `closest()` from a script upward could escape into an outer
|
|
140
|
+
// ancestor that was never part of this swap.
|
|
141
|
+
/** @type {Element[]} */
|
|
142
|
+
const preserved = [];
|
|
143
|
+
if (regraftedPermanents.has(container)) preserved.push(container);
|
|
144
|
+
for (const el of container.querySelectorAll('[data-webjs-permanent]')) {
|
|
145
|
+
if (regraftedPermanents.has(el)) preserved.push(el);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const old of container.querySelectorAll('script')) {
|
|
149
|
+
// STRICT containment: a preserved root exempts its DESCENDANTS, never
|
|
150
|
+
// itself. The regrafts select `[data-webjs-permanent][id]` with no tag
|
|
151
|
+
// filter, so a `<script id data-webjs-permanent>` present on both sides is
|
|
152
|
+
// regrafted like any other element and lands in the WeakSet. Skipping it
|
|
153
|
+
// here would exempt the marked script itself whenever the walk reaches it
|
|
154
|
+
// as a descendant (the full-body path), while the container branch above
|
|
155
|
+
// still re-emits it, so one script would get opposite answers depending on
|
|
156
|
+
// which entry point reached it. `contains()` is reflexive, hence `p !== old`.
|
|
157
|
+
if (preserved.length && preserved.some((p) => p !== old && p.contains(old))) continue;
|
|
158
|
+
old.replaceWith(cloneScriptWithCorrectNonce(/** @type {HTMLScriptElement} */ (old)));
|
|
159
|
+
}
|
|
160
|
+
return container;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Reactivate scripts + upgrade custom elements across a just-swapped boundary
|
|
165
|
+
* range. The range is SNAPSHOT first: `reactivateScripts` replaces a top-level
|
|
166
|
+
* script node, which detaches it and cuts a live `nextSibling` walk, silently
|
|
167
|
+
* skipping every node after it (#1102).
|
|
168
|
+
*
|
|
169
|
+
* The snapshot is taken AFTER the tier has finished writing the range (the
|
|
170
|
+
* replace tier's insert loop, the morph tier's `reconcileSiblings`), so every
|
|
171
|
+
* entry is attached when it is recorded. It can still go stale DURING the walk,
|
|
172
|
+
* because reactivating a script executes it synchronously and that code may
|
|
173
|
+
* mutate the range. Two consequences, both deliberate. A node an earlier script
|
|
174
|
+
* REMOVED is skipped, since `replaceWith` on a parentless node is a spec no-op.
|
|
175
|
+
* A node an earlier script INSERTED is not visited, unlike the live walk this
|
|
176
|
+
* replaced. That costs nothing reachable: such a node is connected by
|
|
177
|
+
* definition, so the browser upgrades it on insertion, and `ensureUpgradeObserver`
|
|
178
|
+
* catches it on the microtask regardless. Do NOT "restore" the live walk to
|
|
179
|
+
* recover it. A correct live walk has to advance off the REPLACEMENT (advancing
|
|
180
|
+
* off the detached original is bug #1102 itself), and in that form a script
|
|
181
|
+
* appending a sibling script loops forever, each clone appending the next; it
|
|
182
|
+
* would also re-run a script that already executed when its own creator
|
|
183
|
+
* inserted it. The snapshot is additionally safer under a MOVE, since a live
|
|
184
|
+
* walk would follow a node out of the range and start re-executing unrelated
|
|
185
|
+
* scripts in the rest of the body.
|
|
186
|
+
*
|
|
187
|
+
* @param {{ start: Comment, end: Comment }} range
|
|
188
|
+
*/
|
|
189
|
+
export function activateSwappedRange(range) {
|
|
190
|
+
/** @type {Element[]} */
|
|
191
|
+
const swapped = [];
|
|
192
|
+
for (let n = range.start.nextSibling; n && n !== range.end; n = n.nextSibling) {
|
|
193
|
+
if (n.nodeType === 1) swapped.push(/** @type {Element} */ (n));
|
|
194
|
+
}
|
|
195
|
+
for (const el of swapped) {
|
|
196
|
+
const live = reactivateScripts(el);
|
|
197
|
+
// Nothing to upgrade in a detached tree. `customElements.upgrade` off
|
|
198
|
+
// document runs the CONSTRUCTOR (not `connectedCallback`, which waits for
|
|
199
|
+
// insertion), so this skips constructing elements for a tree that was just
|
|
200
|
+
// removed and will never be seen.
|
|
201
|
+
if (live.isConnected !== false) upgradeCustomElements(live);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ====================================================================
|
|
206
|
+
* Internal exports for unit testing
|
|
207
|
+
* ==================================================================== */
|
|
208
|
+
|
|
209
|
+
export {
|
|
210
|
+
addNewHeadElements as _addNewHeadElements,
|
|
211
|
+
mergeHead as _mergeHead,
|
|
212
|
+
reactivateScripts as _reactivateScripts,
|
|
213
|
+
isPreBootNavigation as _isPreBootNavigation,
|
|
214
|
+
FALLBACK_MARKER_KEY as _FALLBACK_MARKER_KEY,
|
|
215
|
+
activateSwappedRange as _activateSwappedRange,
|
|
216
|
+
activeFrameId as _activeFrameId,
|
|
217
|
+
resolveTargetFrameId as _resolveTargetFrameId,
|
|
218
|
+
FRAME_TOP as _FRAME_TOP,
|
|
219
|
+
markFrameBusy as _markFrameBusy,
|
|
220
|
+
clearFrameBusy as _clearFrameBusy,
|
|
221
|
+
markFormBusy as _markFormBusy,
|
|
222
|
+
clearFormBusy as _clearFormBusy,
|
|
223
|
+
collectBoundaries as _collectBoundaries,
|
|
224
|
+
planBoundarySwap as _planBoundarySwap,
|
|
225
|
+
parseHTML as _parseHTML,
|
|
226
|
+
resetParseProbe as _resetParseProbe,
|
|
227
|
+
keyOf as _keyOf,
|
|
228
|
+
diffElementInPlace as _diffElementInPlace,
|
|
229
|
+
reconcileChildren as _reconcileChildren,
|
|
230
|
+
onPopState as _onPopState,
|
|
231
|
+
applySwap as _applySwap,
|
|
232
|
+
buildHaveHeader as _buildHaveHeader,
|
|
233
|
+
snapshotCache as _snapshotCache,
|
|
234
|
+
prefetchCache as _prefetchCache,
|
|
235
|
+
LIVE_ATTRS as _LIVE_ATTRS,
|
|
236
|
+
blurOutgoingFocus as _blurOutgoingFocus,
|
|
237
|
+
onSubmit as _onSubmit,
|
|
238
|
+
getSubmitMethod as _getSubmitMethod,
|
|
239
|
+
getSubmitAction as _getSubmitAction,
|
|
240
|
+
buildSubmitFormData as _buildSubmitFormData,
|
|
241
|
+
getSubmitEnctype as _getSubmitEnctype,
|
|
242
|
+
encodeSubmitBody as _encodeSubmitBody,
|
|
243
|
+
restoreOptimistic as _restoreOptimistic,
|
|
244
|
+
eligibleAnchorHref as _eligibleAnchorHref,
|
|
245
|
+
viewTransitionsEnabled as _viewTransitionsEnabled,
|
|
246
|
+
runWithTransition as _runWithTransition,
|
|
247
|
+
regraftPermanentElements as _regraftPermanentElements,
|
|
248
|
+
regraftPermanentInSlice as _regraftPermanentInSlice,
|
|
249
|
+
prefetchSuppressed as _prefetchSuppressed,
|
|
250
|
+
prefetchMode as _prefetchMode,
|
|
251
|
+
prefetchHasHoverPointer as _prefetchHasHoverPointer,
|
|
252
|
+
prefetch as _prefetch,
|
|
253
|
+
prefetchTake as _prefetchTake,
|
|
254
|
+
prefetchAnchor as _prefetchAnchor,
|
|
255
|
+
applyOptimisticLoading as _applyOptimisticLoading,
|
|
256
|
+
prefetchSaysSaveData as _prefetchSaysSaveData,
|
|
257
|
+
readStreamedShell as _readStreamedShell,
|
|
258
|
+
takeResolveUnit as _takeResolveUnit,
|
|
259
|
+
applyStreamedResolve as _applyStreamedResolve,
|
|
260
|
+
streamBoundariesProgressively as _streamBoundariesProgressively,
|
|
261
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client router: view-transition.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of the pre-split `router-client.js`; that barrel holds
|
|
5
|
+
* the router's full contract and the public entry points.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether the current page opts into the native View Transitions API for
|
|
12
|
+
* client-router swaps. OFF by default (no animation surprise, no
|
|
13
|
+
* regression for browsers without the API): a transition is purely
|
|
14
|
+
* opt-in via a `<meta name="view-transition" content="same-origin">` in
|
|
15
|
+
* the document head, mirroring Turbo's `<meta name="view-transition">`
|
|
16
|
+
* convention. The accepted opt-in value is `same-origin` (every
|
|
17
|
+
* client-router swap is same-origin by construction, so it reads as "yes,
|
|
18
|
+
* animate these in-app navigations"). Any other value, or the meta being
|
|
19
|
+
* absent, keeps transitions off.
|
|
20
|
+
*
|
|
21
|
+
* Re-read per navigation rather than cached: the meta can be added or
|
|
22
|
+
* removed by a swap (the head merge brings in the new page's head), so a
|
|
23
|
+
* page can turn transitions on or off as the user navigates.
|
|
24
|
+
*
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function viewTransitionsEnabled() {
|
|
28
|
+
if (typeof document === 'undefined') return false;
|
|
29
|
+
const meta = document.querySelector('meta[name="view-transition"]');
|
|
30
|
+
if (!meta) return false;
|
|
31
|
+
const content = (meta.getAttribute('content') || '').trim().toLowerCase();
|
|
32
|
+
return content === 'same-origin';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run a synchronous DOM-mutation thunk, wrapping it in
|
|
37
|
+
* `document.startViewTransition()` when the page has opted in AND the
|
|
38
|
+
* browser supports the API. Otherwise the thunk runs synchronously,
|
|
39
|
+
* byte-identical to the pre-View-Transitions behaviour (no flash, no
|
|
40
|
+
* regression). The thunk is the SAME swap code in both branches; the
|
|
41
|
+
* transition only captures the before/after around the mutation (the
|
|
42
|
+
* fetch already happened, so it is never inside the callback).
|
|
43
|
+
*
|
|
44
|
+
* @param {() => void} thunk The synchronous DOM swap to perform.
|
|
45
|
+
* @param {() => void} [afterFinished] Optional post-transition work
|
|
46
|
+
* (e.g. re-upgrade custom elements) run when the transition settles; for
|
|
47
|
+
* the synchronous fallback it runs immediately after the thunk.
|
|
48
|
+
*/
|
|
49
|
+
export function runWithTransition(thunk, afterFinished) {
|
|
50
|
+
const start = typeof document !== 'undefined'
|
|
51
|
+
? /** @type any */ (document).startViewTransition
|
|
52
|
+
: undefined;
|
|
53
|
+
if (viewTransitionsEnabled() && typeof start === 'function') {
|
|
54
|
+
const t = start.call(document, thunk);
|
|
55
|
+
if (t && t.finished && typeof t.finished.then === 'function') {
|
|
56
|
+
t.finished.then(() => { if (afterFinished) afterFinished(); }).catch(() => {});
|
|
57
|
+
} else if (afterFinished) {
|
|
58
|
+
afterFinished();
|
|
59
|
+
}
|
|
60
|
+
// Resolve when the DOM MUTATION (the thunk) has actually committed, NOT when
|
|
61
|
+
// the animation finishes. Under `startViewTransition` the thunk is deferred a
|
|
62
|
+
// frame, so anything that reads the swapped-in DOM (a progressively-streamed
|
|
63
|
+
// Suspense resolve, #1048) must await this, or it runs against the pre-swap
|
|
64
|
+
// DOM and drops. `updateCallbackDone` is that signal; fall back to a resolved
|
|
65
|
+
// promise if the browser does not expose it.
|
|
66
|
+
return (t && t.updateCallbackDone && typeof t.updateCallbackDone.then === 'function')
|
|
67
|
+
? t.updateCallbackDone.catch(() => {})
|
|
68
|
+
: Promise.resolve();
|
|
69
|
+
}
|
|
70
|
+
thunk();
|
|
71
|
+
if (afterFinished) afterFinished();
|
|
72
|
+
return Promise.resolve();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Live nodes a regraft actually moved into the incoming tree, so they
|
|
77
|
+
* survived the swap BY IDENTITY. Membership is strictly narrower than
|
|
78
|
+
* "carries `data-webjs-permanent`": the regrafts have a both-exist guard, so
|
|
79
|
+
* a permanent element arriving for the first time is a freshly imported node
|
|
80
|
+
* that was never preserved and is not in here. `reactivateScripts` reads this
|
|
81
|
+
* to decide whether a script inside a permanent element is a script the
|
|
82
|
+
* author kept alive (skip it) or one that has never run (run it).
|
|
83
|
+
*
|
|
84
|
+
* Weak and keyed by node identity, so a destroyed node drops out on its own
|
|
85
|
+
* and a later element reusing the same `#id` is a different object that
|
|
86
|
+
* correctly re-runs. Never cleared per navigation: a node preserved across
|
|
87
|
+
* several navigations must keep its exemption on every one of them.
|
|
88
|
+
*
|
|
89
|
+
* @type {WeakSet<Element>}
|
|
90
|
+
*/
|
|
91
|
+
export const regraftedPermanents = new WeakSet();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Persist `data-webjs-permanent` elements across a swap by NODE IDENTITY.
|
|
95
|
+
*
|
|
96
|
+
* Mirrors Turbo's permanent-element behaviour: an element the author
|
|
97
|
+
* marks `data-webjs-permanent` (and which carries an `id`) survives a
|
|
98
|
+
* destructive swap as the SAME live DOM node, so a playing
|
|
99
|
+
* `<audio>` / `<video>`, a live widget, an open menu, or any element with
|
|
100
|
+
* accumulated JS state keeps running across the navigation instead of
|
|
101
|
+
* being destroyed and re-created from the incoming HTML.
|
|
102
|
+
*
|
|
103
|
+
* The mechanism runs BEFORE the destructive `replaceChildren` / range
|
|
104
|
+
* delete: for each `[data-webjs-permanent][id]` in the CURRENT subtree, if
|
|
105
|
+
* the INCOMING tree has a matching `#id`, the live current node is MOVED
|
|
106
|
+
* into the incoming tree's position (replacing the incoming placeholder).
|
|
107
|
+
* The subsequent swap then ADOPTS the live node (it is already part of the
|
|
108
|
+
* incoming tree) rather than destroying the current one. The keyed
|
|
109
|
+
* reconciler matches it by id afterwards and leaves it in place.
|
|
110
|
+
*
|
|
111
|
+
* Guards (correctness):
|
|
112
|
+
* - both-exist: only regraft an id present in BOTH the current and
|
|
113
|
+
* incoming subtree. An id in the current but NOT the incoming is being
|
|
114
|
+
* removed; leave it (do not force it to persist).
|
|
115
|
+
* - current-is-permanent: only move when the CURRENT node actually
|
|
116
|
+
* carries `data-webjs-permanent` (an incoming `#id` that resolves to a
|
|
117
|
+
* non-permanent current element is left untouched).
|
|
118
|
+
* - boundary-respecting: the live node is placed exactly where the
|
|
119
|
+
* incoming document puts it, so it never escapes a frame/region.
|
|
120
|
+
*
|
|
121
|
+
* @param {ParentNode} currentRoot The live subtree being swapped out.
|
|
122
|
+
* @param {ParentNode} incomingRoot The incoming subtree being swapped in.
|
|
123
|
+
*/
|
|
124
|
+
export function regraftPermanentElements(currentRoot, incomingRoot) {
|
|
125
|
+
if (!currentRoot || !incomingRoot) return;
|
|
126
|
+
if (typeof currentRoot.querySelectorAll !== 'function') return;
|
|
127
|
+
const permanents = currentRoot.querySelectorAll('[data-webjs-permanent][id]');
|
|
128
|
+
for (const live of permanents) {
|
|
129
|
+
const id = live.id;
|
|
130
|
+
if (!id) continue;
|
|
131
|
+
// both-exist guard: the incoming subtree must carry a matching #id.
|
|
132
|
+
let placeholder = null;
|
|
133
|
+
try {
|
|
134
|
+
placeholder = incomingRoot.querySelector(`#${CSS.escape(id)}`);
|
|
135
|
+
} catch { placeholder = null; }
|
|
136
|
+
if (!placeholder) continue;
|
|
137
|
+
// current-is-permanent guard is implicit in the selector above, but
|
|
138
|
+
// re-assert defensively (the live node is the one we move).
|
|
139
|
+
if (!live.hasAttribute || !live.hasAttribute('data-webjs-permanent')) continue;
|
|
140
|
+
const parent = placeholder.parentNode;
|
|
141
|
+
if (!parent) continue;
|
|
142
|
+
// Move the LIVE node into the incoming tree's position, replacing the
|
|
143
|
+
// incoming placeholder. The swap then adopts the live node.
|
|
144
|
+
if (placeholder === live) continue;
|
|
145
|
+
parent.replaceChild(live, placeholder);
|
|
146
|
+
regraftedPermanents.add(live);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Permanent-element regraft for the marker-range path, where the two
|
|
152
|
+
* sides are ARRAYS of sibling nodes (the live slice between markers, and
|
|
153
|
+
* the imported-but-detached incoming slice) rather than single roots.
|
|
154
|
+
*
|
|
155
|
+
* For each `[data-webjs-permanent][id]` reachable from the LIVE slice, if
|
|
156
|
+
* a matching `#id` exists anywhere in the INCOMING slice, replace the
|
|
157
|
+
* incoming (freshly-imported) copy with the LIVE node so the reconciler
|
|
158
|
+
* adopts the live node by identity. Searches both top-level slice members
|
|
159
|
+
* and their descendants. The same both-exist + current-is-permanent
|
|
160
|
+
* guards as `regraftPermanentElements` apply.
|
|
161
|
+
*
|
|
162
|
+
* @param {Node[]} liveSlice
|
|
163
|
+
* @param {Node[]} incomingSlice
|
|
164
|
+
*/
|
|
165
|
+
export function regraftPermanentInSlice(liveSlice, incomingSlice) {
|
|
166
|
+
/** @type {Element[]} */
|
|
167
|
+
const livePermanents = [];
|
|
168
|
+
for (const n of liveSlice) {
|
|
169
|
+
if (n.nodeType !== 1) continue;
|
|
170
|
+
const el = /** @type {Element} */ (n);
|
|
171
|
+
if (el.hasAttribute && el.hasAttribute('data-webjs-permanent') && el.id) {
|
|
172
|
+
livePermanents.push(el);
|
|
173
|
+
}
|
|
174
|
+
if (typeof el.querySelectorAll === 'function') {
|
|
175
|
+
for (const d of el.querySelectorAll('[data-webjs-permanent][id]')) livePermanents.push(d);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!livePermanents.length) return;
|
|
179
|
+
|
|
180
|
+
for (const live of livePermanents) {
|
|
181
|
+
const id = live.id;
|
|
182
|
+
if (!id) continue;
|
|
183
|
+
const placeholder = findInSlice(incomingSlice, id);
|
|
184
|
+
if (!placeholder) continue; // both-exist guard
|
|
185
|
+
if (placeholder === live) continue;
|
|
186
|
+
const parent = placeholder.parentNode;
|
|
187
|
+
if (parent) {
|
|
188
|
+
parent.replaceChild(live, placeholder);
|
|
189
|
+
regraftedPermanents.add(live);
|
|
190
|
+
} else {
|
|
191
|
+
// Placeholder is a top-level slice member with no parent (detached):
|
|
192
|
+
// replace it in the incomingSlice array so the reconciler inserts the
|
|
193
|
+
// live node in that position.
|
|
194
|
+
const idx = incomingSlice.indexOf(placeholder);
|
|
195
|
+
if (idx !== -1) {
|
|
196
|
+
incomingSlice[idx] = live;
|
|
197
|
+
regraftedPermanents.add(live);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Find an element with `#id` within an array of (possibly detached)
|
|
205
|
+
* sibling nodes, searching each member and its descendants.
|
|
206
|
+
*
|
|
207
|
+
* @param {Node[]} slice
|
|
208
|
+
* @param {string} id
|
|
209
|
+
* @returns {Element | null}
|
|
210
|
+
*/
|
|
211
|
+
function findInSlice(slice, id) {
|
|
212
|
+
for (const n of slice) {
|
|
213
|
+
if (n.nodeType !== 1) continue;
|
|
214
|
+
const el = /** @type {Element} */ (n);
|
|
215
|
+
if (el.id === id) return el;
|
|
216
|
+
if (typeof el.querySelector === 'function') {
|
|
217
|
+
let match = null;
|
|
218
|
+
try { match = el.querySelector(`#${CSS.escape(id)}`); } catch { match = null; }
|
|
219
|
+
if (match) return match;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
package/src/router-client.d.ts
CHANGED
|
@@ -6,5 +6,14 @@ export function navigate(url: Route, opts?: { replace?: boolean }): Promise<void
|
|
|
6
6
|
export function loadFrame(
|
|
7
7
|
frameEl: Element,
|
|
8
8
|
url: string,
|
|
9
|
-
): Promise<{ ok: boolean; status: number | null; aborted: boolean }>;
|
|
9
|
+
): Promise<{ ok: boolean; status: number | null; aborted: boolean; applied: boolean }>;
|
|
10
10
|
export function revalidate(url?: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Re-render the current url on the server and apply it in place, with no page
|
|
13
|
+
* reload. Records no history entry and never scrolls. `'page'` (the default)
|
|
14
|
+
* morphs the deepest shared boundary, preserving hydrated component state
|
|
15
|
+
* outside it; `'shell'` replaces the whole body, which a layout change needs.
|
|
16
|
+
* Resolves `false` when the refresh did not apply, so the caller can fall back
|
|
17
|
+
* to a full load.
|
|
18
|
+
*/
|
|
19
|
+
export function refreshPage(mode?: 'page' | 'shell'): Promise<boolean>;
|