@webjsdev/core 0.7.50 → 0.7.53
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 +3 -3
- 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/css.d.ts +9 -0
- package/src/escape.d.ts +2 -0
- package/src/html.d.ts +9 -0
- package/src/render-client/parts.js +2039 -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 +938 -0
- package/src/render-server/text.js +163 -0
- package/src/render-server.js +6 -2405
- package/src/repeat.d.ts +15 -0
- package/src/rich-fetch.d.ts +4 -0
- package/src/router-client/anchors.js +44 -0
- package/src/router-client/boundaries.js +221 -0
- package/src/router-client/constants.js +206 -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 +182 -0
- package/src/router-client/fetch-apply.js +432 -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 +952 -0
- package/src/router-client/prefetch.js +831 -0
- package/src/router-client/scroll.js +308 -0
- package/src/router-client/snapshot-cache.js +113 -0
- package/src/router-client/state.js +155 -0
- package/src/router-client/stream.js +206 -0
- package/src/router-client/swap.js +454 -0
- package/src/router-client/upgrade.js +261 -0
- package/src/router-client/view-transition.js +223 -0
- package/src/router-client.d.ts +11 -2
- package/src/router-client.js +78 -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
- package/src/suspense.d.ts +9 -0
- package/src/websocket-client.d.ts +22 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection: placing authored children into their slots, and resyncing
|
|
3
|
+
* after a re-render.
|
|
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 { applyActualAssignment, applyFallback, arraysEqual, isOwnSlot, queueSlotChange } from './assignment.js';
|
|
11
|
+
import { EMPTY_NODE_SET, isInsideAuthored, parkFor } from './interception.js';
|
|
12
|
+
import { inBrowser, manualSlotFor } from './polyfills.js';
|
|
13
|
+
import { N_appendChild, N_removeChild, processBackstop, withRendererWrites } from './sensors.js';
|
|
14
|
+
import { effectiveKeyOf, ensureSlotState, keyOfName, repartition } from './state.js';
|
|
15
|
+
import { FRAMEWORK_DETACHED, LIGHT_SLOT_ATTR, PARK, PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_FALLBACK_FRAG, SLOT_STATE } from './symbols.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Replace the authored content assigned to slot `name` with `nodes`, in place
|
|
19
|
+
* of the old slice's position (a new name appends). The ONE public seam the
|
|
20
|
+
* client router uses to reconcile a reused light host's projected content
|
|
21
|
+
* during a same-route morph (replacing the deleted `setSlotContent`): the
|
|
22
|
+
* router never touches `authored` / `assignedByName` / `lastSnapshot` directly,
|
|
23
|
+
* and this is the same record-then-place primitive the interception layer runs.
|
|
24
|
+
*
|
|
25
|
+
* @param {Element} host
|
|
26
|
+
* @param {string | null} name
|
|
27
|
+
* @param {Node[] | Node | null} nodes
|
|
28
|
+
*/
|
|
29
|
+
export function projectAuthored(host, name, nodes) {
|
|
30
|
+
const state = ensureSlotState(host);
|
|
31
|
+
const key = keyOfName(name);
|
|
32
|
+
const list = Array.isArray(nodes) ? nodes.filter(Boolean) : nodes ? [nodes] : [];
|
|
33
|
+
// Evict the old slice by the node's EFFECTIVE key (the manual assign()
|
|
34
|
+
// overlay when present, else the slot= attribute): filtering on the raw
|
|
35
|
+
// attribute would evict a manually-assigned attribute-less node from the
|
|
36
|
+
// default slice and silently drop another slot's content.
|
|
37
|
+
let at = state.authored.findIndex((n) => effectiveKeyOf(state, n) === key);
|
|
38
|
+
const evicted = state.authored.filter((n) => effectiveKeyOf(state, n) === key);
|
|
39
|
+
state.authored = state.authored.filter((n) => effectiveKeyOf(state, n) !== key);
|
|
40
|
+
if (at === -1 || at > state.authored.length) at = state.authored.length;
|
|
41
|
+
for (const n of list) {
|
|
42
|
+
// Stamp the slice key onto element nodes so the derived partition
|
|
43
|
+
// matches the projection, EXCEPT nodes with a live manual assign()
|
|
44
|
+
// entry: their routing is element-bound (the overlay outranks the
|
|
45
|
+
// attribute), and the attribute is the author's latent intent that must
|
|
46
|
+
// survive the overlay's release.
|
|
47
|
+
if (n.nodeType === 1 && !manualSlotFor(state, n)) {
|
|
48
|
+
if (key == null) /** @type {Element} */ (n).removeAttribute('slot');
|
|
49
|
+
else /** @type {Element} */ (n).setAttribute('slot', key);
|
|
50
|
+
}
|
|
51
|
+
FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
|
|
52
|
+
}
|
|
53
|
+
state.authored.splice(at, 0, ...list);
|
|
54
|
+
if (state.applying && state.pendingRecordNodes) {
|
|
55
|
+
for (const n of evicted) state.pendingRecordNodes.add(n);
|
|
56
|
+
for (const n of list) state.pendingRecordNodes.add(n);
|
|
57
|
+
} else {
|
|
58
|
+
state.pendingRecordNodes = new Set([...evicted, ...list]);
|
|
59
|
+
}
|
|
60
|
+
// Scope the adoption-clear to THIS call's nodes: under the latch the
|
|
61
|
+
// pending set may carry a co-pending nested op's nodes, whose adoptions
|
|
62
|
+
// are not this projection's to end.
|
|
63
|
+
if (state.adoptedKey) {
|
|
64
|
+
for (const n of evicted) state.adoptedKey.delete(n);
|
|
65
|
+
for (const n of list) state.adoptedKey.delete(n);
|
|
66
|
+
}
|
|
67
|
+
repartition(state);
|
|
68
|
+
applySlotAssignments(host);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Native-write window + host interception (native slot-API liveness)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Place the slot record into the host's OWN light-DOM slots (#1015). The
|
|
77
|
+
* renderer's slot parts call this after the template commits, and the
|
|
78
|
+
* interception + sensors call it after an authored mutation. Idempotent and cheap on
|
|
79
|
+
* no-change passes.
|
|
80
|
+
*
|
|
81
|
+
* 1. Collect the host's OWN slots (no other custom element between the
|
|
82
|
+
* slot and the host; a nested component's slots belong to it).
|
|
83
|
+
* 2. Group by `name`, first-wins: the first slot of each name shows the
|
|
84
|
+
* record content, later duplicates show fallback.
|
|
85
|
+
* 3. `data-projection` is stamped "actual" or "fallback" accordingly;
|
|
86
|
+
* fallback content swaps through the part-owned holding fragment.
|
|
87
|
+
* 4. `slotchange` fires on slots whose assigned set actually changed.
|
|
88
|
+
*
|
|
89
|
+
* @param {Element} host
|
|
90
|
+
*/
|
|
91
|
+
export function applySlotAssignments(host) {
|
|
92
|
+
if (!inBrowser) return;
|
|
93
|
+
const state = /** @type {SlotState | undefined} */ (
|
|
94
|
+
/** @type {any} */ (host)[SLOT_STATE]
|
|
95
|
+
);
|
|
96
|
+
if (!state) return;
|
|
97
|
+
|
|
98
|
+
// Re-entrancy latch: a custom-element reaction fired by a placement or
|
|
99
|
+
// fallback removal (a disconnectedCallback doing host.appendChild) can
|
|
100
|
+
// author-write while THIS pass is mid-flight; running a nested pass
|
|
101
|
+
// would let the outer loop destroy the inner placements and desync
|
|
102
|
+
// data-projection from the snapshot. The nested call returns after
|
|
103
|
+
// flagging a re-run (its record splice already happened in the
|
|
104
|
+
// interceptor), and the outer pass repeats until a full pass runs with
|
|
105
|
+
// no re-entrant write. The end-of-pass source drain also rides this
|
|
106
|
+
// loop instead of recursing.
|
|
107
|
+
if (state.applying) {
|
|
108
|
+
state.reapply = true;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
state.applying = true;
|
|
112
|
+
try {
|
|
113
|
+
do {
|
|
114
|
+
state.reapply = false;
|
|
115
|
+
|
|
116
|
+
// 0. Self-heal, prune, re-derive, in that order. First fold in what a
|
|
117
|
+
// NON-record writer wrote inside our own actual slots (a parent
|
|
118
|
+
// component's hole committed within projected content, or a library
|
|
119
|
+
// operating on the assigned container): the apply step is the only
|
|
120
|
+
// record writer, so physical-vs-snapshot divergence means someone else
|
|
121
|
+
// wrote there, and destroying their nodes on this pass would be the
|
|
122
|
+
// one-writer violation in reverse. Then prune the record of nodes the
|
|
123
|
+
// author detached out from under us (an `el.remove()` on a projected
|
|
124
|
+
// child, or a re-parent elsewhere): their parent is no longer one of our
|
|
125
|
+
// own slots / the park, and we did not detach them ourselves. Then
|
|
126
|
+
// re-derive from the surviving `authored` so a `slot=` change (or any
|
|
127
|
+
// authored mutation) is reflected before placement.
|
|
128
|
+
const pendingNodes = state.pendingRecordNodes || EMPTY_NODE_SET;
|
|
129
|
+
state.pendingRecordNodes = undefined;
|
|
130
|
+
resyncActualSlots(host, state, pendingNodes);
|
|
131
|
+
pruneAuthored(host, state);
|
|
132
|
+
repartition(state);
|
|
133
|
+
|
|
134
|
+
// 1. The host's own slots, document order. A slot that is BOUND but not yet
|
|
135
|
+
// FINALIZED (its slot-part deferred finalize to a microtask: it carries
|
|
136
|
+
// data-webjs-light from compile time but has neither a data-projection
|
|
137
|
+
// stamp nor a harvested fallback frag) is EXCLUDED from placement this
|
|
138
|
+
// pass: treating it as fallback-mode would destroy its un-harvested
|
|
139
|
+
// fallback clone, and the finalize's own queued apply covers it one
|
|
140
|
+
// microtask later. Its NAME still counts as rendered (pendingNames) so
|
|
141
|
+
// the park step does not spuriously park (and bounce) its content.
|
|
142
|
+
/** @type {HTMLSlotElement[]} */
|
|
143
|
+
const slots = [];
|
|
144
|
+
/** @type {Set<string|null>} */
|
|
145
|
+
const pendingNames = new Set();
|
|
146
|
+
for (const el of host.querySelectorAll(`slot[${LIGHT_SLOT_ATTR}]`)) {
|
|
147
|
+
if (!isOwnSlot(host, el)) continue;
|
|
148
|
+
// An own slot can NEVER live inside AUTHORED content: a slot element the
|
|
149
|
+
// author moved or wrote into the host (another component's chunk, a
|
|
150
|
+
// spoofed stamp) is inert content, exactly like a <slot> outside a
|
|
151
|
+
// shadow tree natively. Collecting it would hand it assignments whose
|
|
152
|
+
// nodes can CONTAIN it (HierarchyRequestError at placement).
|
|
153
|
+
if (isInsideAuthored(state, host, el)) continue;
|
|
154
|
+
if (
|
|
155
|
+
!el.hasAttribute(PROJECTION_ATTR) &&
|
|
156
|
+
!(SLOT_FALLBACK_FRAG in /** @type {any} */ (el))
|
|
157
|
+
) {
|
|
158
|
+
pendingNames.add(keyOfName(el.getAttribute('name')));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
slots.push(/** @type {HTMLSlotElement} */ (el));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2. Group by current `name` attribute in document order.
|
|
165
|
+
/** @type {Map<string|null, HTMLSlotElement[]>} */
|
|
166
|
+
const groups = new Map();
|
|
167
|
+
for (const slot of slots) {
|
|
168
|
+
const name = keyOfName(slot.getAttribute('name'));
|
|
169
|
+
let arr = groups.get(name);
|
|
170
|
+
if (!arr) {
|
|
171
|
+
arr = [];
|
|
172
|
+
groups.set(name, arr);
|
|
173
|
+
}
|
|
174
|
+
arr.push(slot);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. Assign per the first-wins rule; a node manually bound via `assign()`
|
|
178
|
+
// routes to ITS slot element (native binds slottables to the receiving
|
|
179
|
+
// element), everything else to the first slot of its name.
|
|
180
|
+
/** @type {HTMLSlotElement[]} */
|
|
181
|
+
const slotsChanged = [];
|
|
182
|
+
for (const [name, group] of groups) {
|
|
183
|
+
const assigned = state.assignedByName.get(name) || [];
|
|
184
|
+
for (let i = 0; i < group.length; i++) {
|
|
185
|
+
const slot = group[i];
|
|
186
|
+
let own = assigned.filter((n) => {
|
|
187
|
+
const m = manualSlotFor(state, n);
|
|
188
|
+
return m ? m === slot : i === 0;
|
|
189
|
+
});
|
|
190
|
+
// Placement-time cycle shield (skip, never throw): a reaction
|
|
191
|
+
// fired by an EARLIER slice's departure removal can reparent the
|
|
192
|
+
// HOST into a node this slice is about to place; inserting it
|
|
193
|
+
// would throw a native HierarchyRequestError out of the whole
|
|
194
|
+
// apply pass. The filter runs BEFORE the actual/fallback
|
|
195
|
+
// decision, so a fully poisoned slice correctly degrades to
|
|
196
|
+
// FALLBACK instead of an actual-stamped empty slot, and the
|
|
197
|
+
// poisoned nodes' prune exemptions are CLEARED so the next pass's
|
|
198
|
+
// prune reaps them by their (outside-the-host) parent chain.
|
|
199
|
+
const poisoned = own.filter(
|
|
200
|
+
(n) => n === host || (n.nodeType === 1 && /** @type {Element} */ (n).contains(host)),
|
|
201
|
+
);
|
|
202
|
+
if (poisoned.length) {
|
|
203
|
+
for (const n of poisoned) FRAMEWORK_DETACHED.delete(n);
|
|
204
|
+
own = own.filter((n) => poisoned.indexOf(n) === -1);
|
|
205
|
+
}
|
|
206
|
+
if (own.length > 0) {
|
|
207
|
+
if (applyActualAssignment(state, slot, own)) {
|
|
208
|
+
slotsChanged.push(slot);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
if (applyFallback(state, slot)) slotsChanged.push(slot);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 4. Queue slotchange on slots whose assignment actually changed. Native
|
|
217
|
+
// timing: assignment recomputes synchronously (placement above already
|
|
218
|
+
// ran) but the slotchange EVENT is async and coalesced (one per slot per
|
|
219
|
+
// microtask). Synchronous dispatch here would let an author mutation
|
|
220
|
+
// inside a slotchange handler recurse into this writer mid-loop, and
|
|
221
|
+
// would fire N events for an N-node loop; coalescing matches the spec.
|
|
222
|
+
for (const slot of slotsChanged) queueSlotChange(state, slot);
|
|
223
|
+
|
|
224
|
+
// 5. Park authored nodes whose name matches no rendered own-slot. Native
|
|
225
|
+
// shadow keeps an unassigned child connected but unrendered (a nested
|
|
226
|
+
// custom element still upgrades and runs connectedCallback); a hidden
|
|
227
|
+
// holding element inside the host reproduces that. Parked nodes have
|
|
228
|
+
// parentNode === park, so the prune rule keeps them. The park is
|
|
229
|
+
// RECONCILED to exactly the current unmatched set: a node that left the
|
|
230
|
+
// record (or now matches a slot) is detached from the park, so a removed
|
|
231
|
+
// parked child ends up isConnected === false like native removeChild.
|
|
232
|
+
const matched = new Set(groups.keys());
|
|
233
|
+
for (const name of pendingNames) matched.add(name);
|
|
234
|
+
const shouldPark = new Set();
|
|
235
|
+
for (const n of state.authored) {
|
|
236
|
+
if (!matched.has(effectiveKeyOf(state, n))) shouldPark.add(n);
|
|
237
|
+
}
|
|
238
|
+
const existingPark = /** @type {any} */ (host)[PARK];
|
|
239
|
+
if (existingPark) {
|
|
240
|
+
for (const n of Array.from(existingPark.childNodes)) {
|
|
241
|
+
if (!shouldPark.has(n)) N_removeChild.call(existingPark, n);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (shouldPark.size) {
|
|
245
|
+
const park = parkFor(host);
|
|
246
|
+
for (const n of shouldPark) {
|
|
247
|
+
if (n.parentNode !== park) {
|
|
248
|
+
FRAMEWORK_DETACHED.delete(n);
|
|
249
|
+
withRendererWrites(host, () => N_appendChild.call(park, n));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// LAST step: consume the records this apply's own placements generated,
|
|
255
|
+
// while their containment evidence is still fresh. A host-to-slot
|
|
256
|
+
// placement removal processed now sees contains === true and is retained
|
|
257
|
+
// trivially, so it can never age into a stale record that a later rescue
|
|
258
|
+
// makes indistinguishable from an author removal (the retention conjuncts
|
|
259
|
+
// in processBackstop stay as a second line for any record that still
|
|
260
|
+
// straddles). Running at the END keeps any recursive fold's inner apply
|
|
261
|
+
// from racing this pass's park bookkeeping; recursion terminates because
|
|
262
|
+
// a pure-placement batch marks nothing dirty and an inner apply leaves an
|
|
263
|
+
// empty queue behind.
|
|
264
|
+
// DEFENSE-IN-DEPTH, empirically not required by any suite-constructible
|
|
265
|
+
// interleaving at this head (deleting it stays green): the window-close
|
|
266
|
+
// drains and the retention conjuncts cover every reproduced sequence.
|
|
267
|
+
// It stays because the stale-placement-record race was independently
|
|
268
|
+
// traced by two reviews, is O(pending records) cheap, provably
|
|
269
|
+
// terminates under the latch, and processing placement records with
|
|
270
|
+
// fresh containment can only ever retain correctly.
|
|
271
|
+
if (state.backstop) {
|
|
272
|
+
const placementRecords = state.backstop.takeRecords();
|
|
273
|
+
if (placementRecords.length) processBackstop(host, state, placementRecords);
|
|
274
|
+
}
|
|
275
|
+
} while (state.reapply);
|
|
276
|
+
} finally {
|
|
277
|
+
state.applying = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Self-heal `authored` against a NON-record writer that wrote INSIDE one of
|
|
283
|
+
* the host's own actual slots. Two such writers are legitimate: a parent
|
|
284
|
+
* component whose hole was authored as this host's content (its child-part
|
|
285
|
+
* marker projects into the slot, so a later array / template commit inserts
|
|
286
|
+
* or removes nodes there with no interceptor in the way), and a third-party
|
|
287
|
+
* library operating on the assigned container (the documented target for
|
|
288
|
+
* generic DOM code). The apply step is the ONLY record-driven DOM writer, so
|
|
289
|
+
* a slot whose physical childNodes diverge from its `lastSnapshot` was
|
|
290
|
+
* written by someone else since the last apply; destroying those nodes on
|
|
291
|
+
* this pass (the pre-fix behaviour) is the one-writer violation in reverse
|
|
292
|
+
* and detaches DOM a live renderer part still points at.
|
|
293
|
+
*
|
|
294
|
+
* Reconciliation rule, per diverged slot. Order authority is NODE-scoped,
|
|
295
|
+
* never pass-scoped (a pass-scoped rule made the outcome depend on which
|
|
296
|
+
* trigger happened to run the apply):
|
|
297
|
+
* - The PHYSICAL order of the slice is the base: a renderer part reordering
|
|
298
|
+
* a keyed list inside the slot is never fought back, regardless of what
|
|
299
|
+
* triggered this apply.
|
|
300
|
+
* - Nodes the current record op TOUCHED (`pendingNodes`: inserted, moved, or
|
|
301
|
+
* removed by the interceptor / assign() / router splice that triggered
|
|
302
|
+
* this apply) are taken OUT of the base and re-anchored at their
|
|
303
|
+
* record-implied position, so an author's expressed move (appendChild of
|
|
304
|
+
* an existing child = move to end) is honoured; an op-REMOVED node is in
|
|
305
|
+
* `pendingNodes` but no longer in the record, so it simply drops.
|
|
306
|
+
* - Record nodes missing from the slot (a bypass move onto the host, a
|
|
307
|
+
* genuine author detach) are also re-anchored; `pruneAuthored`, which runs
|
|
308
|
+
* right after, decides their fate structurally by their current parent
|
|
309
|
+
* (re-place vs drop), so no zombie is resurrected.
|
|
310
|
+
*
|
|
311
|
+
* @param {Element} host
|
|
312
|
+
* @param {SlotState} state
|
|
313
|
+
* @param {Set<Node>} pendingNodes
|
|
314
|
+
*/
|
|
315
|
+
function resyncActualSlots(host, state, pendingNodes) {
|
|
316
|
+
// lastSnapshot is a WeakMap (not iterable): walk the host's own APPLIED
|
|
317
|
+
// actual slots instead, which are exactly the elements a snapshot exists
|
|
318
|
+
// for.
|
|
319
|
+
for (const el of host.querySelectorAll(
|
|
320
|
+
`slot[${LIGHT_SLOT_ATTR}][${PROJECTION_ATTR}="${PROJECTION_ACTUAL}"]`,
|
|
321
|
+
)) {
|
|
322
|
+
if (!isOwnSlot(host, el)) continue;
|
|
323
|
+
if (isInsideAuthored(state, host, el)) continue; // authored content, not a slot
|
|
324
|
+
const slot = /** @type {HTMLSlotElement} */ (el);
|
|
325
|
+
const snapshot = state.lastSnapshot.get(slot);
|
|
326
|
+
if (!snapshot) continue;
|
|
327
|
+
const physical = Array.from(slot.childNodes);
|
|
328
|
+
if (arraysEqual(physical, snapshot)) continue;
|
|
329
|
+
const a = state.authored;
|
|
330
|
+
const key = keyOfName(slot.getAttribute('name'));
|
|
331
|
+
|
|
332
|
+
// Base: the slice in PHYSICAL order, minus op-touched nodes (re-anchored
|
|
333
|
+
// below or op-removed). A physical node unknown to the record is a true
|
|
334
|
+
// addition (renderer hole / library write) and folds in at its physical
|
|
335
|
+
// position; when the container is a NAMED slot and the node's own
|
|
336
|
+
// attribute would key it elsewhere, remember the container's key as the
|
|
337
|
+
// node's ADOPTED key so repartition does not teleport it out of the
|
|
338
|
+
// container it was written into.
|
|
339
|
+
const merged = physical.filter((n) => !pendingNodes.has(n));
|
|
340
|
+
for (const n of merged) {
|
|
341
|
+
if (a.indexOf(n) !== -1) continue; // known to the record already
|
|
342
|
+
if (effectiveKeyOf(state, n) !== key) {
|
|
343
|
+
if (!state.adoptedKey) state.adoptedKey = new WeakMap();
|
|
344
|
+
state.adoptedKey.set(n, key);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Re-anchor every record node of this slice that is not already in the
|
|
349
|
+
// base, in record order, each after the LAST base member that precedes
|
|
350
|
+
// it in the record (start when none). Covers op-inserted/moved nodes and
|
|
351
|
+
// record nodes missing from the slot (prune settles those right after).
|
|
352
|
+
for (const n of a) {
|
|
353
|
+
if (merged.indexOf(n) !== -1) continue;
|
|
354
|
+
if (effectiveKeyOf(state, n) !== key) continue;
|
|
355
|
+
const nIdx = a.indexOf(n);
|
|
356
|
+
let at = 0;
|
|
357
|
+
for (let i = 0; i < merged.length; i++) {
|
|
358
|
+
const mIdx = a.indexOf(merged[i]);
|
|
359
|
+
if (mIdx !== -1 && mIdx < nIdx) at = i + 1;
|
|
360
|
+
}
|
|
361
|
+
merged.splice(at, 0, n);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Replace the old slice with the merged one at the old slice's position.
|
|
365
|
+
const involved = new Set(merged);
|
|
366
|
+
for (const n of snapshot) involved.add(n);
|
|
367
|
+
let at = -1;
|
|
368
|
+
let seen = 0;
|
|
369
|
+
for (const n of a) {
|
|
370
|
+
if (involved.has(n)) {
|
|
371
|
+
if (at === -1) at = seen;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
seen += 1;
|
|
375
|
+
}
|
|
376
|
+
const rest = a.filter((n) => !involved.has(n));
|
|
377
|
+
if (at === -1 || at > rest.length) at = rest.length;
|
|
378
|
+
rest.splice(at, 0, ...merged);
|
|
379
|
+
state.authored = rest;
|
|
380
|
+
// The snapshot itself is settled by the placement step this pass.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Prune `authored` of nodes the author detached out from under the record: a
|
|
386
|
+
* node whose parent is neither one of the host's own actual slots nor the park,
|
|
387
|
+
* and which the framework did not itself detach (capture / teardown rescue mark
|
|
388
|
+
* such nodes so they survive the parentless window before (re)placement). This
|
|
389
|
+
* closes the zombie-child resurrection (`el.remove()` on a projected node) and
|
|
390
|
+
* cross-host theft: the ownership question is answered structurally by the
|
|
391
|
+
* node's real parent, never by stale bookkeeping.
|
|
392
|
+
*
|
|
393
|
+
* @param {Element} host
|
|
394
|
+
* @param {SlotState} state
|
|
395
|
+
*/
|
|
396
|
+
function pruneAuthored(host, state) {
|
|
397
|
+
const park = /** @type {any} */ (host)[PARK];
|
|
398
|
+
state.authored = state.authored.filter((n) => {
|
|
399
|
+
if (FRAMEWORK_DETACHED.has(n)) return true;
|
|
400
|
+
const p = n.parentNode;
|
|
401
|
+
if (p == null) return false;
|
|
402
|
+
// A DIRECT host child is still ours: a raw bypass move pulled it out of
|
|
403
|
+
// its slot onto the host (native: a host child stays assigned), and the
|
|
404
|
+
// physically-verifying placement step re-places it this same pass.
|
|
405
|
+
if (p === host) return true;
|
|
406
|
+
if (p === park) return true;
|
|
407
|
+
// The slot-parent keep requires the slot to be recognizably OURS.
|
|
408
|
+
// lastSnapshot membership is DIRECT proof this host applied that slot,
|
|
409
|
+
// so it stands alone (covering our own detached slots: a cache()-stashed
|
|
410
|
+
// branch, a torn-down conditional, or a FORWARDED slot the re-render
|
|
411
|
+
// detached, whose structural isOwnSlot walk would wrongly hit the child
|
|
412
|
+
// component between it and this host). A merely-CONTAINED slot still
|
|
413
|
+
// needs isOwnSlot, so the apply never steals a node back from an
|
|
414
|
+
// unrelated component's torn-down slot the author moved it into (the
|
|
415
|
+
// bare isOwnSlot walk is vacuously true on any fully detached chain).
|
|
416
|
+
if (
|
|
417
|
+
p.nodeType === 1 &&
|
|
418
|
+
/** @type {Element} */ (p).tagName === 'SLOT' &&
|
|
419
|
+
/** @type {Element} */ (p).hasAttribute(LIGHT_SLOT_ATTR) &&
|
|
420
|
+
(state.lastSnapshot.has(/** @type {HTMLSlotElement} */ (p)) ||
|
|
421
|
+
(host.contains(p) && isOwnSlot(host, /** @type {Element} */ (p))))
|
|
422
|
+
) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
});
|
|
427
|
+
// The manual-assignment overlay needs no pruning here: it holds nodes via
|
|
428
|
+
// WeakRef (native holds manually assigned slottables weakly), so a removed
|
|
429
|
+
// node is not leaked, and an entry for a node assigned BEFORE it becomes a
|
|
430
|
+
// host child stays honoured when the node is later appended (the native
|
|
431
|
+
// assign-first, append-later ordering).
|
|
432
|
+
}
|