@microsoft/webui-framework 0.0.17 → 0.0.18
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/README.md +112 -33
- package/dist/component-asset.d.ts +30 -0
- package/dist/component-asset.js +213 -0
- package/dist/decorators.d.ts +2 -53
- package/dist/decorators.js +93 -171
- package/dist/element/diff.d.ts +0 -20
- package/dist/element/diff.js +25 -62
- package/dist/element/markers.d.ts +0 -44
- package/dist/element/markers.js +5 -54
- package/dist/element/styles.d.ts +0 -10
- package/dist/element/styles.js +2 -63
- package/dist/element/types.d.ts +4 -39
- package/dist/element/types.js +0 -3
- package/dist/element.d.ts +13 -131
- package/dist/element.js +101 -1355
- package/dist/hydration-mismatch.d.ts +22 -0
- package/dist/hydration-mismatch.js +87 -0
- package/dist/index.d.ts +0 -19
- package/dist/index.js +2 -21
- package/dist/lifecycle.d.ts +0 -8
- package/dist/lifecycle.js +0 -32
- package/dist/static-host.d.ts +1 -0
- package/dist/static-host.js +58 -0
- package/dist/template-element.d.ts +76 -0
- package/dist/template-element.js +1252 -0
- package/dist/template-events.d.ts +6 -0
- package/dist/template-events.js +23 -0
- package/dist/template-roots.d.ts +5 -0
- package/dist/template-roots.js +35 -0
- package/dist/template-types.d.ts +11 -15
- package/dist/template-types.js +0 -2
- package/dist/template.d.ts +2 -18
- package/dist/template.js +9 -2
- package/package.json +11 -5
package/dist/element.js
CHANGED
|
@@ -1,267 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Supports Shadow DOM or light DOM, SSR hydration, reactive updates, and compiled
|
|
7
|
-
* SSR content is reused by matching existing DOM nodes through compiled
|
|
8
|
-
* template path mapping. Client-created components use exact childNode
|
|
9
|
-
* indices from the compiled template HTML.
|
|
10
|
-
*
|
|
11
|
-
* ## SSR hydration markers
|
|
12
|
-
*
|
|
13
|
-
* The server-side handler plugin emits lightweight HTML comment markers
|
|
14
|
-
* around structural boundaries so the client can hydrate in-place:
|
|
15
|
-
*
|
|
16
|
-
* - `<!--wr-->` / `<!--/wr-->` — repeat (for-loop) block boundaries
|
|
17
|
-
* - `<!--wi-->` — repeat item boundary (one per item)
|
|
18
|
-
* - `<!--wc-->` / `<!--/wc-->` — conditional (if) block boundaries
|
|
19
|
-
*
|
|
20
|
-
* During hydration these markers are consumed: `<!--wi-->`, `<!--/wr-->`,
|
|
21
|
-
* and `<!--/wc-->` are removed from the DOM. The `<!--wr-->` start
|
|
22
|
-
* marker is kept as the runtime repeat anchor, and `<!--wc-->` is kept
|
|
23
|
-
* as the runtime condition anchor.
|
|
24
|
-
*
|
|
25
|
-
* **Marker removal is deferred** until after all path-based resolution
|
|
26
|
-
* (`$resolveSSR`, `$findSSRText`, `$finalize`) is complete. This is
|
|
27
|
-
* critical because `$resolveSSR` uses marker pairs to skip structural
|
|
28
|
-
* block content when counting element/text ordinals — removing a closing
|
|
29
|
-
* marker mid-hydration would break later resolution calls.
|
|
30
|
-
*
|
|
31
|
-
* ## Comment anchors (client-created)
|
|
32
|
-
*
|
|
33
|
-
* For client-created components (no SSR), the framework inserts empty
|
|
34
|
-
* comment nodes (`document.createComment('')`) as stable DOM anchors
|
|
35
|
-
* for conditional and repeat blocks. When SSR markers are absent,
|
|
36
|
-
* the same fallback anchors are used.
|
|
37
|
-
*
|
|
38
|
-
* These comments are invisible to the user, weigh ~0 bytes, and are the
|
|
39
|
-
* minimum DOM structure needed for the framework to operate.
|
|
40
|
-
*/
|
|
41
|
-
import { getTemplate } from './template.js';
|
|
42
|
-
import { hydrationStart, hydrationEnd } from './lifecycle.js';
|
|
43
|
-
import { getObservableNames, isAttributeProperty, syncAttrProperties } from './decorators.js';
|
|
44
|
-
import { syncRepeat, dotWalk } from './element/diff.js';
|
|
45
|
-
import { collectItemMarkers, nextElement, findByOrdinal, MARKER_COND_START, MARKER_COND_END, MARKER_REPEAT_START, } from './element/markers.js';
|
|
46
|
-
import { injectModuleStyle, } from './element/styles.js';
|
|
47
|
-
import { ATTR_KIND_BOOLEAN, ATTR_KIND_COMPLEX, ATTR_KIND_TEMPLATE, } from './element/types.js';
|
|
48
|
-
// ── Caches ──────────────────────────────────────────────────────
|
|
49
|
-
/** Parsed template cache — cloneNode(true) is faster than re-parsing. */
|
|
50
|
-
const templateCache = new WeakMap();
|
|
51
|
-
/** Parsed template DOM for SSR path mapping, keyed by TemplateBlockMeta. */
|
|
52
|
-
const templateDOMCache = new WeakMap();
|
|
53
|
-
/** Cached root tag name extracted from meta.h before it's released. */
|
|
54
|
-
const rootTagCache = new WeakMap();
|
|
55
|
-
/** Pre-computed ordinals for template nodes: childIndex → [nodeType, ordinal].
|
|
56
|
-
* Avoids re-counting element/text siblings on every $resolveSSR call. */
|
|
57
|
-
const tplOrdinalCache = new WeakMap();
|
|
58
|
-
function getTplOrdinals(tplNode) {
|
|
59
|
-
let map = tplOrdinalCache.get(tplNode);
|
|
60
|
-
if (map)
|
|
61
|
-
return map;
|
|
62
|
-
map = new Map();
|
|
63
|
-
let elemOrd = 0;
|
|
64
|
-
let textOrd = 0;
|
|
65
|
-
const children = tplNode.childNodes;
|
|
66
|
-
for (let k = 0; k < children.length; k++) {
|
|
67
|
-
const type = children[k].nodeType;
|
|
68
|
-
if (type === 1) {
|
|
69
|
-
map.set(k, [1, elemOrd]);
|
|
70
|
-
elemOrd++;
|
|
71
|
-
}
|
|
72
|
-
else if (type === 3) {
|
|
73
|
-
map.set(k, [3, textOrd]);
|
|
74
|
-
textOrd++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
tplOrdinalCache.set(tplNode, map);
|
|
78
|
-
return map;
|
|
79
|
-
}
|
|
80
|
-
// ── Sentinels ───────────────────────────────────────────────────
|
|
81
|
-
const EMPTY_ARR = [];
|
|
82
|
-
// ── Helper: snapshot child nodes into a pre-allocated array ──────
|
|
83
|
-
function childNodesArray(parent) {
|
|
84
|
-
const children = parent.childNodes;
|
|
85
|
-
const len = children.length;
|
|
86
|
-
const result = new Array(len);
|
|
87
|
-
for (let i = 0; i < len; i++)
|
|
88
|
-
result[i] = children[i];
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
// ── Helper: parse template HTML into a temp container ────────────
|
|
92
|
-
function getTemplateDom(meta) {
|
|
93
|
-
let cached = templateDOMCache.get(meta);
|
|
94
|
-
if (cached)
|
|
95
|
-
return cached;
|
|
96
|
-
const div = document.createElement('div');
|
|
97
|
-
div.innerHTML = meta.h;
|
|
98
|
-
templateDOMCache.set(meta, div);
|
|
99
|
-
return div;
|
|
100
|
-
}
|
|
101
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
102
|
-
// WebUIElement
|
|
103
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
104
|
-
export class WebUIElement extends HTMLElement {
|
|
105
|
-
constructor() {
|
|
106
|
-
super(...arguments);
|
|
107
|
-
this.$root = null;
|
|
108
|
-
this.$ready = false;
|
|
109
|
-
this.$hydrated = false;
|
|
110
|
-
this.$dirtyPaths = null;
|
|
111
|
-
this.$pendingFlush = false;
|
|
112
|
-
/** Cached condition resolver — avoids allocating a closure per evaluation. */
|
|
113
|
-
this.$resolver = (p, s) => this.$resolveValue(p, s);
|
|
1
|
+
import { TemplateElement } from './template-element.js';
|
|
2
|
+
import { getObservableNames, isAttributeProperty, syncAttrProperties, } from './decorators.js';
|
|
3
|
+
export class WebUIElement extends TemplateElement {
|
|
4
|
+
$observableNames() {
|
|
5
|
+
return getObservableNames(this.constructor);
|
|
114
6
|
}
|
|
115
|
-
|
|
116
|
-
|
|
7
|
+
$shouldApplySSRState(key) {
|
|
8
|
+
return !isAttributeProperty(this.constructor, key);
|
|
117
9
|
}
|
|
118
|
-
|
|
119
|
-
connectedCallback() {
|
|
120
|
-
const tag = this.tagName.toLowerCase();
|
|
121
|
-
if (this.$hydrated && this.$root) {
|
|
122
|
-
hydrationStart();
|
|
123
|
-
this.$ready = true;
|
|
124
|
-
this.$update();
|
|
125
|
-
hydrationEnd();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const meta = getTemplate(tag);
|
|
129
|
-
if (!meta) {
|
|
130
|
-
console.warn(`[WebUI] Template metadata for <${tag}> not found. ` +
|
|
131
|
-
`Ensure the component is included in the SSR output or registered via __webui.templates.`);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
this.$meta = meta;
|
|
135
|
-
// Custom element upgrade timing: when the HTML parser encounters the
|
|
136
|
-
// opening tag, connectedCallback fires BEFORE children are parsed.
|
|
137
|
-
// If the document is still loading, defer to let the parser finish.
|
|
138
|
-
if (document.readyState === 'loading') {
|
|
139
|
-
const handler = () => {
|
|
140
|
-
document.removeEventListener('DOMContentLoaded', handler);
|
|
141
|
-
this.$mount(meta);
|
|
142
|
-
};
|
|
143
|
-
document.addEventListener('DOMContentLoaded', handler);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
// Document is already parsed — children are available
|
|
147
|
-
this.$mount(meta);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
/** Mount the component after children are available. */
|
|
151
|
-
$mount(meta) {
|
|
152
|
-
if (this.$hydrated)
|
|
153
|
-
return;
|
|
154
|
-
hydrationStart();
|
|
155
|
-
// Auto-detect shadow vs light DOM
|
|
156
|
-
const hasShadow = !!this.shadowRoot;
|
|
157
|
-
const wantShadow = hasShadow || !!meta.sd;
|
|
158
|
-
let root;
|
|
159
|
-
let isSSR;
|
|
160
|
-
let clientRoot = null;
|
|
161
|
-
if (hasShadow) {
|
|
162
|
-
// Shadow DOM SSR — declarative shadow root already has content
|
|
163
|
-
root = this.shadowRoot;
|
|
164
|
-
isSSR = true;
|
|
165
|
-
}
|
|
166
|
-
else if (this.childNodes.length > 0 && !meta.sd) {
|
|
167
|
-
// SSR light-DOM — element already has server-rendered children.
|
|
168
|
-
// Only treat as SSR when the template does NOT explicitly declare
|
|
169
|
-
// shadow DOM (meta.sd). When meta.sd is set, existing children
|
|
170
|
-
// are slot content from an SPA partial, not SSR output.
|
|
171
|
-
root = this;
|
|
172
|
-
isSSR = true;
|
|
173
|
-
}
|
|
174
|
-
else if (wantShadow) {
|
|
175
|
-
// Shadow DOM client-created (or SPA partial with slot content).
|
|
176
|
-
// Existing children are slot content — they stay in light DOM
|
|
177
|
-
// and project through the template's <slot>.
|
|
178
|
-
root = this.attachShadow({ mode: 'open' });
|
|
179
|
-
isSSR = false;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
// Light DOM client-created — populate from template (no shadow = no link issue)
|
|
183
|
-
root = this;
|
|
184
|
-
isSSR = false;
|
|
185
|
-
}
|
|
186
|
-
// Inject CSS module stylesheet after root is determined
|
|
187
|
-
if (meta.sa)
|
|
188
|
-
injectModuleStyle(meta.sa, this.shadowRoot);
|
|
189
|
-
if (isSSR) {
|
|
190
|
-
// Apply the same state that was used for SSR rendering
|
|
191
|
-
// so client observables match the server-rendered DOM.
|
|
192
|
-
this.$applySSRState();
|
|
193
|
-
this.$root = this.$hydrate(root, meta, getTemplateDom(meta));
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
clientRoot = this.$createStagingRoot(meta);
|
|
197
|
-
this.$root = this.$wire(clientRoot, meta);
|
|
198
|
-
}
|
|
199
|
-
this.$meta = meta;
|
|
200
|
-
this.$hydrated = true;
|
|
201
|
-
this.$ready = true;
|
|
10
|
+
$syncAuthoredAttributes() {
|
|
202
11
|
syncAttrProperties(this, this.constructor);
|
|
203
|
-
// Client-created components: flush current attr/observable values
|
|
204
|
-
// into the freshly-wired template DOM. Call $updateInstance directly
|
|
205
|
-
// to avoid the $update() path-index build — it will be lazy-built
|
|
206
|
-
// on the first reactive change instead.
|
|
207
|
-
if (!isSSR && clientRoot) {
|
|
208
|
-
this.$updateInstance(this.$root);
|
|
209
|
-
if (this.$root.repeats.length !== 0 || this.$root.conds.length !== 0) {
|
|
210
|
-
this.$root.nodes = childNodesArray(clientRoot);
|
|
211
|
-
this.$releaseStagingRepeatContainers(this.$root, clientRoot);
|
|
212
|
-
}
|
|
213
|
-
this.$appendStagedChildren(root, clientRoot);
|
|
214
|
-
}
|
|
215
|
-
hydrationEnd();
|
|
216
12
|
}
|
|
217
|
-
disconnectedCallback() {
|
|
218
|
-
// Schedule teardown on microtask — if the element is re-connected
|
|
219
|
-
// before then (e.g. repeat reconciliation), skip the cleanup.
|
|
220
|
-
if (this.$root) {
|
|
221
|
-
queueMicrotask(() => {
|
|
222
|
-
if (!this.isConnected)
|
|
223
|
-
this.$destroy();
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Permanently destroy this component's own bindings and DOM references.
|
|
229
|
-
* Each component is responsible for its own cleanup — child WebUI
|
|
230
|
-
* elements handle theirs via their own `disconnectedCallback`.
|
|
231
|
-
*/
|
|
232
|
-
$destroy() {
|
|
233
|
-
if (!this.$root)
|
|
234
|
-
return;
|
|
235
|
-
this.$teardown(this.$root);
|
|
236
|
-
this.$root = null;
|
|
237
|
-
this.$pathIndex = undefined;
|
|
238
|
-
this.$wildcardBindings = undefined;
|
|
239
|
-
this.$dirtyPaths = null;
|
|
240
|
-
this.$pendingFlush = false;
|
|
241
|
-
this.$ready = false;
|
|
242
|
-
}
|
|
243
|
-
/** Break all DOM references held by a binding instance and its nested blocks. */
|
|
244
|
-
$teardown(instance) {
|
|
245
|
-
for (const c of instance.conds) {
|
|
246
|
-
if (c.instance)
|
|
247
|
-
this.$teardown(c.instance);
|
|
248
|
-
c.instance = null;
|
|
249
|
-
}
|
|
250
|
-
for (const r of instance.repeats) {
|
|
251
|
-
for (const item of r.instances)
|
|
252
|
-
this.$teardown(item.instance);
|
|
253
|
-
r.instances.length = 0;
|
|
254
|
-
r.container = null;
|
|
255
|
-
r.start = null;
|
|
256
|
-
r.end = null;
|
|
257
|
-
}
|
|
258
|
-
instance.nodes.length = 0;
|
|
259
|
-
instance.texts.length = 0;
|
|
260
|
-
instance.attrs.length = 0;
|
|
261
|
-
instance.conds.length = 0;
|
|
262
|
-
instance.repeats.length = 0;
|
|
263
|
-
}
|
|
264
|
-
/** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
|
|
265
13
|
$emit(name, detail) {
|
|
266
14
|
return this.dispatchEvent(new CustomEvent(name, {
|
|
267
15
|
bubbles: true,
|
|
@@ -270,785 +18,123 @@ export class WebUIElement extends HTMLElement {
|
|
|
270
18
|
detail,
|
|
271
19
|
}));
|
|
272
20
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
* view-transition snapshot captures it.
|
|
279
|
-
*/
|
|
280
|
-
setState(state) {
|
|
281
|
-
const names = getObservableNames(this.constructor);
|
|
282
|
-
const keys = Object.keys(state);
|
|
283
|
-
for (let i = 0; i < keys.length; i++) {
|
|
284
|
-
const key = keys[i];
|
|
285
|
-
if (names.has(key)) {
|
|
286
|
-
this[key] = state[key];
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
this.$flushUpdates();
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Apply SSR state from `window.__webui.state`.
|
|
293
|
-
*
|
|
294
|
-
* The handler emits all SSR metadata in a single consolidated
|
|
295
|
-
* `window.__webui` script block. State lives at `.state` — the same
|
|
296
|
-
* props passed to the server render so observables match the DOM.
|
|
297
|
-
* Only observable properties are set — unknown keys are ignored.
|
|
298
|
-
*
|
|
299
|
-
* Writes directly to the backing field (`_prop`) to avoid triggering
|
|
300
|
-
* reactive updates before bindings are wired.
|
|
301
|
-
*/
|
|
302
|
-
$applySSRState() {
|
|
303
|
-
const state = window.__webui?.state;
|
|
304
|
-
if (!state || typeof state !== 'object')
|
|
305
|
-
return;
|
|
306
|
-
const ctor = this.constructor;
|
|
307
|
-
const names = getObservableNames(ctor);
|
|
308
|
-
for (const key of Object.keys(state)) {
|
|
309
|
-
if (names.has(key) && !isAttributeProperty(ctor, key)) {
|
|
310
|
-
// Write to backing field directly — no reactive update yet
|
|
311
|
-
this[`_${key}`] = state[key];
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/** Reactive update — called by @observable/@attr setters. */
|
|
316
|
-
$update(path) {
|
|
317
|
-
if (!this.$ready || !this.$root)
|
|
318
|
-
return;
|
|
319
|
-
// Lazy-build path index on first update (deferred from hydration)
|
|
320
|
-
if (!this.$pathIndex)
|
|
321
|
-
this.$buildPathIndex();
|
|
322
|
-
if (path && this.$pathIndex) {
|
|
323
|
-
const entry = this.$pathIndex.get(path);
|
|
324
|
-
if (entry) {
|
|
325
|
-
// Batch path-specific updates via microtask coalescing.
|
|
326
|
-
if (!this.$dirtyPaths)
|
|
327
|
-
this.$dirtyPaths = new Set();
|
|
328
|
-
this.$dirtyPaths.add(path);
|
|
329
|
-
if (!this.$pendingFlush) {
|
|
330
|
-
this.$pendingFlush = true;
|
|
331
|
-
queueMicrotask(() => this.$flush());
|
|
332
|
-
}
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Full immediate update (initial mount, reconnect, or unknown path).
|
|
337
|
-
this.$dirtyPaths = null;
|
|
338
|
-
this.$updateInstance(this.$root);
|
|
339
|
-
}
|
|
340
|
-
/** Synchronously flush all queued path updates. Call this when you need
|
|
341
|
-
* the DOM to reflect pending property changes immediately. */
|
|
342
|
-
$flushUpdates() {
|
|
343
|
-
if (this.$pendingFlush)
|
|
344
|
-
this.$flush();
|
|
21
|
+
$finalize(instance, root, meta, resolver, scope) {
|
|
22
|
+
this.$wireEvents(instance, root, meta, resolver, scope);
|
|
23
|
+
if (meta.re)
|
|
24
|
+
this.$wireRoot(instance, meta.re);
|
|
25
|
+
this.$wireRefs(root);
|
|
345
26
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (!
|
|
349
|
-
this.$dirtyPaths = null;
|
|
350
|
-
this.$pendingFlush = false;
|
|
27
|
+
$wireEvents(instance, root, meta, resolver, scope) {
|
|
28
|
+
const groups = meta.eg;
|
|
29
|
+
if (!groups)
|
|
351
30
|
return;
|
|
31
|
+
const delegateTarget = this.shadowRoot ?? this;
|
|
32
|
+
for (let i = 0; i < groups.length; i++) {
|
|
33
|
+
const [eventName, bindings] = groups[i];
|
|
34
|
+
const bucket = this.$resolveDelegatedEvents(root, bindings, resolver, scope);
|
|
35
|
+
this.$addDelegatedEvent(instance, delegateTarget, eventName, bucket);
|
|
352
36
|
}
|
|
353
|
-
while (this.$dirtyPaths && this.$dirtyPaths.size > 0) {
|
|
354
|
-
// Snapshot and clear so re-entrant setters get a fresh set.
|
|
355
|
-
const dirty = this.$dirtyPaths;
|
|
356
|
-
this.$dirtyPaths = null;
|
|
357
|
-
for (const path of dirty) {
|
|
358
|
-
const entry = this.$pathIndex.get(path);
|
|
359
|
-
if (entry) {
|
|
360
|
-
this.$updateBindings(entry.texts, entry.attrs, entry.conds, entry.repeats);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Update wildcard bindings once per flush (not per dirty path)
|
|
364
|
-
if (this.$wildcardBindings) {
|
|
365
|
-
const wc = this.$wildcardBindings;
|
|
366
|
-
this.$updateBindings(wc.texts, wc.attrs, wc.conds, wc.repeats);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
this.$pendingFlush = false;
|
|
370
|
-
}
|
|
371
|
-
// ── DOM resolution: client-created path ───────────────────────
|
|
372
|
-
// Compiled paths are childNode indices in meta.h parsed by the browser.
|
|
373
|
-
// For client-created components the DOM matches meta.h exactly.
|
|
374
|
-
$resolve(root, path, pathStart = 0) {
|
|
375
|
-
let cur = root;
|
|
376
|
-
// When pathStart > 0, advance through the skipped segments so `cur`
|
|
377
|
-
// aligns with the already-positioned SSR root.
|
|
378
|
-
for (let i = 0; i < pathStart; i++) {
|
|
379
|
-
const child = cur.childNodes[path[i]];
|
|
380
|
-
if (!child)
|
|
381
|
-
return null;
|
|
382
|
-
cur = child;
|
|
383
|
-
}
|
|
384
|
-
for (let i = pathStart; i < path.length; i++) {
|
|
385
|
-
const child = cur.childNodes[path[i]];
|
|
386
|
-
if (!child)
|
|
387
|
-
return null;
|
|
388
|
-
cur = child;
|
|
389
|
-
}
|
|
390
|
-
return cur;
|
|
391
37
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// <!--wc--><p>no results</p><!--/wc--> ← extra content
|
|
410
|
-
// <div class="grid">...</div> ← now element ordinal 1
|
|
411
|
-
//
|
|
412
|
-
// To resolve the correct element, `$resolveSSR` walks SSR children
|
|
413
|
-
// in parallel with the template DOM, skipping everything between
|
|
414
|
-
// structural marker pairs. This requires closing markers to still
|
|
415
|
-
// be present — marker removal is deferred to the end of $hydrate().
|
|
416
|
-
//
|
|
417
|
-
// pathStart: skip leading path segments for in-place block hydration.
|
|
418
|
-
$resolveSSR(ssrRoot, tplRoot, path, pathStart = 0) {
|
|
419
|
-
let ssr = ssrRoot;
|
|
420
|
-
let tpl = tplRoot;
|
|
421
|
-
// When pathStart > 0, ssr has already descended to the block root
|
|
422
|
-
// but tpl still points at the wrapper from getTemplateDom().
|
|
423
|
-
// Advance tpl through the skipped path segments to align them.
|
|
424
|
-
for (let i = 0; i < pathStart; i++) {
|
|
425
|
-
const tplChild = tpl.childNodes[path[i]];
|
|
426
|
-
if (!tplChild)
|
|
427
|
-
return null;
|
|
428
|
-
tpl = tplChild;
|
|
429
|
-
}
|
|
430
|
-
for (let i = pathStart; i < path.length; i++) {
|
|
431
|
-
const idx = path[i];
|
|
432
|
-
const tplChild = tpl.childNodes[idx];
|
|
433
|
-
if (!tplChild)
|
|
434
|
-
return null;
|
|
435
|
-
// Look up the target's nodeType and ordinal from the template.
|
|
436
|
-
// getTplOrdinals maps childNode index → [nodeType, ordinal],
|
|
437
|
-
// counting elements and text nodes separately (comments ignored).
|
|
438
|
-
const ordinals = getTplOrdinals(tpl);
|
|
439
|
-
const entry = ordinals.get(idx);
|
|
440
|
-
if (!entry)
|
|
441
|
-
return null;
|
|
442
|
-
// Walk SSR children to find the Nth element/text node, skipping
|
|
443
|
-
// structural block content that exists in SSR but not in meta.h.
|
|
444
|
-
// See findByOrdinal() for the full algorithm and invariants.
|
|
445
|
-
const [nodeType, ordinal] = entry;
|
|
446
|
-
const child = findByOrdinal(ssr, nodeType, ordinal);
|
|
447
|
-
if (!child)
|
|
448
|
-
return null;
|
|
449
|
-
ssr = child;
|
|
450
|
-
tpl = tplChild;
|
|
38
|
+
$resolveDelegatedEvents(root, bindings, resolver, scope) {
|
|
39
|
+
const entries = [];
|
|
40
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
41
|
+
const [handlerName, args, target, usesEvent] = bindings[i];
|
|
42
|
+
const el = resolver(root, target);
|
|
43
|
+
if (!el || el.nodeType !== 1)
|
|
44
|
+
continue;
|
|
45
|
+
const method = this[handlerName];
|
|
46
|
+
if (typeof method !== 'function')
|
|
47
|
+
continue;
|
|
48
|
+
entries.push({
|
|
49
|
+
target: el,
|
|
50
|
+
method: method,
|
|
51
|
+
args,
|
|
52
|
+
usesEvent: usesEvent === 1,
|
|
53
|
+
scope,
|
|
54
|
+
});
|
|
451
55
|
}
|
|
452
|
-
return
|
|
453
|
-
}
|
|
454
|
-
// ── Template parsing ──────────────────────────────────────────
|
|
455
|
-
$parseTemplate(meta) {
|
|
456
|
-
let cached = templateCache.get(meta);
|
|
457
|
-
if (cached)
|
|
458
|
-
return cached.cloneNode(true);
|
|
459
|
-
const tpl = document.createElement('template');
|
|
460
|
-
tpl.innerHTML = meta.h;
|
|
461
|
-
templateCache.set(meta, tpl.content);
|
|
462
|
-
return tpl.content.cloneNode(true);
|
|
56
|
+
return entries;
|
|
463
57
|
}
|
|
464
|
-
$
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
customElements.upgrade(wrapper);
|
|
469
|
-
return wrapper;
|
|
470
|
-
}
|
|
471
|
-
$appendStagedChildren(root, stagingRoot) {
|
|
472
|
-
const first = stagingRoot.firstChild;
|
|
473
|
-
if (!first)
|
|
474
|
-
return;
|
|
475
|
-
if (!first.nextSibling) {
|
|
476
|
-
root.appendChild(first);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
const fragment = document.createDocumentFragment();
|
|
480
|
-
while (stagingRoot.firstChild) {
|
|
481
|
-
fragment.appendChild(stagingRoot.firstChild);
|
|
58
|
+
$wireRoot(instance, re) {
|
|
59
|
+
const target = this.shadowRoot ?? this;
|
|
60
|
+
for (let i = 0; i < re.length; i++) {
|
|
61
|
+
this.$addEvent(instance, target, re[i][0], re[i][1], re[i][2], undefined);
|
|
482
62
|
}
|
|
483
|
-
root.appendChild(fragment);
|
|
484
63
|
}
|
|
485
|
-
$
|
|
486
|
-
if (
|
|
64
|
+
$addDelegatedEvent(instance, target, eventName, entries) {
|
|
65
|
+
if (entries.length === 0)
|
|
487
66
|
return;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const stack = [instance];
|
|
491
|
-
while (stack.length > 0) {
|
|
492
|
-
const current = stack.pop();
|
|
493
|
-
if (!current)
|
|
494
|
-
continue;
|
|
495
|
-
for (let i = 0; i < current.repeats.length; i++) {
|
|
496
|
-
const repeat = current.repeats[i];
|
|
497
|
-
if (repeat.container === stagingRoot)
|
|
498
|
-
repeat.container = null;
|
|
499
|
-
for (let j = 0; j < repeat.instances.length; j++) {
|
|
500
|
-
stack.push(repeat.instances[j].instance);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
for (let i = 0; i < current.conds.length; i++) {
|
|
504
|
-
const child = current.conds[i].instance;
|
|
505
|
-
if (child)
|
|
506
|
-
stack.push(child);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
// ═══════════════════════════════════════════════════════════════
|
|
511
|
-
// Client-created wiring — exact childNode index resolution
|
|
512
|
-
// ═══════════════════════════════════════════════════════════════
|
|
513
|
-
$wire(root, meta, scope) {
|
|
514
|
-
const instance = {
|
|
515
|
-
scope, nodes: childNodesArray(root),
|
|
516
|
-
texts: [], attrs: [], conds: [], repeats: [],
|
|
67
|
+
const listener = (event) => {
|
|
68
|
+
this.$dispatchDelegatedEvent(entries, event);
|
|
517
69
|
};
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// capture target positions from the untouched DOM first.
|
|
521
|
-
// Pre-resolve text binding slots
|
|
522
|
-
const textRefs = [];
|
|
523
|
-
if (meta.tx) {
|
|
524
|
-
for (let i = 0; i < meta.tx.length; i++) {
|
|
525
|
-
const entry = meta.tx[i];
|
|
526
|
-
const [slot, parts] = entry;
|
|
527
|
-
const raw = entry[2] === 1;
|
|
528
|
-
const [parentPath, beforeIndex] = slot;
|
|
529
|
-
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
530
|
-
if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
|
|
531
|
-
continue;
|
|
532
|
-
textRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, parts, raw });
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
const condRefs = [];
|
|
536
|
-
if (meta.c) {
|
|
537
|
-
for (let i = 0; i < meta.c.length; i++) {
|
|
538
|
-
const [condition, blockIndex, slotMeta] = meta.c[i];
|
|
539
|
-
const [parentPath, beforeIndex] = slotMeta;
|
|
540
|
-
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
541
|
-
if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
|
|
542
|
-
continue;
|
|
543
|
-
condRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, condition: condition, blockIndex });
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const repRefs = [];
|
|
547
|
-
if (meta.r) {
|
|
548
|
-
for (let i = 0; i < meta.r.length; i++) {
|
|
549
|
-
const [collection, itemVar, blockIndex, slotMeta] = meta.r[i];
|
|
550
|
-
const [parentPath, beforeIndex] = slotMeta;
|
|
551
|
-
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
552
|
-
if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
|
|
553
|
-
continue;
|
|
554
|
-
repRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, collection, itemVar, blockIndex });
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
// Attribute bindings (no DOM mutation — safe to resolve inline)
|
|
558
|
-
this.$wireAttrs(instance, meta, scope, (p) => this.$resolve(root, p));
|
|
559
|
-
// Events + refs — resolve BEFORE anchors shift childNode indices.
|
|
560
|
-
// Events target element nodes (not text/comment positions), but anchor
|
|
561
|
-
// insertions still shift childNode indices for sibling elements.
|
|
562
|
-
this.$finalize(root, meta, (r, p) => this.$resolve(r, p), scope);
|
|
563
|
-
// Now insert anchors using pre-resolved references
|
|
564
|
-
// Text bindings
|
|
565
|
-
for (const t of textRefs) {
|
|
566
|
-
const anchor = document.createComment('');
|
|
567
|
-
t.parent.insertBefore(anchor, t.ref);
|
|
568
|
-
if (t.raw) {
|
|
569
|
-
// Raw binding: create a container span for innerHTML updates
|
|
570
|
-
const container = document.createElement('span');
|
|
571
|
-
t.parent.insertBefore(container, anchor);
|
|
572
|
-
const textNode = document.createTextNode('');
|
|
573
|
-
instance.texts.push({ node: textNode, parts: t.parts, scope, raw: true, rawParent: container });
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
const textNode = document.createTextNode('');
|
|
577
|
-
t.parent.insertBefore(textNode, anchor);
|
|
578
|
-
instance.texts.push({ node: textNode, parts: t.parts, scope });
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Conditional bindings
|
|
582
|
-
for (const c of condRefs) {
|
|
583
|
-
const anchor = document.createComment('');
|
|
584
|
-
c.parent.insertBefore(anchor, c.ref);
|
|
585
|
-
instance.conds.push({ condition: c.condition, blockIndex: c.blockIndex, anchor, scope, instance: null });
|
|
586
|
-
}
|
|
587
|
-
// Repeat bindings
|
|
588
|
-
for (let i = 0; i < repRefs.length; i++) {
|
|
589
|
-
const r = repRefs[i];
|
|
590
|
-
const anchor = document.createComment('');
|
|
591
|
-
r.parent.insertBefore(anchor, r.ref);
|
|
592
|
-
const { attrMap, rootBindings } = this.$repeatMaps(r.blockIndex, r.itemVar);
|
|
593
|
-
instance.repeats.push({
|
|
594
|
-
markerId: i, collection: r.collection, itemVar: r.itemVar, blockIndex: r.blockIndex,
|
|
595
|
-
container: r.parent, start: anchor, end: null,
|
|
596
|
-
scope, owner: instance, instances: [], rootTag: null,
|
|
597
|
-
attrMap, rootBindings,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
// Evaluate conditionals and repeats inline so blocks are created
|
|
601
|
-
// immediately — no deferred $update() flush needed.
|
|
602
|
-
for (let i = 0; i < instance.conds.length; i++)
|
|
603
|
-
this.$toggleCond(instance.conds[i]);
|
|
604
|
-
return instance;
|
|
70
|
+
target.addEventListener(eventName, listener);
|
|
71
|
+
this.$addCleanup(instance, () => target.removeEventListener(eventName, listener));
|
|
605
72
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
* (top-level component hydration).
|
|
614
|
-
*
|
|
615
|
-
* When pathStart=1: ssrRoot is a block element itself (repeat item
|
|
616
|
-
* in-place hydration). The leading [0] wrapper segment is skipped
|
|
617
|
-
* so compiled paths resolve directly against the element.
|
|
618
|
-
*/
|
|
619
|
-
$hydrate(ssrRoot, meta, tplDom, scope, pathStart = 0) {
|
|
620
|
-
const instance = {
|
|
621
|
-
scope,
|
|
622
|
-
nodes: pathStart > 0 ? [ssrRoot] : childNodesArray(ssrRoot),
|
|
623
|
-
texts: [], attrs: [], conds: [], repeats: [],
|
|
624
|
-
};
|
|
625
|
-
// Collect SSR markers for deferred removal. Closing markers
|
|
626
|
-
// (<!--/wc-->, <!--/wr-->) and item markers (<!--wi-->) must stay in
|
|
627
|
-
// the DOM throughout the entire hydration pass so that $resolveSSR
|
|
628
|
-
// and $findSSRText can correctly skip structural block content when
|
|
629
|
-
// counting element/text ordinals. All collected markers are removed
|
|
630
|
-
// in a single cleanup pass after $finalize() (events + refs).
|
|
631
|
-
//
|
|
632
|
-
// Hydration order: text → attrs → conditionals → repeats → events
|
|
633
|
-
// All phases use $resolveSSR, so markers must survive until the end.
|
|
634
|
-
const staleMarkers = [];
|
|
635
|
-
// Text bindings — find existing text nodes rendered by the server
|
|
636
|
-
if (meta.tx) {
|
|
637
|
-
for (let i = 0; i < meta.tx.length; i++) {
|
|
638
|
-
const entry = meta.tx[i];
|
|
639
|
-
const [slot, parts] = entry;
|
|
640
|
-
const raw = entry[2] === 1;
|
|
641
|
-
const [parentPath, beforeIndex] = slot;
|
|
642
|
-
const ssrParent = this.$resolveSSR(ssrRoot, tplDom, parentPath, pathStart);
|
|
643
|
-
if (!ssrParent)
|
|
644
|
-
continue;
|
|
645
|
-
const tplParent = this.$resolve(tplDom, parentPath, pathStart);
|
|
646
|
-
if (!tplParent)
|
|
647
|
-
continue;
|
|
648
|
-
if (raw) {
|
|
649
|
-
const rawParent = ssrParent;
|
|
650
|
-
const textNode = document.createTextNode('');
|
|
651
|
-
instance.texts.push({ node: textNode, parts, scope, raw: true, rawParent });
|
|
73
|
+
$dispatchDelegatedEvent(entries, event) {
|
|
74
|
+
let current = event.target;
|
|
75
|
+
while (current) {
|
|
76
|
+
for (let i = 0; i < entries.length; i++) {
|
|
77
|
+
const entry = entries[i];
|
|
78
|
+
if (entry.target === current) {
|
|
79
|
+
this.$callEventHandler(entry.method, entry.args, event, entry.scope, entry.target, entry.usesEvent);
|
|
652
80
|
}
|
|
653
|
-
else {
|
|
654
|
-
let textNode = this.$findSSRText(ssrParent, tplParent, beforeIndex);
|
|
655
|
-
if (!textNode) {
|
|
656
|
-
textNode = document.createTextNode('');
|
|
657
|
-
const insertRef = this.$findSSRSlotRef(ssrParent, tplParent, beforeIndex);
|
|
658
|
-
ssrParent.insertBefore(textNode, insertRef);
|
|
659
|
-
}
|
|
660
|
-
if (textNode)
|
|
661
|
-
instance.texts.push({ node: textNode, parts, scope });
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
// Attribute bindings
|
|
666
|
-
this.$wireAttrs(instance, meta, scope, (p) => this.$resolveSSR(ssrRoot, tplDom, p, pathStart));
|
|
667
|
-
// Conditional bindings — use <!--wc--> markers as anchors
|
|
668
|
-
if (meta.c) {
|
|
669
|
-
let lastCondMarker = null;
|
|
670
|
-
let lastCondParent = null;
|
|
671
|
-
for (let i = 0; i < meta.c.length; i++) {
|
|
672
|
-
const [condition, blockIndex, slotMeta] = meta.c[i];
|
|
673
|
-
const [parentPath] = slotMeta;
|
|
674
|
-
const ssrParent = this.$resolveSSR(ssrRoot, tplDom, parentPath, pathStart) ?? ssrRoot;
|
|
675
|
-
const blockMeta = this.$block(blockIndex);
|
|
676
|
-
const shown = condition[0](this.$resolver, scope);
|
|
677
|
-
let condInstance = null;
|
|
678
|
-
// Reset cursor when parent changes between iterations
|
|
679
|
-
if (ssrParent !== lastCondParent) {
|
|
680
|
-
lastCondMarker = null;
|
|
681
|
-
lastCondParent = ssrParent;
|
|
682
|
-
}
|
|
683
|
-
// Find the next <!--wc--> marker in ssrParent (after any previously found one)
|
|
684
|
-
const marker = this.$findMarker(ssrParent, MARKER_COND_START, lastCondMarker);
|
|
685
|
-
let condAnchor;
|
|
686
|
-
if (marker) {
|
|
687
|
-
condAnchor = marker;
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
// No marker — insert anchor at the slot position
|
|
691
|
-
condAnchor = document.createComment('');
|
|
692
|
-
const [, beforeIndex] = slotMeta;
|
|
693
|
-
const insertRef = ssrParent.childNodes[beforeIndex ?? ssrParent.childNodes.length] ?? null;
|
|
694
|
-
ssrParent.insertBefore(condAnchor, insertRef);
|
|
695
|
-
}
|
|
696
|
-
if (marker)
|
|
697
|
-
lastCondMarker = marker;
|
|
698
|
-
// SSR hydration: if the marker exists, the server rendered this
|
|
699
|
-
// conditional — hydrate its content regardless of the current
|
|
700
|
-
// condition value. Complex properties from parent bindings may
|
|
701
|
-
// not have arrived yet (the parent hydrates after children), so
|
|
702
|
-
// the condition could evaluate to false even though the server
|
|
703
|
-
// rendered it as true. Trust the SSR DOM and wire it up; the
|
|
704
|
-
// condition will re-evaluate correctly once all data is set.
|
|
705
|
-
const ssrContentPresent = marker && blockMeta && this.$hasContentAfterMarker(condAnchor, MARKER_COND_END);
|
|
706
|
-
if (blockMeta && (shown || ssrContentPresent)) {
|
|
707
|
-
if (marker) {
|
|
708
|
-
condInstance = this.$hydrateCondContent(condAnchor, blockMeta, scope);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
// Collect <!--/wc--> end marker for deferred removal.
|
|
712
|
-
// Do NOT remove here — later phases (repeats, events) still need
|
|
713
|
-
// intact marker pairs for $resolveSSR structural-block skipping.
|
|
714
|
-
if (marker) {
|
|
715
|
-
const lastNode = condInstance ? condInstance.nodes[condInstance.nodes.length - 1] : condAnchor;
|
|
716
|
-
const endMarker = lastNode?.nextSibling;
|
|
717
|
-
if (endMarker && endMarker.nodeType === 8 && endMarker.data === MARKER_COND_END) {
|
|
718
|
-
staleMarkers.push(endMarker);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
instance.conds.push({
|
|
722
|
-
condition: condition, blockIndex,
|
|
723
|
-
anchor: condAnchor,
|
|
724
|
-
scope, instance: condInstance,
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
// Repeat bindings — use <!--wr--> markers as anchors, <!--wi--> for items
|
|
729
|
-
if (meta.r) {
|
|
730
|
-
let lastRepMarker = null;
|
|
731
|
-
let lastRepParent = null;
|
|
732
|
-
for (let i = 0; i < meta.r.length; i++) {
|
|
733
|
-
const [collection, itemVar, blockIndex, slotMeta] = meta.r[i];
|
|
734
|
-
const [parentPath] = slotMeta;
|
|
735
|
-
const ssrParent = this.$resolveSSR(ssrRoot, tplDom, parentPath, pathStart) ?? ssrRoot;
|
|
736
|
-
// Reset cursor when parent changes between iterations
|
|
737
|
-
if (ssrParent !== lastRepParent) {
|
|
738
|
-
lastRepMarker = null;
|
|
739
|
-
lastRepParent = ssrParent;
|
|
740
|
-
}
|
|
741
|
-
const blockMeta = this.$block(blockIndex);
|
|
742
|
-
const { attrMap, rootBindings } = this.$repeatMaps(blockIndex, itemVar);
|
|
743
|
-
const blockTplDom = blockMeta ? getTemplateDom(blockMeta) : null;
|
|
744
|
-
const rootTag = blockMeta && blockTplDom?.childNodes.length === 1 && blockTplDom.children.length === 1
|
|
745
|
-
? this.$rootTag(blockMeta)
|
|
746
|
-
: null;
|
|
747
|
-
const keyPath = Object.values(attrMap)[0];
|
|
748
|
-
// Find the next <!--wr--> marker in ssrParent (after any previously found one)
|
|
749
|
-
const marker = this.$findMarker(ssrParent, MARKER_REPEAT_START, lastRepMarker);
|
|
750
|
-
let anchor;
|
|
751
|
-
if (marker) {
|
|
752
|
-
anchor = marker;
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
// No marker — insert anchor at the slot position for client-created content
|
|
756
|
-
anchor = document.createComment('');
|
|
757
|
-
const [, beforeIndex] = slotMeta;
|
|
758
|
-
const tplParent = this.$resolve(tplDom, parentPath, pathStart);
|
|
759
|
-
const staticCount = tplParent ? tplParent.childNodes.length : 0;
|
|
760
|
-
const insertRef = ssrParent.childNodes[Math.min(beforeIndex ?? staticCount, ssrParent.childNodes.length)] ?? null;
|
|
761
|
-
ssrParent.insertBefore(anchor, insertRef);
|
|
762
|
-
}
|
|
763
|
-
lastRepMarker = anchor;
|
|
764
|
-
const repeatInsts = [];
|
|
765
|
-
const itemsArr = this.$resolveValue(collection, scope);
|
|
766
|
-
const items = Array.isArray(itemsArr) ? itemsArr : [];
|
|
767
|
-
// Collect SSR markers — single walk captures items + end boundary
|
|
768
|
-
const { items: itemMarkers, end: endMarker } = marker
|
|
769
|
-
? collectItemMarkers(anchor)
|
|
770
|
-
: { items: [], end: null };
|
|
771
|
-
if (blockMeta && blockTplDom && items.length > 0 && anchor.parentNode && itemMarkers.length > 0) {
|
|
772
|
-
if (itemMarkers.length !== items.length) {
|
|
773
|
-
console.warn(`[webui] hydration: repeat marker count (${itemMarkers.length}) ≠ data length (${items.length}) for "${collection}"`);
|
|
774
|
-
}
|
|
775
|
-
const firstKey = Object.keys(attrMap)[0];
|
|
776
|
-
const limit = Math.min(itemMarkers.length, items.length);
|
|
777
|
-
for (let j = 0; j < limit; j++) {
|
|
778
|
-
const itemValue = items[j];
|
|
779
|
-
const itemScope = { name: itemVar, value: itemValue, parent: scope };
|
|
780
|
-
if (rootTag) {
|
|
781
|
-
const itemEl = nextElement(itemMarkers[j]);
|
|
782
|
-
if (itemEl) {
|
|
783
|
-
const key = firstKey !== undefined
|
|
784
|
-
? itemEl.getAttribute(firstKey)
|
|
785
|
-
: String(j);
|
|
786
|
-
const childInstance = this.$hydrate(itemEl, blockMeta, blockTplDom, itemScope, 1);
|
|
787
|
-
repeatInsts.push({ key, value: itemValue, instance: childInstance });
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
else {
|
|
791
|
-
const itemParent = itemMarkers[j].parentNode;
|
|
792
|
-
const nextBound = j + 1 < itemMarkers.length ? itemMarkers[j + 1] : endMarker;
|
|
793
|
-
const wrapper = document.createElement('div');
|
|
794
|
-
let cursor = itemMarkers[j].nextSibling;
|
|
795
|
-
while (cursor && cursor !== nextBound) {
|
|
796
|
-
const next = cursor.nextSibling;
|
|
797
|
-
wrapper.appendChild(cursor);
|
|
798
|
-
cursor = next;
|
|
799
|
-
}
|
|
800
|
-
const inst = this.$hydrate(wrapper, blockMeta, blockTplDom, itemScope);
|
|
801
|
-
inst.nodes = childNodesArray(wrapper);
|
|
802
|
-
let afterNode = itemMarkers[j];
|
|
803
|
-
for (let nodeIndex = 0; nodeIndex < inst.nodes.length; nodeIndex++) {
|
|
804
|
-
const node = inst.nodes[nodeIndex];
|
|
805
|
-
itemParent?.insertBefore(node, afterNode.nextSibling);
|
|
806
|
-
afterNode = node;
|
|
807
|
-
}
|
|
808
|
-
const key = keyPath ? String(dotWalk(itemValue, keyPath, 0) ?? '') : null;
|
|
809
|
-
repeatInsts.push({ key, value: itemValue, instance: inst });
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
// Defer <!--wi--> item marker removal (anchor <!--wr--> stays
|
|
813
|
-
// as the runtime repeat anchor; <!--/wr--> collected below).
|
|
814
|
-
for (let m = 0; m < itemMarkers.length; m++) {
|
|
815
|
-
staleMarkers.push(itemMarkers[m]);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
// Defer <!--/wr--> end marker removal (including empty repeats).
|
|
819
|
-
if (endMarker)
|
|
820
|
-
staleMarkers.push(endMarker);
|
|
821
|
-
instance.repeats.push({
|
|
822
|
-
markerId: i, collection, itemVar, blockIndex,
|
|
823
|
-
container: (anchor.parentNode ?? ssrRoot),
|
|
824
|
-
start: anchor, end: null,
|
|
825
|
-
scope, owner: instance, instances: repeatInsts, rootTag,
|
|
826
|
-
attrMap, rootBindings, synced: true,
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
// Events + refs — this is the last phase that uses $resolveSSR.
|
|
831
|
-
this.$finalize(ssrRoot, meta, (r, p) => this.$resolveSSR(r, tplDom, p, pathStart), scope);
|
|
832
|
-
// All path-based resolution is complete. Remove the SSR markers that
|
|
833
|
-
// were kept alive for structural-block skipping. Start markers
|
|
834
|
-
// (<!--wc-->, <!--wr-->) are intentionally NOT collected — they
|
|
835
|
-
// remain as runtime anchors for conditional/repeat toggling.
|
|
836
|
-
for (let i = 0; i < staleMarkers.length; i++) {
|
|
837
|
-
staleMarkers[i].parentNode?.removeChild(staleMarkers[i]);
|
|
838
|
-
}
|
|
839
|
-
return instance;
|
|
840
|
-
}
|
|
841
|
-
// ── SSR helpers ───────────────────────────────────────────────
|
|
842
|
-
/** Collect sibling nodes between a start marker and an end marker comment. */
|
|
843
|
-
$collectBetween(start, endData) {
|
|
844
|
-
const nodes = [];
|
|
845
|
-
let node = start.nextSibling;
|
|
846
|
-
while (node) {
|
|
847
|
-
if (node.nodeType === 8 && node.data === endData)
|
|
848
|
-
break;
|
|
849
|
-
nodes.push(node);
|
|
850
|
-
node = node.nextSibling;
|
|
851
|
-
}
|
|
852
|
-
return nodes;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Hydrate a conditional block's content — shared by top-level and
|
|
856
|
-
* repeat-item conditional hydration paths.
|
|
857
|
-
*/
|
|
858
|
-
$hydrateCondContent(condAnchor, blockMeta, scope) {
|
|
859
|
-
const rootTag = this.$rootTag(blockMeta);
|
|
860
|
-
const tplDom = getTemplateDom(blockMeta);
|
|
861
|
-
if (rootTag && tplDom.children.length === 1) {
|
|
862
|
-
// Single-root optimisation: hydrate the element in-place (pathStart=1).
|
|
863
|
-
const el = nextElement(condAnchor);
|
|
864
|
-
if (el) {
|
|
865
|
-
// Wire bindings only — do NOT call $updateInstance. SSR text
|
|
866
|
-
// nodes already contain correct values; evaluating bindings now
|
|
867
|
-
// would overwrite them with stale data (e.g. a complex property
|
|
868
|
-
// from a parent that hasn't hydrated yet). This is consistent
|
|
869
|
-
// with $mount which also skips $updateInstance for SSR roots.
|
|
870
|
-
return this.$hydrate(el, blockMeta, tplDom, scope, 1);
|
|
871
|
-
}
|
|
872
|
-
return null;
|
|
873
|
-
}
|
|
874
|
-
// Multi-root or text-only conditional: collect nodes between <!--wc--> and <!--/wc-->
|
|
875
|
-
const condNodes = this.$collectBetween(condAnchor, MARKER_COND_END);
|
|
876
|
-
if (condNodes.length === 0)
|
|
877
|
-
return null;
|
|
878
|
-
const wrapper = document.createElement('div');
|
|
879
|
-
for (let cn = 0; cn < condNodes.length; cn++)
|
|
880
|
-
wrapper.appendChild(condNodes[cn]);
|
|
881
|
-
const inst = this.$hydrate(wrapper, blockMeta, tplDom, scope);
|
|
882
|
-
inst.nodes = childNodesArray(wrapper);
|
|
883
|
-
let afterNode = condAnchor;
|
|
884
|
-
for (let cn = 0; cn < inst.nodes.length; cn++) {
|
|
885
|
-
condAnchor.parentNode?.insertBefore(inst.nodes[cn], afterNode.nextSibling);
|
|
886
|
-
afterNode = inst.nodes[cn];
|
|
887
|
-
}
|
|
888
|
-
// Same as above — trust SSR DOM, skip binding evaluation.
|
|
889
|
-
return inst;
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Find the next marker comment with the given data among a parent's children.
|
|
893
|
-
* Starts searching from `after` (exclusive) if provided, or from firstChild.
|
|
894
|
-
*/
|
|
895
|
-
$findMarker(parent, data, after) {
|
|
896
|
-
let child = after ? after.nextSibling : parent.firstChild;
|
|
897
|
-
while (child) {
|
|
898
|
-
if (child.nodeType === 8 && child.data === data) {
|
|
899
|
-
return child;
|
|
900
|
-
}
|
|
901
|
-
child = child.nextSibling;
|
|
902
|
-
}
|
|
903
|
-
return null;
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* Check whether there is non-marker content between a conditional
|
|
907
|
-
* start anchor and its closing marker. Used during SSR hydration to
|
|
908
|
-
* detect server-rendered conditional content even when the runtime
|
|
909
|
-
* condition value has not been set yet (e.g. complex property from a
|
|
910
|
-
* parent repeat binding that hydrates after its children).
|
|
911
|
-
*/
|
|
912
|
-
$hasContentAfterMarker(anchor, endData) {
|
|
913
|
-
let sibling = anchor.nextSibling;
|
|
914
|
-
while (sibling) {
|
|
915
|
-
if (sibling.nodeType === 8 && sibling.data === endData) {
|
|
916
|
-
return false; // reached end marker with no content in between
|
|
917
|
-
}
|
|
918
|
-
return true; // any non-end-marker node = content present
|
|
919
|
-
}
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Find existing SSR text node by mapping template text-node ordinal.
|
|
924
|
-
*
|
|
925
|
-
* Similar to `$resolveSSR`, the SSR DOM may contain extra text nodes
|
|
926
|
-
* inside structural blocks (`<if>`/`<for>`) that are not in the
|
|
927
|
-
* compiled template. We skip `<!--wc-->...<!--/wc-->` and
|
|
928
|
-
* `<!--wr-->...<!--/wr-->` ranges to keep text ordinals aligned.
|
|
929
|
-
*/
|
|
930
|
-
$findSSRText(ssrParent, tplParent, beforeIndex) {
|
|
931
|
-
// Count how many text nodes precede `beforeIndex` in the template
|
|
932
|
-
const ordinals = getTplOrdinals(tplParent);
|
|
933
|
-
let textOrd = 0;
|
|
934
|
-
for (let k = 0; k < beforeIndex; k++) {
|
|
935
|
-
const entry = ordinals.get(k);
|
|
936
|
-
if (entry && entry[0] === 3)
|
|
937
|
-
textOrd++;
|
|
938
|
-
}
|
|
939
|
-
// Find the matching text node in SSR DOM, skipping structural block
|
|
940
|
-
// content — same algorithm as $resolveSSR (see findByOrdinal).
|
|
941
|
-
const found = findByOrdinal(ssrParent, 3 /* TEXT_NODE */, textOrd);
|
|
942
|
-
if (found)
|
|
943
|
-
return found;
|
|
944
|
-
// Fallback: any text node with content
|
|
945
|
-
let child = ssrParent.firstChild;
|
|
946
|
-
while (child) {
|
|
947
|
-
if (child.nodeType === 3 && child.data && child.data.trim()) {
|
|
948
|
-
return child;
|
|
949
81
|
}
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
return null;
|
|
953
|
-
}
|
|
954
|
-
/** Find the SSR insertion reference for an empty text slot. */
|
|
955
|
-
$findSSRSlotRef(ssrParent, tplParent, beforeIndex) {
|
|
956
|
-
const ordinals = getTplOrdinals(tplParent);
|
|
957
|
-
const children = tplParent.childNodes;
|
|
958
|
-
for (let i = beforeIndex; i < children.length; i++) {
|
|
959
|
-
const entry = ordinals.get(i);
|
|
960
|
-
if (!entry)
|
|
961
|
-
continue;
|
|
962
|
-
return findByOrdinal(ssrParent, entry[0], entry[1]);
|
|
82
|
+
current = current.parentNode;
|
|
963
83
|
}
|
|
964
|
-
return null;
|
|
965
84
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
if (cached !== undefined)
|
|
970
|
-
return cached;
|
|
971
|
-
const h = meta.h;
|
|
972
|
-
if (!h || h.charCodeAt(0) !== 60) {
|
|
973
|
-
rootTagCache.set(meta, null);
|
|
974
|
-
return null;
|
|
975
|
-
}
|
|
976
|
-
let end = 1;
|
|
977
|
-
while (end < h.length) {
|
|
978
|
-
const c = h.charCodeAt(end);
|
|
979
|
-
if (c === 32 || c === 62 || c === 47)
|
|
980
|
-
break;
|
|
981
|
-
end++;
|
|
982
|
-
}
|
|
983
|
-
const tag = h.slice(1, end).toLowerCase();
|
|
984
|
-
rootTagCache.set(meta, tag);
|
|
985
|
-
return tag;
|
|
986
|
-
}
|
|
987
|
-
// ═══════════════════════════════════════════════════════════════
|
|
988
|
-
// Shared: binding wiring, event wiring, refs
|
|
989
|
-
// ═══════════════════════════════════════════════════════════════
|
|
990
|
-
/** Wire attribute bindings using a resolver (shared by $wire and $hydrate). */
|
|
991
|
-
$wireAttrs(instance, meta, scope, resolve) {
|
|
992
|
-
if (!meta.a || !meta.ag)
|
|
85
|
+
$addEvent(instance, target, eventName, handlerName, args, scope) {
|
|
86
|
+
const method = this[handlerName];
|
|
87
|
+
if (typeof method !== 'function')
|
|
993
88
|
return;
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (!el || el.nodeType !== 1)
|
|
998
|
-
continue;
|
|
999
|
-
for (let j = 0; j < count; j++) {
|
|
1000
|
-
const entry = meta.a[start + j];
|
|
1001
|
-
if (entry)
|
|
1002
|
-
instance.attrs.push(this.$makeAttr(el, entry, scope));
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
89
|
+
const listener = (event) => this.$callEventHandler(method, args, event, scope);
|
|
90
|
+
target.addEventListener(eventName, listener);
|
|
91
|
+
this.$addCleanup(instance, () => target.removeEventListener(eventName, listener));
|
|
1005
92
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
this.$wireEvents(root, meta, resolver, scope);
|
|
1009
|
-
if (meta.re)
|
|
1010
|
-
this.$wireRoot(meta.re);
|
|
1011
|
-
this.$wireRefs(root);
|
|
93
|
+
$addCleanup(instance, cleanup) {
|
|
94
|
+
(instance.cleanups ??= []).push(cleanup);
|
|
1012
95
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
96
|
+
$callEventHandler(method, args, event, scope, currentTarget, usesEvent = false) {
|
|
97
|
+
if (currentTarget && usesEvent) {
|
|
98
|
+
this.$callEventHandlerWithCurrentTarget(method, args, event, scope, currentTarget);
|
|
1016
99
|
return;
|
|
1017
|
-
for (let i = 0; i < meta.e.length; i++) {
|
|
1018
|
-
const [eventName, handlerName, args, target] = meta.e[i];
|
|
1019
|
-
const el = resolver(root, target);
|
|
1020
|
-
if (!el || el.nodeType !== 1)
|
|
1021
|
-
continue;
|
|
1022
|
-
this.$addEvent(el, eventName, handlerName, args, scope);
|
|
1023
100
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
101
|
+
switch (args.length) {
|
|
102
|
+
case 0:
|
|
103
|
+
method.call(this);
|
|
104
|
+
return;
|
|
105
|
+
case 1:
|
|
106
|
+
method.call(this, this.$resolveEventArg(args[0], event, scope));
|
|
107
|
+
return;
|
|
108
|
+
case 2:
|
|
109
|
+
method.call(this, this.$resolveEventArg(args[0], event, scope), this.$resolveEventArg(args[1], event, scope));
|
|
110
|
+
return;
|
|
111
|
+
case 3:
|
|
112
|
+
method.call(this, this.$resolveEventArg(args[0], event, scope), this.$resolveEventArg(args[1], event, scope), this.$resolveEventArg(args[2], event, scope));
|
|
113
|
+
return;
|
|
114
|
+
case 4:
|
|
115
|
+
method.call(this, this.$resolveEventArg(args[0], event, scope), this.$resolveEventArg(args[1], event, scope), this.$resolveEventArg(args[2], event, scope), this.$resolveEventArg(args[3], event, scope));
|
|
116
|
+
return;
|
|
117
|
+
default:
|
|
118
|
+
method.apply(this, this.$resolveEventArgs(args, event, scope));
|
|
1030
119
|
}
|
|
1031
120
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
});
|
|
1041
|
-
return;
|
|
121
|
+
$callEventHandlerWithCurrentTarget(method, args, event, scope, currentTarget) {
|
|
122
|
+
const descriptor = Object.getOwnPropertyDescriptor(event, 'currentTarget');
|
|
123
|
+
Object.defineProperty(event, 'currentTarget', {
|
|
124
|
+
configurable: true,
|
|
125
|
+
value: currentTarget,
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
this.$callEventHandler(method, args, event, scope);
|
|
1042
129
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
130
|
+
finally {
|
|
131
|
+
if (descriptor) {
|
|
132
|
+
Object.defineProperty(event, 'currentTarget', descriptor);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
delete event.currentTarget;
|
|
136
|
+
}
|
|
1048
137
|
}
|
|
1049
|
-
target.addEventListener(eventName, (event) => {
|
|
1050
|
-
method.apply(this, this.$resolveEventArgs(args, event, scope));
|
|
1051
|
-
});
|
|
1052
138
|
}
|
|
1053
139
|
$resolveEventArgs(args, event, scope) {
|
|
1054
140
|
const resolved = [];
|
|
@@ -1067,7 +153,6 @@ export class WebUIElement extends HTMLElement {
|
|
|
1067
153
|
case 'z': return null;
|
|
1068
154
|
}
|
|
1069
155
|
}
|
|
1070
|
-
/** Find w-ref attributes and assign to component properties. */
|
|
1071
156
|
$wireRefs(root) {
|
|
1072
157
|
if (root.nodeType !== 1 && root.nodeType !== 11)
|
|
1073
158
|
return;
|
|
@@ -1081,343 +166,4 @@ export class WebUIElement extends HTMLElement {
|
|
|
1081
166
|
this[name] = refs[i];
|
|
1082
167
|
}
|
|
1083
168
|
}
|
|
1084
|
-
/** Create an AttrBinding from compiled metadata. */
|
|
1085
|
-
$makeAttr(el, entry, scope) {
|
|
1086
|
-
const name = entry[0];
|
|
1087
|
-
const kind = entry[1];
|
|
1088
|
-
if (kind === ATTR_KIND_BOOLEAN)
|
|
1089
|
-
return { element: el, name, kind, condition: entry[2], scope };
|
|
1090
|
-
if (kind === ATTR_KIND_TEMPLATE)
|
|
1091
|
-
return { element: el, name, kind, parts: entry[2], scope };
|
|
1092
|
-
return { element: el, name, kind: kind, path: entry[2] || '', scope };
|
|
1093
|
-
}
|
|
1094
|
-
/** Build attrMap and rootBindings for a repeat block. */
|
|
1095
|
-
$repeatMaps(blockIndex, itemVar) {
|
|
1096
|
-
const attrMap = {};
|
|
1097
|
-
const rootBindings = [];
|
|
1098
|
-
const bm = this.$block(blockIndex);
|
|
1099
|
-
if (bm?.a && bm.ag) {
|
|
1100
|
-
for (let g = 0; g < bm.ag.length; g++) {
|
|
1101
|
-
const [tp, s, c] = bm.ag[g];
|
|
1102
|
-
// tp.length === 0 means root of the block container;
|
|
1103
|
-
// tp = [0] means the first (and typically only) child element,
|
|
1104
|
-
// which IS the repeated root element in blocks like <todo-item>.
|
|
1105
|
-
const isRoot = tp.length === 0 || (tp.length === 1 && tp[0] === 0);
|
|
1106
|
-
if (isRoot) {
|
|
1107
|
-
for (let j = 0; j < c; j++) {
|
|
1108
|
-
const entry = bm.a[s + j];
|
|
1109
|
-
if (entry) {
|
|
1110
|
-
rootBindings.push(entry);
|
|
1111
|
-
if (entry[1] === 0 || entry[1] === 3) {
|
|
1112
|
-
const dp = this.$singleDynamic(entry[1] === 3 ? entry[2] : [[entry[2]]]);
|
|
1113
|
-
// Only use item-scoped paths as keys; outer-scope bindings
|
|
1114
|
-
// (e.g. group.name inside <for each="opt in ...">) would
|
|
1115
|
-
// resolve to the same value for every item and break keying.
|
|
1116
|
-
if (dp && dp.path.startsWith(itemVar + '.')) {
|
|
1117
|
-
attrMap[entry[0]] = dp.path.slice(itemVar.length + 1);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
return { attrMap, rootBindings };
|
|
1126
|
-
}
|
|
1127
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1128
|
-
// Reactive update system
|
|
1129
|
-
// ═══════════════════════════════════════════════════════════════
|
|
1130
|
-
$buildPathIndex() {
|
|
1131
|
-
if (!this.$root)
|
|
1132
|
-
return;
|
|
1133
|
-
const observableNames = getObservableNames(this.constructor);
|
|
1134
|
-
const index = new Map();
|
|
1135
|
-
const ensure = (key) => {
|
|
1136
|
-
let e = index.get(key);
|
|
1137
|
-
if (!e) {
|
|
1138
|
-
e = { texts: [], attrs: [], conds: [], repeats: [] };
|
|
1139
|
-
index.set(key, e);
|
|
1140
|
-
}
|
|
1141
|
-
return e;
|
|
1142
|
-
};
|
|
1143
|
-
const keyFor = (path) => {
|
|
1144
|
-
const dot = path.indexOf('.');
|
|
1145
|
-
const root = dot > -1 ? path.slice(0, dot) : path;
|
|
1146
|
-
return observableNames.has(root) ? root : '*';
|
|
1147
|
-
};
|
|
1148
|
-
const isLocalPath = (path, scope) => {
|
|
1149
|
-
const dot = path.indexOf('.');
|
|
1150
|
-
const root = dot > -1 ? path.slice(0, dot) : path;
|
|
1151
|
-
let current = scope;
|
|
1152
|
-
while (current) {
|
|
1153
|
-
if (current.name === root)
|
|
1154
|
-
return true;
|
|
1155
|
-
current = current.parent;
|
|
1156
|
-
}
|
|
1157
|
-
return false;
|
|
1158
|
-
};
|
|
1159
|
-
const visit = (instance) => {
|
|
1160
|
-
for (const t of instance.texts) {
|
|
1161
|
-
if (t.parts) {
|
|
1162
|
-
for (const p of t.parts) {
|
|
1163
|
-
if (typeof p !== 'string' && !isLocalPath(p[0], t.scope)) {
|
|
1164
|
-
ensure(keyFor(p[0])).texts.push(t);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
else if (t.path && !isLocalPath(t.path, t.scope)) {
|
|
1169
|
-
ensure(keyFor(t.path)).texts.push(t);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
for (const a of instance.attrs) {
|
|
1173
|
-
if (a.path && !isLocalPath(a.path, a.scope)) {
|
|
1174
|
-
ensure(keyFor(a.path)).attrs.push(a);
|
|
1175
|
-
}
|
|
1176
|
-
if (a.parts) {
|
|
1177
|
-
for (const p of a.parts) {
|
|
1178
|
-
if (typeof p !== 'string' && !isLocalPath(p[0], a.scope)) {
|
|
1179
|
-
ensure(keyFor(p[0])).attrs.push(a);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
if (a.condition) {
|
|
1184
|
-
for (const p of a.condition[1]) {
|
|
1185
|
-
if (!isLocalPath(p, a.scope))
|
|
1186
|
-
ensure(keyFor(p)).attrs.push(a);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
for (const c of instance.conds) {
|
|
1191
|
-
for (const p of c.condition[1]) {
|
|
1192
|
-
if (!isLocalPath(p, c.scope))
|
|
1193
|
-
ensure(keyFor(p)).conds.push(c);
|
|
1194
|
-
}
|
|
1195
|
-
if (c.instance)
|
|
1196
|
-
visit(c.instance);
|
|
1197
|
-
}
|
|
1198
|
-
for (const rep of instance.repeats) {
|
|
1199
|
-
if (!isLocalPath(rep.collection, rep.scope)) {
|
|
1200
|
-
ensure(keyFor(rep.collection)).repeats.push(rep);
|
|
1201
|
-
}
|
|
1202
|
-
for (let i = 0; i < rep.instances.length; i++) {
|
|
1203
|
-
visit(rep.instances[i].instance);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
};
|
|
1207
|
-
visit(this.$root);
|
|
1208
|
-
// Store wildcard bindings separately — avoids duplicating them into every path
|
|
1209
|
-
const wc = index.get('*');
|
|
1210
|
-
if (wc) {
|
|
1211
|
-
index.delete('*');
|
|
1212
|
-
this.$wildcardBindings = wc;
|
|
1213
|
-
}
|
|
1214
|
-
else {
|
|
1215
|
-
this.$wildcardBindings = null;
|
|
1216
|
-
}
|
|
1217
|
-
this.$pathIndex = index;
|
|
1218
|
-
}
|
|
1219
|
-
$updateBindings(texts, attrs, conds, repeats) {
|
|
1220
|
-
for (let i = 0; i < texts.length; i++)
|
|
1221
|
-
this.$patchText(texts[i]);
|
|
1222
|
-
for (let i = 0; i < attrs.length; i++)
|
|
1223
|
-
this.$patchAttr(attrs[i]);
|
|
1224
|
-
for (let i = 0; i < conds.length; i++)
|
|
1225
|
-
this.$toggleCond(conds[i]);
|
|
1226
|
-
for (let i = 0; i < repeats.length; i++) {
|
|
1227
|
-
syncRepeat(this, repeats[i]);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
$updateInstance(instance) {
|
|
1231
|
-
this.$updateBindings(instance.texts, instance.attrs, instance.conds, instance.repeats);
|
|
1232
|
-
}
|
|
1233
|
-
$patchText(b) {
|
|
1234
|
-
let val;
|
|
1235
|
-
if (b.parts) {
|
|
1236
|
-
val = this.$resolveParts(b.parts, b.scope);
|
|
1237
|
-
}
|
|
1238
|
-
else if (b.path) {
|
|
1239
|
-
const raw = this.$resolveValue(b.path, b.scope);
|
|
1240
|
-
val = raw == null ? '' : String(raw);
|
|
1241
|
-
}
|
|
1242
|
-
else {
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
if (b.raw && b.rawParent) {
|
|
1246
|
-
// Raw binding: render unescaped HTML via innerHTML
|
|
1247
|
-
if (b.rawParent.innerHTML !== val)
|
|
1248
|
-
b.rawParent.innerHTML = val;
|
|
1249
|
-
}
|
|
1250
|
-
else {
|
|
1251
|
-
if (b.node.data !== val)
|
|
1252
|
-
b.node.data = val;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
$patchAttr(b) {
|
|
1256
|
-
const el = b.element;
|
|
1257
|
-
switch (b.kind) {
|
|
1258
|
-
case ATTR_KIND_COMPLEX: {
|
|
1259
|
-
const v = this.$resolveValue(b.path, b.scope);
|
|
1260
|
-
el[b.name] = v;
|
|
1261
|
-
// If the target is a WebUIElement, flush its pending updates
|
|
1262
|
-
// synchronously so child <for> loops re-render immediately.
|
|
1263
|
-
// Without this, the child's microtask-coalesced update runs
|
|
1264
|
-
// too late for view transitions that snapshot the DOM.
|
|
1265
|
-
const flush = el['$flushUpdates'];
|
|
1266
|
-
if (typeof flush === 'function')
|
|
1267
|
-
flush.call(el);
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
case ATTR_KIND_BOOLEAN: {
|
|
1271
|
-
const show = b.condition[0](this.$resolver, b.scope);
|
|
1272
|
-
if (show)
|
|
1273
|
-
el.setAttribute(b.name, '');
|
|
1274
|
-
else
|
|
1275
|
-
el.removeAttribute(b.name);
|
|
1276
|
-
// Form control properties must be set via DOM property, not attribute
|
|
1277
|
-
if (b.name === 'checked' || b.name === 'selected' || b.name === 'disabled') {
|
|
1278
|
-
el[b.name] = show;
|
|
1279
|
-
}
|
|
1280
|
-
break;
|
|
1281
|
-
}
|
|
1282
|
-
case ATTR_KIND_TEMPLATE: {
|
|
1283
|
-
const v = this.$resolveParts(b.parts, b.scope);
|
|
1284
|
-
if (el.getAttribute(b.name) !== v)
|
|
1285
|
-
el.setAttribute(b.name, v);
|
|
1286
|
-
break;
|
|
1287
|
-
}
|
|
1288
|
-
default: {
|
|
1289
|
-
const v = this.$resolveValue(b.path, b.scope);
|
|
1290
|
-
const s = v == null ? '' : String(v);
|
|
1291
|
-
// Form control properties diverge from attributes after user interaction
|
|
1292
|
-
if (b.name === 'checked' || b.name === 'selected') {
|
|
1293
|
-
el[b.name] = !!v && v !== 'false' && v !== '0';
|
|
1294
|
-
}
|
|
1295
|
-
else if (b.name === 'value') {
|
|
1296
|
-
if (el.value !== s)
|
|
1297
|
-
el.value = s;
|
|
1298
|
-
}
|
|
1299
|
-
else {
|
|
1300
|
-
if (el.getAttribute(b.name) !== s)
|
|
1301
|
-
el.setAttribute(b.name, s);
|
|
1302
|
-
}
|
|
1303
|
-
break;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
$toggleCond(c) {
|
|
1308
|
-
const show = c.condition[0](this.$resolver, c.scope);
|
|
1309
|
-
if (show) {
|
|
1310
|
-
if (!c.instance) {
|
|
1311
|
-
c.instance = this.$createBlockInstance(c.blockIndex, c.scope);
|
|
1312
|
-
if (c.instance) {
|
|
1313
|
-
const frag = document.createDocumentFragment();
|
|
1314
|
-
for (const n of c.instance.nodes)
|
|
1315
|
-
frag.appendChild(n);
|
|
1316
|
-
c.anchor.parentNode?.insertBefore(frag, c.anchor.nextSibling);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
else {
|
|
1320
|
-
this.$updateInstance(c.instance);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
else if (c.instance) {
|
|
1324
|
-
this.$removeInstance(c.instance);
|
|
1325
|
-
c.instance = null;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
// ── Value resolution ──────────────────────────────────────────
|
|
1329
|
-
$resolveValue(path, scope) {
|
|
1330
|
-
// Check scope frames first (repeat item variables)
|
|
1331
|
-
let frame = scope;
|
|
1332
|
-
while (frame) {
|
|
1333
|
-
if (path === frame.name)
|
|
1334
|
-
return frame.value;
|
|
1335
|
-
if (path.length > frame.name.length && path.charCodeAt(frame.name.length) === 46 && path.startsWith(frame.name)) {
|
|
1336
|
-
return dotWalk(frame.value, path, frame.name.length + 1);
|
|
1337
|
-
}
|
|
1338
|
-
frame = frame.parent;
|
|
1339
|
-
}
|
|
1340
|
-
// Resolve against component — fast path for single-segment (no dot)
|
|
1341
|
-
const dot = path.indexOf('.');
|
|
1342
|
-
if (dot === -1)
|
|
1343
|
-
return this[path];
|
|
1344
|
-
return dotWalk(this[path.substring(0, dot)], path, dot + 1);
|
|
1345
|
-
}
|
|
1346
|
-
$resolveParts(parts, scope) {
|
|
1347
|
-
let result = '';
|
|
1348
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1349
|
-
const p = parts[i];
|
|
1350
|
-
if (typeof p === 'string') {
|
|
1351
|
-
result += p;
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
const v = this.$resolveValue(p[0], scope);
|
|
1355
|
-
result += v == null ? '' : String(v);
|
|
1356
|
-
}
|
|
1357
|
-
return result;
|
|
1358
|
-
}
|
|
1359
|
-
// ── Block instance management ─────────────────────────────────
|
|
1360
|
-
$block(blockIndex) {
|
|
1361
|
-
return this.$meta?.b?.[blockIndex];
|
|
1362
|
-
}
|
|
1363
|
-
$createBlockInstance(blockIndex, scope) {
|
|
1364
|
-
const bm = this.$block(blockIndex);
|
|
1365
|
-
if (!bm)
|
|
1366
|
-
return null;
|
|
1367
|
-
const wrapper = this.$createStagingRoot(bm);
|
|
1368
|
-
const inst = this.$wire(wrapper, bm, scope);
|
|
1369
|
-
inst.nodes = childNodesArray(wrapper);
|
|
1370
|
-
this.$updateInstance(inst);
|
|
1371
|
-
if (inst.repeats.length !== 0 || inst.conds.length !== 0) {
|
|
1372
|
-
inst.nodes = childNodesArray(wrapper);
|
|
1373
|
-
this.$releaseStagingRepeatContainers(inst, wrapper);
|
|
1374
|
-
}
|
|
1375
|
-
return inst;
|
|
1376
|
-
}
|
|
1377
|
-
$removeInstance(instance) {
|
|
1378
|
-
for (const n of instance.nodes)
|
|
1379
|
-
n.parentNode?.removeChild(n);
|
|
1380
|
-
for (const c of instance.conds) {
|
|
1381
|
-
if (c.instance)
|
|
1382
|
-
this.$removeInstance(c.instance);
|
|
1383
|
-
}
|
|
1384
|
-
for (const r of instance.repeats) {
|
|
1385
|
-
for (const item of r.instances)
|
|
1386
|
-
this.$removeInstance(item.instance);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
$insertInstanceAfter(cursor, container, instance) {
|
|
1390
|
-
const nodes = instance.nodes;
|
|
1391
|
-
if (nodes.length === 0)
|
|
1392
|
-
return cursor;
|
|
1393
|
-
const ref = cursor ? cursor.nextSibling : container.firstChild;
|
|
1394
|
-
if (nodes[0] === ref)
|
|
1395
|
-
return nodes[nodes.length - 1];
|
|
1396
|
-
const frag = document.createDocumentFragment();
|
|
1397
|
-
for (let i = 0; i < nodes.length; i++)
|
|
1398
|
-
frag.appendChild(nodes[i]);
|
|
1399
|
-
container.insertBefore(frag, ref);
|
|
1400
|
-
return nodes[nodes.length - 1];
|
|
1401
|
-
}
|
|
1402
|
-
/** Extract the single dynamic path from a compiled attr parts array. */
|
|
1403
|
-
$singleDynamic(parts) {
|
|
1404
|
-
let path = '';
|
|
1405
|
-
let prefix = '';
|
|
1406
|
-
let suffix = '';
|
|
1407
|
-
let seen = false;
|
|
1408
|
-
for (const p of parts) {
|
|
1409
|
-
if (typeof p === 'string') {
|
|
1410
|
-
if (seen)
|
|
1411
|
-
suffix += p;
|
|
1412
|
-
else
|
|
1413
|
-
prefix += p;
|
|
1414
|
-
continue;
|
|
1415
|
-
}
|
|
1416
|
-
if (seen)
|
|
1417
|
-
return null;
|
|
1418
|
-
path = p[0];
|
|
1419
|
-
seen = true;
|
|
1420
|
-
}
|
|
1421
|
-
return seen ? { path, prefix, suffix } : null;
|
|
1422
|
-
}
|
|
1423
169
|
}
|