@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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assignment commit: placing assigned nodes, restoring fallback content
|
|
3
|
+
* and firing slotchange.
|
|
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 { inBrowser } from './polyfills.js';
|
|
11
|
+
import { ensureSlotState, hasSlotState, keyOfName } from './state.js';
|
|
12
|
+
import { FRAMEWORK_DETACHED, LIGHT_SLOT_ATTR, PROJECTION_ACTUAL, PROJECTION_ATTR, PROJECTION_FALLBACK, SLOT_FALLBACK_FRAG, SLOT_OWNER, SLOT_OWNER_ATTR } from './symbols.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* True when this host's current markup is FRAMEWORK-RENDERED output rather
|
|
16
|
+
* than author-written children: a rendered light template carries the
|
|
17
|
+
* framework's own `slot[data-webjs-light]` elements, an attribute only the
|
|
18
|
+
* renderer / SSR ever stamps (own-slot filtered so a nested serialized
|
|
19
|
+
* component inside genuinely-authored children does not misfire; and
|
|
20
|
+
* `data-wj-host` is NOT usable here, since connectedCallback stamps it on
|
|
21
|
+
* every light host before this check runs). The connectedCallback branch
|
|
22
|
+
* chooser uses this STRUCTURAL signal to pick adopt-not-capture for a
|
|
23
|
+
* framework-rendered subtree, so a back/forward snapshot restore
|
|
24
|
+
* (post-hydration HTML, no `webjs-hydrate` marker) adopts the projected
|
|
25
|
+
* children instead of hoovering the rendered tree into `authored` (the #1006
|
|
26
|
+
* duplication shape on the restore path).
|
|
27
|
+
*
|
|
28
|
+
* @param {Element} host
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function hasFrameworkRenderedSubtree(host) {
|
|
32
|
+
if (!inBrowser) return false;
|
|
33
|
+
// BOTH attributes are required. data-webjs-light alone is stamped at
|
|
34
|
+
// TEMPLATE COMPILE time on every <slot> in every html template, including a
|
|
35
|
+
// slot FORWARDED as an authored child of a nested component tag
|
|
36
|
+
// (html`<inner-shell><slot>fallback</slot></inner-shell>`), so matching it
|
|
37
|
+
// alone would misfire on the forwarding shape at a client-side first mount
|
|
38
|
+
// and adopt (discard) the forwarded slot. data-projection is stamped only
|
|
39
|
+
// when the framework has PLACED the slot (SSR substitution or the apply
|
|
40
|
+
// step), so light + projection together mean genuinely rendered output.
|
|
41
|
+
for (const el of host.querySelectorAll(`slot[${LIGHT_SLOT_ATTR}][${PROJECTION_ATTR}]`)) {
|
|
42
|
+
if (isOwnSlot(host, el)) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when `slot` belongs to `host` directly: no OTHER custom element
|
|
49
|
+
* sits between them. A slot nested inside a child custom element belongs
|
|
50
|
+
* to THAT component and is applied from its own record.
|
|
51
|
+
*
|
|
52
|
+
* SUBTLETY: for a slot in a fully DETACHED chain the walk ends at null
|
|
53
|
+
* without reaching `host` and returns vacuously true. Callers that must not
|
|
54
|
+
* treat a FOREIGN detached slot as owned (the prune rule, isVirtualChild)
|
|
55
|
+
* pair this with a `host.contains(p)` gate; adopted-SSR children in the
|
|
56
|
+
* detached old chain survive pruning via their FRAMEWORK_DETACHED mark, not
|
|
57
|
+
* via this walk.
|
|
58
|
+
*
|
|
59
|
+
* @param {Element} host
|
|
60
|
+
* @param {Element} slot
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function isOwnSlot(host, slot) {
|
|
64
|
+
// Template-ownership is authoritative when known: a forwarded slot sits
|
|
65
|
+
// physically inside a child component but belongs to the host whose
|
|
66
|
+
// template rendered it. Two carriers of the SAME fact, consulted in order:
|
|
67
|
+
// 1. the SLOT_OWNER symbol, stamped by the client renderer at render
|
|
68
|
+
// time (createInstance) and thus present once this host has rendered;
|
|
69
|
+
// 2. the data-wj-slot-owner attribute, the SSR carrier, which is the
|
|
70
|
+
// ACTIVE resolver on the adopt/hydration path (adoptSSRAssignments
|
|
71
|
+
// runs in connectedCallback, before the deferred first render stamps
|
|
72
|
+
// the symbol), resolved by nearest-matching-tag ancestor.
|
|
73
|
+
const owner = /** @type {any} */ (slot)[SLOT_OWNER];
|
|
74
|
+
if (owner) return owner === host;
|
|
75
|
+
const ownerTag =
|
|
76
|
+
typeof slot.getAttribute === 'function' ? slot.getAttribute(SLOT_OWNER_ATTR) : null;
|
|
77
|
+
if (ownerTag) {
|
|
78
|
+
const resolved = ownerHostFor(slot, ownerTag);
|
|
79
|
+
if (resolved) return resolved === host;
|
|
80
|
+
// Unresolvable (a detached chain the owner is no longer an ancestor of):
|
|
81
|
+
// fall through to the structural walk rather than falsely denying.
|
|
82
|
+
}
|
|
83
|
+
// Structural fallback: no OTHER custom element sits between slot and host.
|
|
84
|
+
for (let p = slot.parentElement; p && p !== host; p = p.parentElement) {
|
|
85
|
+
if (p.tagName.includes('-')) return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The host a `data-wj-slot-owner="<tag>"` attribute resolves to: the nearest
|
|
92
|
+
* ANCESTOR whose tag matches, by tag alone (NOT gated on SLOT_STATE). The
|
|
93
|
+
* gate would be wrong during the connect-time chooser, where an inner host
|
|
94
|
+
* has not upgraded yet, so a forwarded slot would fail to resolve to its
|
|
95
|
+
* outer owner and the outer would wrongly capture-hoover the SSR subtree.
|
|
96
|
+
* One-level forwarding resolves cleanly; same-tag-nested forwarding picks
|
|
97
|
+
* the nearest (the accepted edge, no worse than the structural walk it
|
|
98
|
+
* replaces).
|
|
99
|
+
*
|
|
100
|
+
* @param {Element} slot
|
|
101
|
+
* @param {string} ownerTag
|
|
102
|
+
* @returns {Element | null}
|
|
103
|
+
*/
|
|
104
|
+
function ownerHostFor(slot, ownerTag) {
|
|
105
|
+
const want = ownerTag.toLowerCase();
|
|
106
|
+
for (let p = slot.parentElement; p; p = p.parentElement) {
|
|
107
|
+
if (p.tagName.toLowerCase() === want) return p;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set a slot to actual-assignment mode and move the given nodes into it.
|
|
114
|
+
* Preserves DOM identity by re-using the same Node references when they
|
|
115
|
+
* are already inside the slot in the same order.
|
|
116
|
+
*
|
|
117
|
+
* @param {SlotState} state
|
|
118
|
+
* @param {HTMLSlotElement} slot
|
|
119
|
+
* @param {Node[]} assigned
|
|
120
|
+
* @returns {boolean} True if the slot's assignment changed compared to
|
|
121
|
+
* its last snapshot (so slotchange should fire).
|
|
122
|
+
*/
|
|
123
|
+
export function applyActualAssignment(state, slot, assigned) {
|
|
124
|
+
// These nodes are being placed into an own actual slot, so they are
|
|
125
|
+
// author-live now: clear the prune exemption on EVERY path (including the
|
|
126
|
+
// unchanged and in-place fast paths below, which the router's morph hits).
|
|
127
|
+
// Missing this leaves a reprojected node permanently exempt, so a later
|
|
128
|
+
// el.remove() / cross-host move on it would not be pruned (zombie / theft).
|
|
129
|
+
for (const n of assigned) FRAMEWORK_DETACHED.delete(n);
|
|
130
|
+
const wasFallback = slot.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL;
|
|
131
|
+
const prev = state.lastSnapshot.get(slot) || [];
|
|
132
|
+
const setChanged = wasFallback || !arraysEqual(prev, assigned);
|
|
133
|
+
|
|
134
|
+
// Physical fast path: the assigned nodes are ALREADY the slot's children in
|
|
135
|
+
// the same order (the idempotent no-change pass, and the router's morph
|
|
136
|
+
// which reconciles in place then syncs the record). Settle the snapshot;
|
|
137
|
+
// slotchange reflects the SET change vs the previous snapshot. Verifying
|
|
138
|
+
// physically (never trusting the snapshot alone) also makes the apply
|
|
139
|
+
// self-repairing after a bypass move pulled a node out of the slot.
|
|
140
|
+
if (!wasFallback && arraysEqual(Array.from(slot.childNodes), assigned)) {
|
|
141
|
+
state.lastSnapshot.set(slot, assigned.slice());
|
|
142
|
+
return setChanged;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Preserve fallback content. If the slot currently holds fallback nodes
|
|
146
|
+
// (either because we just hydrated from SSR's data-projection="fallback"
|
|
147
|
+
// or because the slot-part placed them there at bind time), move them
|
|
148
|
+
// back into the part-owned holding fragment so they survive for a later
|
|
149
|
+
// transition. Identified via the SLOT_FALLBACK_FRAG symbol that the
|
|
150
|
+
// slot-part wrote to the element.
|
|
151
|
+
const fallbackFrag = /** @type {DocumentFragment | undefined} */ (
|
|
152
|
+
/** @type {any} */ (slot)[SLOT_FALLBACK_FRAG]
|
|
153
|
+
);
|
|
154
|
+
if (wasFallback && fallbackFrag) {
|
|
155
|
+
while (slot.firstChild) fallbackFrag.appendChild(slot.firstChild);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Incremental reconcile, native parity: an assignment change must never
|
|
159
|
+
// reparent a SURVIVING assigned node (native shadow assignment does not
|
|
160
|
+
// move host children), or appending one sibling would bounce every nested
|
|
161
|
+
// custom element through disconnect/connect, drop focus, and reload an
|
|
162
|
+
// <iframe>/<video> in the projected content. Remove departures, then
|
|
163
|
+
// position each assigned node touching ONLY the new, departing, or
|
|
164
|
+
// out-of-order ones.
|
|
165
|
+
const want = new Set(assigned);
|
|
166
|
+
for (const c of Array.from(slot.childNodes)) {
|
|
167
|
+
if (!want.has(c)) slot.removeChild(c);
|
|
168
|
+
}
|
|
169
|
+
let cursor = slot.firstChild;
|
|
170
|
+
for (const node of assigned) {
|
|
171
|
+
if (node === cursor) {
|
|
172
|
+
cursor = cursor.nextSibling;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
slot.insertBefore(node, cursor);
|
|
176
|
+
}
|
|
177
|
+
slot.setAttribute(PROJECTION_ATTR, PROJECTION_ACTUAL);
|
|
178
|
+
state.lastSnapshot.set(slot, assigned.slice());
|
|
179
|
+
return setChanged;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set a slot to fallback mode: clear any actual-assignment children and
|
|
184
|
+
* restore the part-owned fallback fragment. The record keeps the nodes
|
|
185
|
+
* (they are values now), so a later authored write or slot re-creation
|
|
186
|
+
* re-places them.
|
|
187
|
+
*
|
|
188
|
+
* @param {SlotState} state
|
|
189
|
+
* @param {HTMLSlotElement} slot
|
|
190
|
+
* @returns {boolean} True if the slot transitioned from actual to
|
|
191
|
+
* fallback this pass.
|
|
192
|
+
*/
|
|
193
|
+
export function applyFallback(state, slot) {
|
|
194
|
+
const wasActual = slot.getAttribute(PROJECTION_ATTR) === PROJECTION_ACTUAL;
|
|
195
|
+
slot.setAttribute(PROJECTION_ATTR, PROJECTION_FALLBACK);
|
|
196
|
+
if (!wasActual) {
|
|
197
|
+
// Already fallback. Make sure the fallback content is materialised
|
|
198
|
+
// in the slot if the slot-part has a holding fragment with nodes.
|
|
199
|
+
restoreFallbackInto(slot);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Slot transitioning from actual to fallback (the record no longer has
|
|
204
|
+
// content for this name).
|
|
205
|
+
state.lastSnapshot.delete(slot);
|
|
206
|
+
while (slot.firstChild) slot.removeChild(slot.firstChild);
|
|
207
|
+
restoreFallbackInto(slot);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Move the slot-part's holding fragment back into the slot. No-op if no
|
|
213
|
+
* fragment is attached or it is already empty.
|
|
214
|
+
*
|
|
215
|
+
* @param {HTMLSlotElement} slot
|
|
216
|
+
*/
|
|
217
|
+
function restoreFallbackInto(slot) {
|
|
218
|
+
const frag = /** @type {DocumentFragment | undefined} */ (
|
|
219
|
+
/** @type {any} */ (slot)[SLOT_FALLBACK_FRAG]
|
|
220
|
+
);
|
|
221
|
+
if (!frag || frag.childNodes.length === 0) return;
|
|
222
|
+
slot.appendChild(frag);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Renderer teardown hook (called from render-client.js)
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Detach a slot's record-owned children from the slot element before the
|
|
231
|
+
* renderer's template teardown disposes the slot's subtree (a conditional
|
|
232
|
+
* fragment collapsing). The RECORD keeps the node references, so when a
|
|
233
|
+
* re-render re-creates the slot, `applySlotAssignments` re-places the very
|
|
234
|
+
* same nodes: children are values, teardown never disposes consumer nodes.
|
|
235
|
+
*
|
|
236
|
+
* @param {Element} host
|
|
237
|
+
* @param {HTMLSlotElement} slot
|
|
238
|
+
*/
|
|
239
|
+
export function rescueAssignedNodes(host, slot) {
|
|
240
|
+
if (!hasSlotState(host)) return;
|
|
241
|
+
const state = ensureSlotState(host);
|
|
242
|
+
const name = keyOfName(slot.getAttribute('name'));
|
|
243
|
+
const assigned = state.assignedByName.get(name);
|
|
244
|
+
if (assigned) {
|
|
245
|
+
for (const node of assigned) {
|
|
246
|
+
if (node.parentNode === slot) {
|
|
247
|
+
// Framework-detached on teardown: the record keeps the ref (children
|
|
248
|
+
// are values), so the prune rule must not drop it while it is parked
|
|
249
|
+
// out of the tree waiting for a re-created slot to re-place it.
|
|
250
|
+
FRAMEWORK_DETACHED.add(node);
|
|
251
|
+
slot.removeChild(node);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
state.lastSnapshot.delete(slot);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// slotchange event dispatch
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/** Fire a `slotchange` event on the slot (bubbles, not composed; per spec). */
|
|
263
|
+
export function fireSlotChange(slot) {
|
|
264
|
+
slot.dispatchEvent(new Event('slotchange', { bubbles: true, composed: false }));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Queue a coalesced `slotchange` for a slot. The event is dispatched on a
|
|
269
|
+
* microtask, and a slot that changes more than once before the flush fires
|
|
270
|
+
* exactly once (native `slotchange` timing: async and coalesced per slot).
|
|
271
|
+
* A slot detached before the flush is skipped.
|
|
272
|
+
*
|
|
273
|
+
* @param {SlotState} state
|
|
274
|
+
* @param {HTMLSlotElement} slot
|
|
275
|
+
*/
|
|
276
|
+
export function queueSlotChange(state, slot) {
|
|
277
|
+
if (!state.pendingSlotChanges) state.pendingSlotChanges = new Set();
|
|
278
|
+
state.pendingSlotChanges.add(slot);
|
|
279
|
+
if (state.slotChangeScheduled) return;
|
|
280
|
+
state.slotChangeScheduled = true;
|
|
281
|
+
queueMicrotask(() => {
|
|
282
|
+
state.slotChangeScheduled = false;
|
|
283
|
+
const pending = state.pendingSlotChanges || new Set();
|
|
284
|
+
state.pendingSlotChanges = new Set();
|
|
285
|
+
// Fire regardless of connectivity: native slot assignment (and its
|
|
286
|
+
// slotchange) works in disconnected trees, and dropping the event here
|
|
287
|
+
// would lose it forever for a detached-then-reused host.
|
|
288
|
+
for (const s of pending) fireSlotChange(s);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Misc helpers
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/** Strict per-index equality on two arrays. */
|
|
297
|
+
export function arraysEqual(a, b) {
|
|
298
|
+
if (a === b) return true;
|
|
299
|
+
if (a.length !== b.length) return false;
|
|
300
|
+
for (let i = 0; i < a.length; i++) {
|
|
301
|
+
if (a[i] !== b[i]) return false;
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|