@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,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interception of the native insertion API on a slotted host, so an
|
|
3
|
+
* authored write lands in the authored list rather than the rendered tree.
|
|
4
|
+
*
|
|
5
|
+
* Moved verbatim out of the pre-split `slot.js`; see that barrel for the
|
|
6
|
+
* runtime's full contract.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { isOwnSlot } from './assignment.js';
|
|
11
|
+
import { inBrowser } from './polyfills.js';
|
|
12
|
+
import { applySlotAssignments } from './project.js';
|
|
13
|
+
import { INNER_HTML_DESC, N_append, N_appendChild, N_insertBefore, N_prepend, N_removeChild, N_replaceChild, N_replaceChildren, TEXT_CONTENT_DESC, captureNatives, withRendererWrites } from './sensors.js';
|
|
14
|
+
import { ensureSlotState, repartition } from './state.js';
|
|
15
|
+
import { FRAMEWORK_DETACHED, INTERCEPTED, LIGHT_SLOT_ATTR, PARK, RENDERING, SLOT_STATE } from './symbols.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True when `node` is renderer-owned: it is one of the instance's bookend
|
|
19
|
+
* markers (`wjm-s` / `wjm-e`) or sits between them. Object-identity check
|
|
20
|
+
* against the marker refs the instance holds, never comment-text sniffing.
|
|
21
|
+
*/
|
|
22
|
+
export function instanceOwns(inst, node) {
|
|
23
|
+
if (!inst || !inst.startNode || !inst.endNode) return false;
|
|
24
|
+
if (node === inst.startNode || node === inst.endNode) return true;
|
|
25
|
+
for (let n = inst.startNode.nextSibling; n && n !== inst.endNode; n = n.nextSibling) {
|
|
26
|
+
if (n === node) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The host's hidden park element, created + attached lazily. */
|
|
32
|
+
export function parkFor(host) {
|
|
33
|
+
const h = /** @type {any} */ (host);
|
|
34
|
+
let park = h[PARK];
|
|
35
|
+
if (!park || !park.isConnected) {
|
|
36
|
+
if (!park) {
|
|
37
|
+
park = host.ownerDocument.createElement('wj-slot-park');
|
|
38
|
+
park.setAttribute('hidden', '');
|
|
39
|
+
park.style.display = 'none';
|
|
40
|
+
h[PARK] = park;
|
|
41
|
+
}
|
|
42
|
+
// Attach inside the host, after the rendered output, via the native path
|
|
43
|
+
// (never through the patched appendChild).
|
|
44
|
+
withRendererWrites(host, () => N_appendChild.call(host, park));
|
|
45
|
+
}
|
|
46
|
+
return park;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True for a REAL platform Node from any realm: a same-realm Node passes
|
|
51
|
+
* instanceof; a cross-realm Node passes via its own realm's constructor.
|
|
52
|
+
* KNOWN EDGE: a node from a DISCARDED iframe realm (defaultView null) and
|
|
53
|
+
* any cross-realm DOCUMENT (ownerDocument is null on a Document by spec)
|
|
54
|
+
* fail both arms where native would adopt or HierarchyRequestError:
|
|
55
|
+
* appendChild-shaped calls throw TypeError, and the variadic
|
|
56
|
+
* string-accepting calls stringify to text. Both outcomes are strictly
|
|
57
|
+
* safer than admitting an unverifiable object into the record.
|
|
58
|
+
*
|
|
59
|
+
* @param {any} n
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
function isRealmNode(n) {
|
|
63
|
+
return (
|
|
64
|
+
n instanceof Node ||
|
|
65
|
+
Boolean(
|
|
66
|
+
n &&
|
|
67
|
+
typeof n === 'object' &&
|
|
68
|
+
/** @type {any} */ (n).ownerDocument &&
|
|
69
|
+
/** @type {any} */ (n).ownerDocument.defaultView &&
|
|
70
|
+
n instanceof /** @type {any} */ (n).ownerDocument.defaultView.Node,
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Expand one argument of a DOM insertion call into a flat node list. A
|
|
77
|
+
* DocumentFragment is DRAINED (native contract: the fragment ends empty) and
|
|
78
|
+
* its children returned; a string becomes a Text node when `allowString`
|
|
79
|
+
* (append / prepend / replaceChildren accept strings, appendChild does not).
|
|
80
|
+
*
|
|
81
|
+
* @param {Element} host
|
|
82
|
+
* @param {any} arg
|
|
83
|
+
* @param {boolean} allowString
|
|
84
|
+
* @returns {Node[]}
|
|
85
|
+
*/
|
|
86
|
+
function expandArg(host, arg, allowString) {
|
|
87
|
+
// WebIDL (Node or DOMString) coercion: append/prepend/replaceChildren
|
|
88
|
+
// stringify ANY argument that is not a real platform Node (a number, null,
|
|
89
|
+
// an object, even a duck-typed fake with a numeric nodeType); native
|
|
90
|
+
// host.append(42) appends the text "42" and host.append({}) appends
|
|
91
|
+
// "[object Object]".
|
|
92
|
+
if (allowString && !isRealmNode(arg)) {
|
|
93
|
+
// A template literal performs exactly ES ToString (ToPrimitive with hint
|
|
94
|
+
// "string": toString before valueOf, and a Symbol THROWS TypeError),
|
|
95
|
+
// matching WebIDL DOMString conversion; '' + x would use hint "default"
|
|
96
|
+
// (valueOf first) and diverge for objects overriding both.
|
|
97
|
+
return [host.ownerDocument.createTextNode(`${/** @type {any} */ (arg)}`)];
|
|
98
|
+
}
|
|
99
|
+
// Non-string path (appendChild / insertBefore / replaceChild): reject any
|
|
100
|
+
// non-platform-node BEFORE the fragment branch, or a duck-typed
|
|
101
|
+
// {nodeType: 11} fake would bypass guardInsertable entirely.
|
|
102
|
+
if (!isRealmNode(arg)) {
|
|
103
|
+
throw new TypeError('Failed to execute insertion on the host: parameter is not of type Node.');
|
|
104
|
+
}
|
|
105
|
+
if (arg && arg.nodeType === 11) {
|
|
106
|
+
const kids = Array.from(arg.childNodes);
|
|
107
|
+
// Guard BEFORE draining: a cycle error must leave the fragment intact
|
|
108
|
+
// (native throws with the fragment untouched).
|
|
109
|
+
guardInsertable(host, kids);
|
|
110
|
+
for (const k of kids) N_removeChild.call(arg, k);
|
|
111
|
+
return kids;
|
|
112
|
+
}
|
|
113
|
+
guardInsertable(host, [/** @type {Node} */ (arg)]);
|
|
114
|
+
return [/** @type {Node} */ (arg)];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Throw `HierarchyRequestError` (native parity) if any node would create a
|
|
119
|
+
* cycle: the host itself, or an ancestor of the host. Checked BEFORE the record
|
|
120
|
+
* is mutated, so a bad insert leaves `authored` untouched, like native.
|
|
121
|
+
*
|
|
122
|
+
* @param {Element} host
|
|
123
|
+
* @param {Node[]} nodes
|
|
124
|
+
*/
|
|
125
|
+
function guardInsertable(host, nodes) {
|
|
126
|
+
guardCycle(host, nodes);
|
|
127
|
+
for (const n of nodes) {
|
|
128
|
+
// Node-TYPE validity is DOM pre-insert step 4, AFTER the ref's
|
|
129
|
+
// NotFoundError (step 3): callers run this via expandArg once the ref
|
|
130
|
+
// checks passed. A non-insertable type (an Attr, a Document, a doctype)
|
|
131
|
+
// is a HierarchyRequestError. Realm validity (TypeError for a
|
|
132
|
+
// duck-typed fake, which would otherwise mutate the record and wedge
|
|
133
|
+
// every later apply) is checked by callers BEFORE any DOM step, per
|
|
134
|
+
// WebIDL argument conversion.
|
|
135
|
+
if (!isRealmNode(n)) {
|
|
136
|
+
throw new TypeError('Failed to execute insertion on the host: parameter is not of type Node.');
|
|
137
|
+
}
|
|
138
|
+
const t = n.nodeType;
|
|
139
|
+
if (t !== 1 && t !== 3 && t !== 4 && t !== 7 && t !== 8 && t !== 11) {
|
|
140
|
+
throw new DOMException(
|
|
141
|
+
'Failed to execute insertion on the host: the node type may not be inserted here.',
|
|
142
|
+
'HierarchyRequestError',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* DOM pre-insert step 2, the cycle check: run after ALL WebIDL argument
|
|
150
|
+
* conversions and before the ref's NotFoundError (step 3).
|
|
151
|
+
*
|
|
152
|
+
* @param {Element} host
|
|
153
|
+
* @param {Node[]} nodes
|
|
154
|
+
*/
|
|
155
|
+
function guardCycle(host, nodes) {
|
|
156
|
+
for (const n of nodes) {
|
|
157
|
+
if (n === host || (n.nodeType === 1 && /** @type {Element} */ (n).contains(host))) {
|
|
158
|
+
throw new DOMException(
|
|
159
|
+
'Failed to execute insertion on the host: the new child contains the parent.',
|
|
160
|
+
'HierarchyRequestError',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Splice `nodes` into `authored` before `ref` (a node already in authored) or
|
|
168
|
+
* at the end when `ref` is null. Nodes already present are removed from their
|
|
169
|
+
* current position first (native move semantics for a re-inserted child).
|
|
170
|
+
*
|
|
171
|
+
* @param {SlotState} state
|
|
172
|
+
* @param {Node[]} nodes
|
|
173
|
+
* @param {Node | null} ref
|
|
174
|
+
*/
|
|
175
|
+
function authoredSplice(state, nodes, ref) {
|
|
176
|
+
const a = state.authored;
|
|
177
|
+
for (const n of nodes) {
|
|
178
|
+
const i = a.indexOf(n);
|
|
179
|
+
if (i !== -1) a.splice(i, 1);
|
|
180
|
+
}
|
|
181
|
+
let at = ref == null ? a.length : a.indexOf(ref);
|
|
182
|
+
if (at === -1) at = a.length;
|
|
183
|
+
a.splice(at, 0, ...nodes);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* True when `el` is, or sits inside, an AUTHORED node of this host: such an
|
|
188
|
+
* element is CONTENT (a chunk the author wrote or moved in), never one of
|
|
189
|
+
* the host's own rendered slots, no matter what attributes it carries.
|
|
190
|
+
*
|
|
191
|
+
* Exported for the router's own-slot collection, which must apply the SAME
|
|
192
|
+
* invariant when picking reprojection targets.
|
|
193
|
+
*
|
|
194
|
+
* @param {Element} host
|
|
195
|
+
* @param {Element} el
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
export function isAuthoredContentSlot(host, el) {
|
|
199
|
+
const state = /** @type {SlotState | undefined} */ (
|
|
200
|
+
/** @type {any} */ (host)[SLOT_STATE]
|
|
201
|
+
);
|
|
202
|
+
if (!state) return false;
|
|
203
|
+
return isInsideAuthored(state, host, el);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @param {SlotState} state @param {Element} host @param {Element} el */
|
|
207
|
+
export function isInsideAuthored(state, host, el) {
|
|
208
|
+
for (let p = /** @type {Node | null} */ (el); p && p !== host; p = p.parentNode) {
|
|
209
|
+
if (state.authored.indexOf(p) !== -1) return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* True when an authored node is still VIRTUALLY a child of the host: it
|
|
216
|
+
* physically sits in one of the host's own slots, the park, or the host
|
|
217
|
+
* itself, or the framework holds it detached as a record value (a rescued
|
|
218
|
+
* closed-slot child). A node the author moved elsewhere out-of-band (into a
|
|
219
|
+
* fragment, another element) is NOT a child anymore even while the record
|
|
220
|
+
* still lists it, and native removeChild / replaceChild / insertBefore-ref
|
|
221
|
+
* answer that with NotFoundError.
|
|
222
|
+
*
|
|
223
|
+
* @param {Element} host
|
|
224
|
+
* @param {Node} node
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
export function isVirtualChild(host, node) {
|
|
228
|
+
if (FRAMEWORK_DETACHED.has(node)) return true;
|
|
229
|
+
const p = node.parentNode;
|
|
230
|
+
if (p == null) return false;
|
|
231
|
+
if (p === host) return true;
|
|
232
|
+
if (p === /** @type {any} */ (host)[PARK]) return true;
|
|
233
|
+
const state = /** @type {SlotState | undefined} */ (
|
|
234
|
+
/** @type {any} */ (host)[SLOT_STATE]
|
|
235
|
+
);
|
|
236
|
+
// lastSnapshot membership proves ownership on its own (a detached forwarded
|
|
237
|
+
// slot's structural isOwnSlot would wrongly veto it); a contained slot still
|
|
238
|
+
// needs isOwnSlot so a foreign torn-down slot is not claimed.
|
|
239
|
+
return (
|
|
240
|
+
p.nodeType === 1 &&
|
|
241
|
+
/** @type {Element} */ (p).tagName === 'SLOT' &&
|
|
242
|
+
/** @type {Element} */ (p).hasAttribute(LIGHT_SLOT_ATTR) &&
|
|
243
|
+
((state && state.lastSnapshot.has(/** @type {HTMLSlotElement} */ (p))) ||
|
|
244
|
+
(host.contains(p) && isOwnSlot(host, /** @type {Element} */ (p))))
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The empty set handed to resync when no record op is pending. */
|
|
249
|
+
export const EMPTY_NODE_SET = new Set();
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* WebIDL for the variadic insertion methods converts EVERY argument before
|
|
253
|
+
* any node is moved: phase 1 validates each argument (a Symbol or fake
|
|
254
|
+
* throws with every fragment still intact), phase 2 drains fragments and
|
|
255
|
+
* builds the flat node list.
|
|
256
|
+
*
|
|
257
|
+
* @param {Element} host
|
|
258
|
+
* @param {any[]} args
|
|
259
|
+
* @returns {Node[]}
|
|
260
|
+
*/
|
|
261
|
+
function convertVariadicArgs(host, args) {
|
|
262
|
+
// Phase 1 is PURE WebIDL conversion, left to right, with NO DOM validity
|
|
263
|
+
// interleaved: native converts every argument (running ToString side
|
|
264
|
+
// effects, throwing on a Symbol) before any operation-body step, so a
|
|
265
|
+
// later argument's conversion TypeError must preempt an earlier
|
|
266
|
+
// argument's HierarchyRequestError.
|
|
267
|
+
/** @type {Array<{ text?: string, frag?: DocumentFragment, node?: Node }>} */
|
|
268
|
+
const converted = [];
|
|
269
|
+
for (const arg of args) {
|
|
270
|
+
if (!isRealmNode(arg)) {
|
|
271
|
+
converted.push({ text: `${arg}` }); // Symbol throws here, nothing drained
|
|
272
|
+
} else if (arg.nodeType === 11) {
|
|
273
|
+
converted.push({ frag: /** @type {DocumentFragment} */ (arg) });
|
|
274
|
+
} else {
|
|
275
|
+
converted.push({ node: arg });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Phase 2: DOM validity for every converted argument, THEN the build.
|
|
279
|
+
for (const c of converted) {
|
|
280
|
+
if (c.frag) guardCycle(host, Array.from(c.frag.childNodes));
|
|
281
|
+
else if (c.node) guardInsertable(host, [c.node]);
|
|
282
|
+
}
|
|
283
|
+
// The build mirrors native's "converting nodes into a node": append each
|
|
284
|
+
// converted item to a SCRATCH fragment in argument order. Appending a
|
|
285
|
+
// plain-node argument detaches it from wherever it currently sits,
|
|
286
|
+
// INCLUDING a later fragment argument, so append(a, fragContaining(a))
|
|
287
|
+
// nets [a, ...fragRest] exactly like native; a repeated Node argument is
|
|
288
|
+
// physically moved, netting keep-LAST order; and the record can never
|
|
289
|
+
// receive a duplicate entry (scratch children are unique by construction).
|
|
290
|
+
// ACCEPTED DIVERGENCE, documented here as its canonical spot: detaching a
|
|
291
|
+
// connected argument fires its [CEReactions] callbacks MID-call, where
|
|
292
|
+
// native defers them to the end of the outer variadic operation (a
|
|
293
|
+
// userland polyfill has no reactions queue). The net DOM converges
|
|
294
|
+
// through the latch + record ops; the one observable divergence is a
|
|
295
|
+
// same-host-mutating disconnect callback racing replaceChildren's
|
|
296
|
+
// wholesale displacement. (Connected variadic arguments also fire their
|
|
297
|
+
// disconnect reactions on detach, matching native's own per-node moves.)
|
|
298
|
+
const scratch = host.ownerDocument.createDocumentFragment();
|
|
299
|
+
for (const c of converted) {
|
|
300
|
+
if (c.text !== undefined) {
|
|
301
|
+
N_appendChild.call(scratch, host.ownerDocument.createTextNode(c.text));
|
|
302
|
+
} else if (c.frag) {
|
|
303
|
+
const kids = Array.from(c.frag.childNodes);
|
|
304
|
+
guardInsertable(host, kids);
|
|
305
|
+
for (const k of kids) N_appendChild.call(scratch, k);
|
|
306
|
+
} else if (c.node) {
|
|
307
|
+
// DEFENSE-IN-DEPTH early throw (the LOAD-BEARING check is the final
|
|
308
|
+
// assembled guard below): a reaction fired by an earlier argument's
|
|
309
|
+
// detach can reparent the HOST into this one; catching it here just
|
|
310
|
+
// surfaces the error before this arg enters the scratch.
|
|
311
|
+
guardCycle(host, [c.node]);
|
|
312
|
+
N_appendChild.call(scratch, c.node);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const nodes = Array.from(scratch.childNodes);
|
|
316
|
+
// The LOAD-BEARING assembled-validity check (DOM pre-insert runs validity
|
|
317
|
+
// AFTER the conversion step): whatever mid-conversion reactions did,
|
|
318
|
+
// including a LATER argument's reaction poisoning an EARLIER scratch
|
|
319
|
+
// child, the returned list never contains a host-containing node, so the
|
|
320
|
+
// CONVERSION window cannot feed one into the record. (The record-to-
|
|
321
|
+
// placement window is separately shielded by the placement-time skip in
|
|
322
|
+
// applyActualAssignment.)
|
|
323
|
+
guardCycle(host, nodes);
|
|
324
|
+
for (const n of nodes) N_removeChild.call(scratch, n);
|
|
325
|
+
return nodes;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Commit an authored mutation: record it, re-derive, re-place. `touched`
|
|
330
|
+
* lists the nodes this op inserted, moved, or removed, so the resync step
|
|
331
|
+
* honours the record's position for exactly those nodes (an expressed move)
|
|
332
|
+
* while adopting physical order for everything else.
|
|
333
|
+
*
|
|
334
|
+
* @param {Element} host
|
|
335
|
+
* @param {SlotState} state
|
|
336
|
+
* @param {Node[]} [touched]
|
|
337
|
+
*/
|
|
338
|
+
function commitAuthored(host, state, touched) {
|
|
339
|
+
// UNION under the latch: a nested record op cannot run its own pass, so
|
|
340
|
+
// its touched set must survive until the outer loop's next iteration; a
|
|
341
|
+
// second nested op must not clobber the first one's order authority.
|
|
342
|
+
if (state.applying && state.pendingRecordNodes) {
|
|
343
|
+
for (const n of touched || []) state.pendingRecordNodes.add(n);
|
|
344
|
+
} else {
|
|
345
|
+
state.pendingRecordNodes = new Set(touched || []);
|
|
346
|
+
}
|
|
347
|
+
// An author record op on a node ends its self-heal adoption: once the
|
|
348
|
+
// author takes over, the node routes by its own attribute again (also
|
|
349
|
+
// covers an attribute change made while the sensors were down).
|
|
350
|
+
if (state.adoptedKey && touched) {
|
|
351
|
+
for (const n of touched) state.adoptedKey.delete(n);
|
|
352
|
+
}
|
|
353
|
+
repartition(state);
|
|
354
|
+
applySlotAssignments(host);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Install the per-instance interception on a LIGHT-DOM host so native DOM
|
|
359
|
+
* writes drive the slot record. Own data properties / accessors shadow the
|
|
360
|
+
* prototype methods; installed once, never removed (so a mutation while the
|
|
361
|
+
* host is disconnected still updates the record). No-op in shadow DOM.
|
|
362
|
+
*
|
|
363
|
+
* @param {Element} host
|
|
364
|
+
*/
|
|
365
|
+
export function installSlotInterception(host) {
|
|
366
|
+
if (!inBrowser) return;
|
|
367
|
+
const h = /** @type {any} */ (host);
|
|
368
|
+
if (h[INTERCEPTED]) return;
|
|
369
|
+
captureNatives();
|
|
370
|
+
h[INTERCEPTED] = true;
|
|
371
|
+
const state = ensureSlotState(host);
|
|
372
|
+
|
|
373
|
+
h.appendChild = function (node) {
|
|
374
|
+
if (h[RENDERING]) return N_appendChild.call(this, node);
|
|
375
|
+
const nodes = expandArg(host, node, false);
|
|
376
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
377
|
+
authoredSplice(state, nodes, null);
|
|
378
|
+
commitAuthored(host, state, nodes);
|
|
379
|
+
return node;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
h.insertBefore = function (node, ref) {
|
|
383
|
+
if (h[RENDERING]) return N_insertBefore.call(this, node, ref);
|
|
384
|
+
// WebIDL converts arguments LEFT TO RIGHT and DOM pre-insert validity
|
|
385
|
+
// runs the cycle (HierarchyRequestError) step before the ref
|
|
386
|
+
// (NotFoundError) step: validate parameter 1's type and insertability
|
|
387
|
+
// FIRST, without draining a fragment (the drain happens in expandArg
|
|
388
|
+
// only after every validity check passed, so an error leaves the
|
|
389
|
+
// fragment intact like native).
|
|
390
|
+
if (!isRealmNode(node)) {
|
|
391
|
+
throw new TypeError('Failed to execute insertBefore on the host: parameter 1 is not of type Node.');
|
|
392
|
+
}
|
|
393
|
+
if (ref != null && !isRealmNode(ref)) {
|
|
394
|
+
throw new TypeError('Failed to execute insertBefore on the host: parameter 2 is not of type Node.');
|
|
395
|
+
}
|
|
396
|
+
// Both conversions done; now DOM validity in step order (cycle, then the
|
|
397
|
+
// ref's NotFoundError below, then node-type inside expandArg).
|
|
398
|
+
guardCycle(host, node.nodeType === 11 ? Array.from(node.childNodes) : [node]);
|
|
399
|
+
// A non-null ref MUST be an assigned child (native throws NotFoundError
|
|
400
|
+
// otherwise), checked before the self-ref no-op so insertBefore(x, x) on
|
|
401
|
+
// a NON-child still throws like native.
|
|
402
|
+
if (
|
|
403
|
+
ref != null &&
|
|
404
|
+
(state.authored.indexOf(ref) === -1 || !isVirtualChild(host, ref))
|
|
405
|
+
) {
|
|
406
|
+
throw new DOMException(
|
|
407
|
+
'insertBefore: reference node is not an assigned child of this host',
|
|
408
|
+
'NotFoundError',
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
// insertBefore(n, n) with n an existing child is a native no-op.
|
|
412
|
+
if (node === ref) return node;
|
|
413
|
+
const nodes = expandArg(host, node, false);
|
|
414
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
415
|
+
authoredSplice(state, nodes, ref || null);
|
|
416
|
+
commitAuthored(host, state, nodes);
|
|
417
|
+
return node;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
h.removeChild = function (node) {
|
|
421
|
+
if (h[RENDERING]) return N_removeChild.call(this, node);
|
|
422
|
+
const i = state.authored.indexOf(node);
|
|
423
|
+
if (i === -1) return N_removeChild.call(this, node);
|
|
424
|
+
// Record-listed but physically moved elsewhere out-of-band: native
|
|
425
|
+
// answers "not a child" (the record heals on the next apply).
|
|
426
|
+
if (!isVirtualChild(host, node)) {
|
|
427
|
+
throw new DOMException(
|
|
428
|
+
'removeChild: the node is not an assigned child of this host',
|
|
429
|
+
'NotFoundError',
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
state.authored.splice(i, 1);
|
|
433
|
+
commitAuthored(host, state, [node]);
|
|
434
|
+
return node;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
h.replaceChild = function (newNode, oldNode) {
|
|
438
|
+
if (h[RENDERING]) return N_replaceChild.call(this, newNode, oldNode);
|
|
439
|
+
const i = state.authored.indexOf(oldNode);
|
|
440
|
+
if (i === -1) return N_replaceChild.call(this, newNode, oldNode);
|
|
441
|
+
// WebIDL converts parameters left to right and the cycle check precedes
|
|
442
|
+
// the NotFound check: validate parameter 1 first, without draining.
|
|
443
|
+
if (!isRealmNode(newNode)) {
|
|
444
|
+
throw new TypeError('Failed to execute replaceChild on the host: parameter 1 is not of type Node.');
|
|
445
|
+
}
|
|
446
|
+
guardCycle(host, newNode.nodeType === 11 ? Array.from(newNode.childNodes) : [newNode]);
|
|
447
|
+
if (!isVirtualChild(host, oldNode)) {
|
|
448
|
+
throw new DOMException(
|
|
449
|
+
'replaceChild: the node to be replaced is not an assigned child of this host',
|
|
450
|
+
'NotFoundError',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (newNode === oldNode) return oldNode; // native no-op
|
|
454
|
+
const nodes = expandArg(host, newNode, false); // guards cycle before draining
|
|
455
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
456
|
+
// Remove any incoming node already authored (so it MOVES to the new slot),
|
|
457
|
+
// but never oldNode itself: it is the replacement target, so skipping it
|
|
458
|
+
// keeps `at` valid even for the pathological replaceChild(fragmentWithOld,
|
|
459
|
+
// old) input, avoiding a splice(-1) that would corrupt an unrelated sibling.
|
|
460
|
+
for (const n of nodes) {
|
|
461
|
+
if (n === oldNode) continue;
|
|
462
|
+
const j = state.authored.indexOf(n);
|
|
463
|
+
if (j !== -1) state.authored.splice(j, 1);
|
|
464
|
+
}
|
|
465
|
+
const at = state.authored.indexOf(oldNode);
|
|
466
|
+
state.authored.splice(at, 1, ...nodes);
|
|
467
|
+
commitAuthored(host, state, [...nodes, oldNode]);
|
|
468
|
+
return oldNode;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
h.append = function (...args) {
|
|
472
|
+
if (h[RENDERING]) return N_append.apply(this, args);
|
|
473
|
+
const nodes = convertVariadicArgs(host, args);
|
|
474
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
475
|
+
authoredSplice(state, nodes, null);
|
|
476
|
+
commitAuthored(host, state, nodes);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
h.prepend = function (...args) {
|
|
480
|
+
if (h[RENDERING]) return N_prepend.apply(this, args);
|
|
481
|
+
const nodes = convertVariadicArgs(host, args);
|
|
482
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
483
|
+
// Remove any incoming node from its current position, then insert at the
|
|
484
|
+
// FRONT via unshift. Passing `authored[0]` as an authoredSplice ref would be
|
|
485
|
+
// wrong, because that ref is captured before the incoming-removal, so
|
|
486
|
+
// prepending the current first child loses the ref and appends at the end.
|
|
487
|
+
for (const n of nodes) {
|
|
488
|
+
const j = state.authored.indexOf(n);
|
|
489
|
+
if (j !== -1) state.authored.splice(j, 1);
|
|
490
|
+
}
|
|
491
|
+
state.authored.unshift(...nodes);
|
|
492
|
+
commitAuthored(host, state, nodes);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
h.replaceChildren = function (...args) {
|
|
496
|
+
if (h[RENDERING]) return N_replaceChildren.apply(this, args);
|
|
497
|
+
const nodes = convertVariadicArgs(host, args);
|
|
498
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
499
|
+
const displaced = state.authored;
|
|
500
|
+
state.authored = nodes.slice();
|
|
501
|
+
commitAuthored(host, state, [...displaced, ...nodes]);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
Object.defineProperty(h, 'innerHTML', {
|
|
505
|
+
configurable: true,
|
|
506
|
+
get() {
|
|
507
|
+
return INNER_HTML_DESC.get.call(this);
|
|
508
|
+
},
|
|
509
|
+
set(str) {
|
|
510
|
+
if (h[RENDERING]) {
|
|
511
|
+
INNER_HTML_DESC.set.call(this, str);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Parse in a DIV (the "in body" fragment context a custom-element host
|
|
515
|
+
// gets natively): a <template> retains table-section tokens (<td>,
|
|
516
|
+
// <tr>) that the real host context drops to text.
|
|
517
|
+
const tmp = host.ownerDocument.createElement('div');
|
|
518
|
+
// innerHTML IS [LegacyNullToEmptyString]: null maps to the empty
|
|
519
|
+
// string (the common clear idiom must clear, not insert the text
|
|
520
|
+
// "null"); undefined stringifies; a Symbol throws (ToString).
|
|
521
|
+
INNER_HTML_DESC.set.call(tmp, str === null ? '' : `${str}`);
|
|
522
|
+
const nodes = Array.from(tmp.childNodes);
|
|
523
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
524
|
+
const displaced = state.authored;
|
|
525
|
+
state.authored = nodes;
|
|
526
|
+
commitAuthored(host, state, [...displaced, ...nodes]);
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
Object.defineProperty(h, 'textContent', {
|
|
531
|
+
configurable: true,
|
|
532
|
+
get() {
|
|
533
|
+
return TEXT_CONTENT_DESC.get.call(this);
|
|
534
|
+
},
|
|
535
|
+
set(str) {
|
|
536
|
+
if (h[RENDERING]) {
|
|
537
|
+
TEXT_CONTENT_DESC.set.call(this, str);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// Node.textContent is a NULLABLE DOMString? (LegacyNullToEmptyString
|
|
541
|
+
// belongs to innerHTML, not here): WebIDL converts undefined to null
|
|
542
|
+
// for nullable types, so BOTH null and undefined EMPTY, verified
|
|
543
|
+
// against all three engines. A Symbol still throws (ToString).
|
|
544
|
+
const nodes =
|
|
545
|
+
str == null || str === ''
|
|
546
|
+
? []
|
|
547
|
+
: [host.ownerDocument.createTextNode(`${str}`)];
|
|
548
|
+
for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
549
|
+
const displaced = state.authored;
|
|
550
|
+
state.authored = nodes;
|
|
551
|
+
commitAuthored(host, state, [...displaced, ...nodes]);
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Render-owned slot application
|
|
558
|
+
// ---------------------------------------------------------------------------
|