@microsoft/webui-framework 0.0.4 → 0.0.5
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/decorators.d.ts +30 -0
- package/dist/decorators.js +95 -0
- package/dist/element/diff.d.ts +23 -0
- package/dist/element/diff.js +117 -0
- package/dist/element/markers.d.ts +34 -0
- package/dist/element/markers.js +42 -0
- package/dist/element/styles.d.ts +7 -0
- package/dist/element/styles.js +29 -0
- package/dist/element/types.d.ts +105 -0
- package/dist/element/types.js +10 -0
- package/dist/element.d.ts +104 -0
- package/dist/element.js +1008 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +12 -0
- package/dist/lifecycle.d.ts +10 -0
- package/dist/lifecycle.js +27 -0
- package/dist/template-types.d.ts +49 -0
- package/dist/template-types.js +0 -0
- package/dist/template.d.ts +30 -0
- package/dist/template.js +6 -0
- package/package.json +2 -2
package/dist/element.js
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
import { getTemplate } from "./template.js";
|
|
2
|
+
import { hydrationStart, hydrationEnd } from "./lifecycle.js";
|
|
3
|
+
import { getObservableNames } from "./decorators.js";
|
|
4
|
+
import { syncRepeat, dotWalk } from "./element/diff.js";
|
|
5
|
+
import {
|
|
6
|
+
collectItemMarkers,
|
|
7
|
+
nextElement,
|
|
8
|
+
MARKER_COND_START,
|
|
9
|
+
MARKER_COND_END,
|
|
10
|
+
MARKER_REPEAT_START
|
|
11
|
+
} from "./element/markers.js";
|
|
12
|
+
import {
|
|
13
|
+
injectModuleStyle
|
|
14
|
+
} from "./element/styles.js";
|
|
15
|
+
import {
|
|
16
|
+
ATTR_KIND_BOOLEAN,
|
|
17
|
+
ATTR_KIND_COMPLEX,
|
|
18
|
+
ATTR_KIND_TEMPLATE
|
|
19
|
+
} from "./element/types.js";
|
|
20
|
+
const templateCache = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
const templateDOMCache = /* @__PURE__ */ new Map();
|
|
22
|
+
const tplOrdinalCache = /* @__PURE__ */ new WeakMap();
|
|
23
|
+
function getTplOrdinals(tplNode) {
|
|
24
|
+
let map = tplOrdinalCache.get(tplNode);
|
|
25
|
+
if (map) return map;
|
|
26
|
+
map = /* @__PURE__ */ new Map();
|
|
27
|
+
let elemOrd = 0;
|
|
28
|
+
let textOrd = 0;
|
|
29
|
+
const children = tplNode.childNodes;
|
|
30
|
+
for (let k = 0; k < children.length; k++) {
|
|
31
|
+
const type = children[k].nodeType;
|
|
32
|
+
if (type === 1) {
|
|
33
|
+
map.set(k, [1, elemOrd]);
|
|
34
|
+
elemOrd++;
|
|
35
|
+
} else if (type === 3) {
|
|
36
|
+
map.set(k, [3, textOrd]);
|
|
37
|
+
textOrd++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
tplOrdinalCache.set(tplNode, map);
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
const EMPTY_ARR = [];
|
|
44
|
+
function childNodesArray(parent) {
|
|
45
|
+
const children = parent.childNodes;
|
|
46
|
+
const len = children.length;
|
|
47
|
+
const result = new Array(len);
|
|
48
|
+
for (let i = 0; i < len; i++) result[i] = children[i];
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function getTemplateDom(meta) {
|
|
52
|
+
let cached = templateDOMCache.get(meta.h);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
const div = document.createElement("div");
|
|
55
|
+
div.innerHTML = meta.h;
|
|
56
|
+
templateDOMCache.set(meta.h, div);
|
|
57
|
+
return div;
|
|
58
|
+
}
|
|
59
|
+
class WebUIElement extends HTMLElement {
|
|
60
|
+
constructor() {
|
|
61
|
+
super(...arguments);
|
|
62
|
+
this.$root = null;
|
|
63
|
+
this.$ready = false;
|
|
64
|
+
this.$hydrated = false;
|
|
65
|
+
this.$dirtyPaths = null;
|
|
66
|
+
this.$pendingFlush = false;
|
|
67
|
+
/** Cached condition resolver — avoids allocating a closure per evaluation. */
|
|
68
|
+
this.$resolver = (p, s) => this.$resolveValue(p, s);
|
|
69
|
+
}
|
|
70
|
+
static define(tagName) {
|
|
71
|
+
customElements.define(tagName, this);
|
|
72
|
+
}
|
|
73
|
+
// ── Lifecycle ─────────────────────────────────────────────────
|
|
74
|
+
connectedCallback() {
|
|
75
|
+
const tag = this.tagName.toLowerCase();
|
|
76
|
+
if (this.$hydrated && this.$root) {
|
|
77
|
+
hydrationStart();
|
|
78
|
+
this.$ready = true;
|
|
79
|
+
this.$update();
|
|
80
|
+
hydrationEnd();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const meta = getTemplate(tag);
|
|
84
|
+
if (!meta) {
|
|
85
|
+
console.warn(
|
|
86
|
+
`[WebUI] Template metadata for <${tag}> not found. Ensure the component is included in the SSR output or registered via __webui_templates.`
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.$meta = meta;
|
|
91
|
+
if (document.readyState === "loading") {
|
|
92
|
+
const handler = () => {
|
|
93
|
+
document.removeEventListener("DOMContentLoaded", handler);
|
|
94
|
+
this.$mount(meta);
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener("DOMContentLoaded", handler);
|
|
97
|
+
} else {
|
|
98
|
+
this.$mount(meta);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Mount the component after children are available. */
|
|
102
|
+
$mount(meta) {
|
|
103
|
+
if (this.$hydrated) return;
|
|
104
|
+
hydrationStart();
|
|
105
|
+
const hasShadow = !!this.shadowRoot;
|
|
106
|
+
const wantShadow = hasShadow || !!meta.sd || !!window.__webui_shadow;
|
|
107
|
+
let root;
|
|
108
|
+
let isSSR;
|
|
109
|
+
if (hasShadow) {
|
|
110
|
+
root = this.shadowRoot;
|
|
111
|
+
isSSR = true;
|
|
112
|
+
} else if (this.childNodes.length > 0) {
|
|
113
|
+
root = this;
|
|
114
|
+
isSSR = true;
|
|
115
|
+
} else if (wantShadow) {
|
|
116
|
+
root = this.attachShadow({ mode: "open" });
|
|
117
|
+
const fragment = this.$parseTemplate(meta);
|
|
118
|
+
root.appendChild(fragment);
|
|
119
|
+
isSSR = false;
|
|
120
|
+
} else {
|
|
121
|
+
const fragment = this.$parseTemplate(meta);
|
|
122
|
+
this.appendChild(fragment);
|
|
123
|
+
root = this;
|
|
124
|
+
isSSR = false;
|
|
125
|
+
}
|
|
126
|
+
if (meta.sa) injectModuleStyle(meta.sa, this.shadowRoot);
|
|
127
|
+
if (isSSR) {
|
|
128
|
+
this.$applySSRState();
|
|
129
|
+
this.$root = this.$hydrate(root, meta, getTemplateDom(meta));
|
|
130
|
+
} else {
|
|
131
|
+
this.$root = this.$wire(root, meta);
|
|
132
|
+
}
|
|
133
|
+
this.$meta = meta;
|
|
134
|
+
this.$hydrated = true;
|
|
135
|
+
this.$ready = true;
|
|
136
|
+
hydrationEnd();
|
|
137
|
+
}
|
|
138
|
+
disconnectedCallback() {
|
|
139
|
+
}
|
|
140
|
+
/** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
|
|
141
|
+
$emit(name, detail) {
|
|
142
|
+
return this.dispatchEvent(
|
|
143
|
+
new CustomEvent(name, {
|
|
144
|
+
bubbles: true,
|
|
145
|
+
cancelable: true,
|
|
146
|
+
composed: !!this.shadowRoot,
|
|
147
|
+
detail
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
/** Populate @observable properties from router state. */
|
|
152
|
+
setInitialState(state) {
|
|
153
|
+
const names = getObservableNames(this.constructor);
|
|
154
|
+
for (const key of Object.keys(state)) {
|
|
155
|
+
if (names.has(key)) {
|
|
156
|
+
this[key] = state[key];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Apply SSR state from the global `window.__webui_state` object.
|
|
162
|
+
*
|
|
163
|
+
* Passing the same props to both server render and client
|
|
164
|
+
* hydrate, this ensures component observables match the server-rendered
|
|
165
|
+
* DOM. The handler emits the state as a `<script>` tag at the end of
|
|
166
|
+
* the page. Only observable properties are set — unknown keys are ignored.
|
|
167
|
+
*
|
|
168
|
+
* Writes directly to the backing field (`_prop`) to avoid triggering
|
|
169
|
+
* reactive updates before bindings are wired.
|
|
170
|
+
*/
|
|
171
|
+
$applySSRState() {
|
|
172
|
+
const state = window.__webui_state;
|
|
173
|
+
if (!state || typeof state !== "object") return;
|
|
174
|
+
const names = getObservableNames(this.constructor);
|
|
175
|
+
for (const key of Object.keys(state)) {
|
|
176
|
+
if (names.has(key)) {
|
|
177
|
+
this[`_${key}`] = state[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Reactive update — called by @observable/@attr setters. */
|
|
182
|
+
$update(path) {
|
|
183
|
+
if (!this.$ready || !this.$root) return;
|
|
184
|
+
if (!this.$pathIndex) this.$buildPathIndex();
|
|
185
|
+
if (path && this.$pathIndex) {
|
|
186
|
+
const entry = this.$pathIndex.get(path);
|
|
187
|
+
if (entry) {
|
|
188
|
+
if (!this.$dirtyPaths) this.$dirtyPaths = /* @__PURE__ */ new Set();
|
|
189
|
+
this.$dirtyPaths.add(path);
|
|
190
|
+
if (!this.$pendingFlush) {
|
|
191
|
+
this.$pendingFlush = true;
|
|
192
|
+
queueMicrotask(() => this.$flush());
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.$dirtyPaths = null;
|
|
198
|
+
this.$updateInstance(this.$root);
|
|
199
|
+
}
|
|
200
|
+
/** Synchronously flush all queued path updates. Call this when you need
|
|
201
|
+
* the DOM to reflect pending property changes immediately. */
|
|
202
|
+
$flushUpdates() {
|
|
203
|
+
if (this.$pendingFlush) this.$flush();
|
|
204
|
+
}
|
|
205
|
+
/** Flush all queued path updates. Handles re-entrant setter calls. */
|
|
206
|
+
$flush() {
|
|
207
|
+
if (!this.$ready || !this.$root || !this.$pathIndex) {
|
|
208
|
+
this.$dirtyPaths = null;
|
|
209
|
+
this.$pendingFlush = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
while (this.$dirtyPaths && this.$dirtyPaths.size > 0) {
|
|
213
|
+
const dirty = this.$dirtyPaths;
|
|
214
|
+
this.$dirtyPaths = null;
|
|
215
|
+
for (const path of dirty) {
|
|
216
|
+
const entry = this.$pathIndex.get(path);
|
|
217
|
+
if (entry) {
|
|
218
|
+
this.$updateBindings(entry.texts, entry.attrs, entry.conds, entry.repeats);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (this.$wildcardBindings) {
|
|
222
|
+
const wc = this.$wildcardBindings;
|
|
223
|
+
this.$updateBindings(wc.texts, wc.attrs, wc.conds, wc.repeats);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
this.$pendingFlush = false;
|
|
227
|
+
}
|
|
228
|
+
// ── DOM resolution: client-created path ───────────────────────
|
|
229
|
+
// Compiled paths are childNode indices in meta.h parsed by the browser.
|
|
230
|
+
// For client-created components the DOM matches meta.h exactly.
|
|
231
|
+
$resolve(root, path, pathStart = 0) {
|
|
232
|
+
let cur = root;
|
|
233
|
+
for (let i = 0; i < pathStart; i++) {
|
|
234
|
+
const child = cur.childNodes[path[i]];
|
|
235
|
+
if (!child) return null;
|
|
236
|
+
cur = child;
|
|
237
|
+
}
|
|
238
|
+
for (let i = pathStart; i < path.length; i++) {
|
|
239
|
+
const child = cur.childNodes[path[i]];
|
|
240
|
+
if (!child) return null;
|
|
241
|
+
cur = child;
|
|
242
|
+
}
|
|
243
|
+
return cur;
|
|
244
|
+
}
|
|
245
|
+
// ── DOM resolution: SSR hydration path ────────────────────────
|
|
246
|
+
// SSR DOM may lack whitespace text nodes the compiled template has.
|
|
247
|
+
// We walk the template DOM in parallel to translate each childNode
|
|
248
|
+
// index into an element-ordinal lookup in the SSR DOM.
|
|
249
|
+
// pathStart: skip leading path segments for in-place block hydration.
|
|
250
|
+
$resolveSSR(ssrRoot, tplRoot, path, pathStart = 0) {
|
|
251
|
+
let ssr = ssrRoot;
|
|
252
|
+
let tpl = tplRoot;
|
|
253
|
+
for (let i = 0; i < pathStart; i++) {
|
|
254
|
+
const tplChild = tpl.childNodes[path[i]];
|
|
255
|
+
if (!tplChild) return null;
|
|
256
|
+
tpl = tplChild;
|
|
257
|
+
}
|
|
258
|
+
for (let i = pathStart; i < path.length; i++) {
|
|
259
|
+
const idx = path[i];
|
|
260
|
+
const tplChild = tpl.childNodes[idx];
|
|
261
|
+
if (!tplChild) return null;
|
|
262
|
+
const ordinals = getTplOrdinals(tpl);
|
|
263
|
+
const entry = ordinals.get(idx);
|
|
264
|
+
if (!entry) return null;
|
|
265
|
+
const [nodeType, ordinal] = entry;
|
|
266
|
+
let count = 0;
|
|
267
|
+
let child = ssr.firstChild;
|
|
268
|
+
while (child) {
|
|
269
|
+
if (child.nodeType === nodeType) {
|
|
270
|
+
if (count === ordinal) break;
|
|
271
|
+
count++;
|
|
272
|
+
}
|
|
273
|
+
child = child.nextSibling;
|
|
274
|
+
}
|
|
275
|
+
if (!child) return null;
|
|
276
|
+
ssr = child;
|
|
277
|
+
tpl = tplChild;
|
|
278
|
+
}
|
|
279
|
+
return ssr;
|
|
280
|
+
}
|
|
281
|
+
// ── Template parsing ──────────────────────────────────────────
|
|
282
|
+
$parseTemplate(meta) {
|
|
283
|
+
let cached = templateCache.get(meta);
|
|
284
|
+
if (cached) return cached.cloneNode(true);
|
|
285
|
+
const tpl = document.createElement("template");
|
|
286
|
+
tpl.innerHTML = meta.h;
|
|
287
|
+
templateCache.set(meta, tpl.content);
|
|
288
|
+
return tpl.content.cloneNode(true);
|
|
289
|
+
}
|
|
290
|
+
// ═══════════════════════════════════════════════════════════════
|
|
291
|
+
// Client-created wiring — exact childNode index resolution
|
|
292
|
+
// ═══════════════════════════════════════════════════════════════
|
|
293
|
+
$wire(root, meta, scope) {
|
|
294
|
+
const instance = {
|
|
295
|
+
scope,
|
|
296
|
+
nodes: childNodesArray(root),
|
|
297
|
+
texts: [],
|
|
298
|
+
attrs: [],
|
|
299
|
+
conds: [],
|
|
300
|
+
repeats: []
|
|
301
|
+
};
|
|
302
|
+
const textRefs = [];
|
|
303
|
+
if (meta.tx) {
|
|
304
|
+
for (let i = 0; i < meta.tx.length; i++) {
|
|
305
|
+
const entry = meta.tx[i];
|
|
306
|
+
const [slot, parts] = entry;
|
|
307
|
+
const raw = entry[2] === 1;
|
|
308
|
+
const [parentPath, beforeIndex] = slot;
|
|
309
|
+
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
310
|
+
if (!parent || parent.nodeType !== 1 && parent.nodeType !== 11) continue;
|
|
311
|
+
textRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, parts, raw });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const condRefs = [];
|
|
315
|
+
if (meta.c) {
|
|
316
|
+
for (let i = 0; i < meta.c.length; i++) {
|
|
317
|
+
const [condition, blockIndex, slotMeta] = meta.c[i];
|
|
318
|
+
const [parentPath, beforeIndex] = slotMeta;
|
|
319
|
+
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
320
|
+
if (!parent || parent.nodeType !== 1 && parent.nodeType !== 11) continue;
|
|
321
|
+
condRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, condition, blockIndex });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const repRefs = [];
|
|
325
|
+
if (meta.r) {
|
|
326
|
+
for (let i = 0; i < meta.r.length; i++) {
|
|
327
|
+
const [collection, itemVar, blockIndex, slotMeta] = meta.r[i];
|
|
328
|
+
const [parentPath, beforeIndex] = slotMeta;
|
|
329
|
+
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
330
|
+
if (!parent || parent.nodeType !== 1 && parent.nodeType !== 11) continue;
|
|
331
|
+
repRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, collection, itemVar, blockIndex });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.$wireAttrs(instance, meta, scope, (p) => this.$resolve(root, p));
|
|
335
|
+
this.$finalize(root, meta, (r, p) => this.$resolve(r, p));
|
|
336
|
+
for (const t of textRefs) {
|
|
337
|
+
const anchor = document.createComment("");
|
|
338
|
+
t.parent.insertBefore(anchor, t.ref);
|
|
339
|
+
if (t.raw) {
|
|
340
|
+
const container = document.createElement("span");
|
|
341
|
+
t.parent.insertBefore(container, anchor);
|
|
342
|
+
const textNode = document.createTextNode("");
|
|
343
|
+
instance.texts.push({ node: textNode, parts: t.parts, scope, raw: true, rawParent: container });
|
|
344
|
+
} else {
|
|
345
|
+
const textNode = document.createTextNode("");
|
|
346
|
+
t.parent.insertBefore(textNode, anchor);
|
|
347
|
+
instance.texts.push({ node: textNode, parts: t.parts, scope });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const c of condRefs) {
|
|
351
|
+
const anchor = document.createComment("");
|
|
352
|
+
c.parent.insertBefore(anchor, c.ref);
|
|
353
|
+
instance.conds.push({ condition: c.condition, blockIndex: c.blockIndex, anchor, scope, instance: null });
|
|
354
|
+
}
|
|
355
|
+
for (let i = 0; i < repRefs.length; i++) {
|
|
356
|
+
const r = repRefs[i];
|
|
357
|
+
const anchor = document.createComment("");
|
|
358
|
+
r.parent.insertBefore(anchor, r.ref);
|
|
359
|
+
const { attrMap, rootBindings } = this.$repeatMaps(r.blockIndex, r.itemVar);
|
|
360
|
+
instance.repeats.push({
|
|
361
|
+
markerId: i,
|
|
362
|
+
collection: r.collection,
|
|
363
|
+
itemVar: r.itemVar,
|
|
364
|
+
blockIndex: r.blockIndex,
|
|
365
|
+
container: r.parent,
|
|
366
|
+
start: anchor,
|
|
367
|
+
end: null,
|
|
368
|
+
scope,
|
|
369
|
+
owner: instance,
|
|
370
|
+
instances: [],
|
|
371
|
+
rootTag: null,
|
|
372
|
+
attrMap,
|
|
373
|
+
rootBindings
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
for (let i = 0; i < instance.conds.length; i++) this.$toggleCond(instance.conds[i]);
|
|
377
|
+
return instance;
|
|
378
|
+
}
|
|
379
|
+
// ═══════════════════════════════════════════════════════════════
|
|
380
|
+
// SSR hydration — marker-based in-place DOM matching
|
|
381
|
+
// ═══════════════════════════════════════════════════════════════
|
|
382
|
+
/**
|
|
383
|
+
* Hydrate SSR-rendered DOM against compiled template metadata.
|
|
384
|
+
*
|
|
385
|
+
* When pathStart=0 (default): ssrRoot is a container with children
|
|
386
|
+
* (top-level component hydration).
|
|
387
|
+
*
|
|
388
|
+
* When pathStart=1: ssrRoot is a block element itself (repeat item
|
|
389
|
+
* in-place hydration). The leading [0] wrapper segment is skipped
|
|
390
|
+
* so compiled paths resolve directly against the element.
|
|
391
|
+
*/
|
|
392
|
+
$hydrate(ssrRoot, meta, tplDom, scope, pathStart = 0) {
|
|
393
|
+
const instance = {
|
|
394
|
+
scope,
|
|
395
|
+
nodes: pathStart > 0 ? [ssrRoot] : childNodesArray(ssrRoot),
|
|
396
|
+
texts: [],
|
|
397
|
+
attrs: [],
|
|
398
|
+
conds: [],
|
|
399
|
+
repeats: []
|
|
400
|
+
};
|
|
401
|
+
if (meta.tx) {
|
|
402
|
+
for (let i = 0; i < meta.tx.length; i++) {
|
|
403
|
+
const entry = meta.tx[i];
|
|
404
|
+
const [slot, parts] = entry;
|
|
405
|
+
const raw = entry[2] === 1;
|
|
406
|
+
const [parentPath, beforeIndex] = slot;
|
|
407
|
+
const ssrParent = this.$resolveSSR(ssrRoot, tplDom, parentPath, pathStart);
|
|
408
|
+
if (!ssrParent) continue;
|
|
409
|
+
const tplParent = this.$resolve(tplDom, parentPath, pathStart);
|
|
410
|
+
if (!tplParent) continue;
|
|
411
|
+
if (raw) {
|
|
412
|
+
const rawParent = ssrParent;
|
|
413
|
+
const textNode = document.createTextNode("");
|
|
414
|
+
instance.texts.push({ node: textNode, parts, scope, raw: true, rawParent });
|
|
415
|
+
} else {
|
|
416
|
+
const textNode = this.$findSSRText(ssrParent, tplParent, beforeIndex);
|
|
417
|
+
if (textNode) instance.texts.push({ node: textNode, parts, scope });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
this.$wireAttrs(
|
|
422
|
+
instance,
|
|
423
|
+
meta,
|
|
424
|
+
scope,
|
|
425
|
+
(p) => this.$resolveSSR(ssrRoot, tplDom, p, pathStart)
|
|
426
|
+
);
|
|
427
|
+
if (meta.c) {
|
|
428
|
+
let lastCondMarker = null;
|
|
429
|
+
let lastCondParent = null;
|
|
430
|
+
for (let i = 0; i < meta.c.length; i++) {
|
|
431
|
+
const [condition, blockIndex, slotMeta] = meta.c[i];
|
|
432
|
+
const [parentPath] = slotMeta;
|
|
433
|
+
const ssrParent = this.$resolveSSR(ssrRoot, tplDom, parentPath, pathStart) ?? ssrRoot;
|
|
434
|
+
const blockMeta = this.$block(blockIndex);
|
|
435
|
+
const shown = condition[0](this.$resolver, scope);
|
|
436
|
+
let condInstance = null;
|
|
437
|
+
if (ssrParent !== lastCondParent) {
|
|
438
|
+
lastCondMarker = null;
|
|
439
|
+
lastCondParent = ssrParent;
|
|
440
|
+
}
|
|
441
|
+
const marker = this.$findMarker(ssrParent, MARKER_COND_START, lastCondMarker);
|
|
442
|
+
let condAnchor;
|
|
443
|
+
if (marker) {
|
|
444
|
+
condAnchor = marker;
|
|
445
|
+
} else {
|
|
446
|
+
condAnchor = document.createComment("");
|
|
447
|
+
const [, beforeIndex] = slotMeta;
|
|
448
|
+
const insertRef = ssrParent.childNodes[beforeIndex ?? ssrParent.childNodes.length] ?? null;
|
|
449
|
+
ssrParent.insertBefore(condAnchor, insertRef);
|
|
450
|
+
}
|
|
451
|
+
if (marker) lastCondMarker = marker;
|
|
452
|
+
if (shown && blockMeta && marker) {
|
|
453
|
+
condInstance = this.$hydrateCondContent(condAnchor, blockMeta, scope);
|
|
454
|
+
}
|
|
455
|
+
if (marker) {
|
|
456
|
+
const lastNode = condInstance ? condInstance.nodes[condInstance.nodes.length - 1] : condAnchor;
|
|
457
|
+
const endMarker = lastNode?.nextSibling;
|
|
458
|
+
if (endMarker && endMarker.nodeType === 8 && endMarker.data === MARKER_COND_END) {
|
|
459
|
+
endMarker.parentNode?.removeChild(endMarker);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
instance.conds.push({
|
|
463
|
+
condition,
|
|
464
|
+
blockIndex,
|
|
465
|
+
anchor: condAnchor,
|
|
466
|
+
scope,
|
|
467
|
+
instance: condInstance
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (meta.r) {
|
|
472
|
+
let lastRepMarker = null;
|
|
473
|
+
let lastRepParent = null;
|
|
474
|
+
for (let i = 0; i < meta.r.length; i++) {
|
|
475
|
+
const [collection, itemVar, blockIndex, slotMeta] = meta.r[i];
|
|
476
|
+
const [parentPath] = slotMeta;
|
|
477
|
+
const ssrParent = this.$resolveSSR(ssrRoot, tplDom, parentPath, pathStart) ?? ssrRoot;
|
|
478
|
+
if (ssrParent !== lastRepParent) {
|
|
479
|
+
lastRepMarker = null;
|
|
480
|
+
lastRepParent = ssrParent;
|
|
481
|
+
}
|
|
482
|
+
const blockMeta = this.$block(blockIndex);
|
|
483
|
+
const { attrMap, rootBindings } = this.$repeatMaps(blockIndex, itemVar);
|
|
484
|
+
const rootTag = blockMeta ? this.$rootTag(blockMeta) : null;
|
|
485
|
+
const marker = this.$findMarker(ssrParent, MARKER_REPEAT_START, lastRepMarker);
|
|
486
|
+
let anchor;
|
|
487
|
+
if (marker) {
|
|
488
|
+
anchor = marker;
|
|
489
|
+
} else {
|
|
490
|
+
anchor = document.createComment("");
|
|
491
|
+
const [, beforeIndex] = slotMeta;
|
|
492
|
+
const tplParent = this.$resolve(tplDom, parentPath, pathStart);
|
|
493
|
+
const staticCount = tplParent ? tplParent.childNodes.length : 0;
|
|
494
|
+
const insertRef = ssrParent.childNodes[Math.min(beforeIndex ?? staticCount, ssrParent.childNodes.length)] ?? null;
|
|
495
|
+
ssrParent.insertBefore(anchor, insertRef);
|
|
496
|
+
}
|
|
497
|
+
lastRepMarker = anchor;
|
|
498
|
+
const repeatInsts = [];
|
|
499
|
+
const itemsArr = this.$resolveValue(collection, scope);
|
|
500
|
+
const items = Array.isArray(itemsArr) ? itemsArr : [];
|
|
501
|
+
const { items: itemMarkers, end: endMarker } = marker ? collectItemMarkers(anchor) : { items: [], end: null };
|
|
502
|
+
if (blockMeta && items.length > 0 && anchor.parentNode && itemMarkers.length > 0) {
|
|
503
|
+
if (itemMarkers.length !== items.length) {
|
|
504
|
+
console.warn(
|
|
505
|
+
`[webui] hydration: repeat marker count (${itemMarkers.length}) \u2260 data length (${items.length}) for "${collection}"`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const firstKey = Object.keys(attrMap)[0];
|
|
509
|
+
const blockTplDom = getTemplateDom(blockMeta);
|
|
510
|
+
const limit = Math.min(itemMarkers.length, items.length);
|
|
511
|
+
for (let j = 0; j < limit; j++) {
|
|
512
|
+
const itemValue = items[j];
|
|
513
|
+
const itemScope = { name: itemVar, value: itemValue, parent: scope };
|
|
514
|
+
if (rootTag) {
|
|
515
|
+
const itemEl = nextElement(itemMarkers[j]);
|
|
516
|
+
if (itemEl) {
|
|
517
|
+
const key = firstKey !== void 0 ? itemEl.getAttribute(firstKey) : String(j);
|
|
518
|
+
const childInstance = this.$hydrate(itemEl, blockMeta, blockTplDom, itemScope, 1);
|
|
519
|
+
repeatInsts.push({ key, value: itemValue, instance: childInstance });
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
const inst = {
|
|
523
|
+
scope: itemScope,
|
|
524
|
+
nodes: [],
|
|
525
|
+
texts: EMPTY_ARR,
|
|
526
|
+
attrs: EMPTY_ARR,
|
|
527
|
+
conds: [],
|
|
528
|
+
repeats: EMPTY_ARR
|
|
529
|
+
};
|
|
530
|
+
if (blockMeta.c) {
|
|
531
|
+
let cursor = itemMarkers[j].nextSibling;
|
|
532
|
+
const nextBound = j + 1 < itemMarkers.length ? itemMarkers[j + 1] : endMarker;
|
|
533
|
+
const itemParent = itemMarkers[j].parentNode;
|
|
534
|
+
for (let ci = 0; ci < blockMeta.c.length; ci++) {
|
|
535
|
+
const [condCond, condBlockIndex] = blockMeta.c[ci];
|
|
536
|
+
let condAnchor = null;
|
|
537
|
+
while (cursor && cursor !== nextBound) {
|
|
538
|
+
if (cursor.nodeType === 8 && cursor.data === MARKER_COND_START) {
|
|
539
|
+
condAnchor = cursor;
|
|
540
|
+
cursor = cursor.nextSibling;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
cursor = cursor.nextSibling;
|
|
544
|
+
}
|
|
545
|
+
if (!condAnchor) {
|
|
546
|
+
condAnchor = document.createComment("");
|
|
547
|
+
if (itemParent) itemParent.insertBefore(condAnchor, cursor ?? null);
|
|
548
|
+
}
|
|
549
|
+
const condMet = condCond[0](this.$resolver, itemScope);
|
|
550
|
+
let condInstance = null;
|
|
551
|
+
if (condMet) {
|
|
552
|
+
const condBlockMeta = this.$block(condBlockIndex);
|
|
553
|
+
if (condBlockMeta) {
|
|
554
|
+
condInstance = this.$hydrateCondContent(condAnchor, condBlockMeta, itemScope);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const lastNode = condInstance ? condInstance.nodes[condInstance.nodes.length - 1] : condAnchor;
|
|
558
|
+
const endM = lastNode?.nextSibling;
|
|
559
|
+
if (endM && endM.nodeType === 8 && endM.data === MARKER_COND_END) {
|
|
560
|
+
cursor = endM.nextSibling;
|
|
561
|
+
endM.parentNode?.removeChild(endM);
|
|
562
|
+
} else {
|
|
563
|
+
cursor = lastNode?.nextSibling ?? null;
|
|
564
|
+
}
|
|
565
|
+
inst.conds.push({
|
|
566
|
+
condition: condCond,
|
|
567
|
+
blockIndex: condBlockIndex,
|
|
568
|
+
anchor: condAnchor,
|
|
569
|
+
scope: itemScope,
|
|
570
|
+
instance: condInstance
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
repeatInsts.push({ key: String(j), value: itemValue, instance: inst });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
for (let m = 0; m < itemMarkers.length; m++) {
|
|
578
|
+
itemMarkers[m].parentNode?.removeChild(itemMarkers[m]);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (endMarker) endMarker.parentNode?.removeChild(endMarker);
|
|
582
|
+
instance.repeats.push({
|
|
583
|
+
markerId: i,
|
|
584
|
+
collection,
|
|
585
|
+
itemVar,
|
|
586
|
+
blockIndex,
|
|
587
|
+
container: anchor.parentNode ?? ssrRoot,
|
|
588
|
+
start: anchor,
|
|
589
|
+
end: null,
|
|
590
|
+
scope,
|
|
591
|
+
owner: instance,
|
|
592
|
+
instances: repeatInsts,
|
|
593
|
+
rootTag,
|
|
594
|
+
attrMap,
|
|
595
|
+
rootBindings,
|
|
596
|
+
synced: true
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
this.$finalize(ssrRoot, meta, (r, p) => this.$resolveSSR(r, tplDom, p, pathStart));
|
|
601
|
+
return instance;
|
|
602
|
+
}
|
|
603
|
+
// ── SSR helpers ───────────────────────────────────────────────
|
|
604
|
+
/** Collect sibling nodes between a start marker and an end marker comment. */
|
|
605
|
+
$collectBetween(start, endData) {
|
|
606
|
+
const nodes = [];
|
|
607
|
+
let node = start.nextSibling;
|
|
608
|
+
while (node) {
|
|
609
|
+
if (node.nodeType === 8 && node.data === endData) break;
|
|
610
|
+
nodes.push(node);
|
|
611
|
+
node = node.nextSibling;
|
|
612
|
+
}
|
|
613
|
+
return nodes;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Hydrate a conditional block's content — shared by top-level and
|
|
617
|
+
* repeat-item conditional hydration paths.
|
|
618
|
+
*/
|
|
619
|
+
$hydrateCondContent(condAnchor, blockMeta, scope) {
|
|
620
|
+
const rootTag = this.$rootTag(blockMeta);
|
|
621
|
+
if (rootTag) {
|
|
622
|
+
const el = nextElement(condAnchor);
|
|
623
|
+
if (el) {
|
|
624
|
+
const inst2 = this.$hydrate(el, blockMeta, getTemplateDom(blockMeta), scope, 1);
|
|
625
|
+
this.$updateInstance(inst2);
|
|
626
|
+
return inst2;
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const condNodes = this.$collectBetween(condAnchor, MARKER_COND_END);
|
|
631
|
+
if (condNodes.length === 0) return null;
|
|
632
|
+
const wrapper = document.createElement("div");
|
|
633
|
+
for (let cn = 0; cn < condNodes.length; cn++) wrapper.appendChild(condNodes[cn]);
|
|
634
|
+
const inst = this.$hydrate(wrapper, blockMeta, getTemplateDom(blockMeta), scope);
|
|
635
|
+
inst.nodes = childNodesArray(wrapper);
|
|
636
|
+
let afterNode = condAnchor;
|
|
637
|
+
for (let cn = 0; cn < inst.nodes.length; cn++) {
|
|
638
|
+
condAnchor.parentNode?.insertBefore(inst.nodes[cn], afterNode.nextSibling);
|
|
639
|
+
afterNode = inst.nodes[cn];
|
|
640
|
+
}
|
|
641
|
+
this.$updateInstance(inst);
|
|
642
|
+
return inst;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Find the next marker comment with the given data among a parent's children.
|
|
646
|
+
* Starts searching from `after` (exclusive) if provided, or from firstChild.
|
|
647
|
+
*/
|
|
648
|
+
$findMarker(parent, data, after) {
|
|
649
|
+
let child = after ? after.nextSibling : parent.firstChild;
|
|
650
|
+
while (child) {
|
|
651
|
+
if (child.nodeType === 8 && child.data === data) {
|
|
652
|
+
return child;
|
|
653
|
+
}
|
|
654
|
+
child = child.nextSibling;
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
/** Find existing SSR text node by mapping template text-node ordinal. */
|
|
659
|
+
$findSSRText(ssrParent, tplParent, beforeIndex) {
|
|
660
|
+
const ordinals = getTplOrdinals(tplParent);
|
|
661
|
+
let textOrd = 0;
|
|
662
|
+
for (let k = 0; k < beforeIndex; k++) {
|
|
663
|
+
const entry = ordinals.get(k);
|
|
664
|
+
if (entry && entry[0] === 3) textOrd++;
|
|
665
|
+
}
|
|
666
|
+
let count = 0;
|
|
667
|
+
let child = ssrParent.firstChild;
|
|
668
|
+
while (child) {
|
|
669
|
+
if (child.nodeType === 3) {
|
|
670
|
+
if (count === textOrd) return child;
|
|
671
|
+
count++;
|
|
672
|
+
}
|
|
673
|
+
child = child.nextSibling;
|
|
674
|
+
}
|
|
675
|
+
child = ssrParent.firstChild;
|
|
676
|
+
while (child) {
|
|
677
|
+
if (child.nodeType === 3 && child.data && child.data.trim()) {
|
|
678
|
+
return child;
|
|
679
|
+
}
|
|
680
|
+
child = child.nextSibling;
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
/** Extract root tag name from block metadata. */
|
|
685
|
+
$rootTag(meta) {
|
|
686
|
+
const h = meta.h;
|
|
687
|
+
if (!h || h.charCodeAt(0) !== 60) return null;
|
|
688
|
+
let end = 1;
|
|
689
|
+
while (end < h.length) {
|
|
690
|
+
const c = h.charCodeAt(end);
|
|
691
|
+
if (c === 32 || c === 62 || c === 47) break;
|
|
692
|
+
end++;
|
|
693
|
+
}
|
|
694
|
+
return h.slice(1, end).toLowerCase();
|
|
695
|
+
}
|
|
696
|
+
// ═══════════════════════════════════════════════════════════════
|
|
697
|
+
// Shared: binding wiring, event wiring, refs
|
|
698
|
+
// ═══════════════════════════════════════════════════════════════
|
|
699
|
+
/** Wire attribute bindings using a resolver (shared by $wire and $hydrate). */
|
|
700
|
+
$wireAttrs(instance, meta, scope, resolve) {
|
|
701
|
+
if (!meta.a || !meta.ag) return;
|
|
702
|
+
for (let g = 0; g < meta.ag.length; g++) {
|
|
703
|
+
const [targetPath, start, count] = meta.ag[g];
|
|
704
|
+
const el = resolve(targetPath);
|
|
705
|
+
if (!el || el.nodeType !== 1) continue;
|
|
706
|
+
for (let j = 0; j < count; j++) {
|
|
707
|
+
const entry = meta.a[start + j];
|
|
708
|
+
if (entry) instance.attrs.push(this.$makeAttr(el, entry, scope));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/** Wire events + root events + refs (shared by $wire and $hydrate). */
|
|
713
|
+
$finalize(root, meta, resolver) {
|
|
714
|
+
this.$wireEvents(root, meta, resolver);
|
|
715
|
+
if (meta.re) this.$wireRoot(meta.re);
|
|
716
|
+
this.$wireRefs(root);
|
|
717
|
+
}
|
|
718
|
+
/** Wire events using a resolver function (works for both client and SSR). */
|
|
719
|
+
$wireEvents(root, meta, resolver) {
|
|
720
|
+
if (!meta.e) return;
|
|
721
|
+
for (let i = 0; i < meta.e.length; i++) {
|
|
722
|
+
const [eventName, handlerName, needsEvent, target] = meta.e[i];
|
|
723
|
+
const el = resolver(root, target);
|
|
724
|
+
if (!el || el.nodeType !== 1) continue;
|
|
725
|
+
this.$addEvent(el, eventName, handlerName, needsEvent);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/** Wire root-level events on the host element (or shadow root when present). */
|
|
729
|
+
$wireRoot(re) {
|
|
730
|
+
const target = this.shadowRoot ?? this;
|
|
731
|
+
for (let i = 0; i < re.length; i++) {
|
|
732
|
+
this.$addEvent(target, re[i][0], re[i][1], re[i][2]);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/** Attach a single event listener. */
|
|
736
|
+
$addEvent(target, eventName, handlerName, needsEvent) {
|
|
737
|
+
const method = this[handlerName];
|
|
738
|
+
if (typeof method !== "function") return;
|
|
739
|
+
const self = this;
|
|
740
|
+
target.addEventListener(
|
|
741
|
+
eventName,
|
|
742
|
+
needsEvent ? (e) => method.call(self, e) : () => method.call(self)
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
/** Find w-ref attributes and assign to component properties. */
|
|
746
|
+
$wireRefs(root) {
|
|
747
|
+
if (root.nodeType !== 1 && root.nodeType !== 11) return;
|
|
748
|
+
const refs = root.querySelectorAll("[w-ref]");
|
|
749
|
+
for (let i = 0; i < refs.length; i++) {
|
|
750
|
+
const raw = refs[i].getAttribute("w-ref");
|
|
751
|
+
if (!raw || raw.charCodeAt(0) !== 123) continue;
|
|
752
|
+
const name = raw.slice(1, -1);
|
|
753
|
+
if (name) this[name] = refs[i];
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** Create an AttrBinding from compiled metadata. */
|
|
757
|
+
$makeAttr(el, entry, scope) {
|
|
758
|
+
const name = entry[0];
|
|
759
|
+
const kind = entry[1];
|
|
760
|
+
if (kind === ATTR_KIND_BOOLEAN) return { element: el, name, kind, condition: entry[2], scope };
|
|
761
|
+
if (kind === ATTR_KIND_TEMPLATE) return { element: el, name, kind, parts: entry[2], scope };
|
|
762
|
+
return { element: el, name, kind, path: entry[2] || "", scope };
|
|
763
|
+
}
|
|
764
|
+
/** Build attrMap and rootBindings for a repeat block. */
|
|
765
|
+
$repeatMaps(blockIndex, itemVar) {
|
|
766
|
+
const attrMap = {};
|
|
767
|
+
const rootBindings = [];
|
|
768
|
+
const bm = this.$block(blockIndex);
|
|
769
|
+
if (bm?.a && bm.ag) {
|
|
770
|
+
for (let g = 0; g < bm.ag.length; g++) {
|
|
771
|
+
const [tp, s, c] = bm.ag[g];
|
|
772
|
+
const isRoot = tp.length === 0 || tp.length === 1 && tp[0] === 0;
|
|
773
|
+
if (isRoot) {
|
|
774
|
+
for (let j = 0; j < c; j++) {
|
|
775
|
+
const entry = bm.a[s + j];
|
|
776
|
+
if (entry) {
|
|
777
|
+
rootBindings.push(entry);
|
|
778
|
+
if (entry[1] === 0 || entry[1] === 3) {
|
|
779
|
+
const dp = this.$singleDynamic(
|
|
780
|
+
entry[1] === 3 ? entry[2] : [[entry[2]]]
|
|
781
|
+
);
|
|
782
|
+
if (dp && dp.path.startsWith(itemVar + ".")) {
|
|
783
|
+
attrMap[entry[0]] = dp.path.slice(itemVar.length + 1);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return { attrMap, rootBindings };
|
|
792
|
+
}
|
|
793
|
+
// ═══════════════════════════════════════════════════════════════
|
|
794
|
+
// Reactive update system
|
|
795
|
+
// ═══════════════════════════════════════════════════════════════
|
|
796
|
+
$buildPathIndex() {
|
|
797
|
+
if (!this.$root) return;
|
|
798
|
+
const observableNames = getObservableNames(this.constructor);
|
|
799
|
+
const index = /* @__PURE__ */ new Map();
|
|
800
|
+
const ensure = (key) => {
|
|
801
|
+
let e = index.get(key);
|
|
802
|
+
if (!e) {
|
|
803
|
+
e = { texts: [], attrs: [], conds: [], repeats: [] };
|
|
804
|
+
index.set(key, e);
|
|
805
|
+
}
|
|
806
|
+
return e;
|
|
807
|
+
};
|
|
808
|
+
const keyFor = (path) => {
|
|
809
|
+
const dot = path.indexOf(".");
|
|
810
|
+
const root = dot > -1 ? path.slice(0, dot) : path;
|
|
811
|
+
return observableNames.has(root) ? root : "*";
|
|
812
|
+
};
|
|
813
|
+
const r = this.$root;
|
|
814
|
+
for (const t of r.texts) {
|
|
815
|
+
if (t.parts) {
|
|
816
|
+
for (const p of t.parts) {
|
|
817
|
+
if (typeof p !== "string") ensure(keyFor(p[0])).texts.push(t);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const a of r.attrs) {
|
|
822
|
+
if (a.path) ensure(keyFor(a.path)).attrs.push(a);
|
|
823
|
+
if (a.parts) {
|
|
824
|
+
for (const p of a.parts) {
|
|
825
|
+
if (typeof p !== "string") ensure(keyFor(p[0])).attrs.push(a);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (a.condition) {
|
|
829
|
+
for (const p of a.condition[1]) ensure(keyFor(p)).attrs.push(a);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
for (const c of r.conds) {
|
|
833
|
+
for (const p of c.condition[1]) ensure(keyFor(p)).conds.push(c);
|
|
834
|
+
}
|
|
835
|
+
for (const rep of r.repeats) {
|
|
836
|
+
ensure(keyFor(rep.collection)).repeats.push(rep);
|
|
837
|
+
}
|
|
838
|
+
const wc = index.get("*");
|
|
839
|
+
if (wc) {
|
|
840
|
+
index.delete("*");
|
|
841
|
+
this.$wildcardBindings = wc;
|
|
842
|
+
} else {
|
|
843
|
+
this.$wildcardBindings = null;
|
|
844
|
+
}
|
|
845
|
+
this.$pathIndex = index;
|
|
846
|
+
}
|
|
847
|
+
$updateBindings(texts, attrs, conds, repeats) {
|
|
848
|
+
for (let i = 0; i < texts.length; i++) this.$patchText(texts[i]);
|
|
849
|
+
for (let i = 0; i < attrs.length; i++) this.$patchAttr(attrs[i]);
|
|
850
|
+
for (let i = 0; i < conds.length; i++) this.$toggleCond(conds[i]);
|
|
851
|
+
for (let i = 0; i < repeats.length; i++) {
|
|
852
|
+
syncRepeat(this, repeats[i]);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
$updateInstance(instance) {
|
|
856
|
+
this.$updateBindings(instance.texts, instance.attrs, instance.conds, instance.repeats);
|
|
857
|
+
}
|
|
858
|
+
$patchText(b) {
|
|
859
|
+
let val;
|
|
860
|
+
if (b.parts) {
|
|
861
|
+
val = this.$resolveParts(b.parts, b.scope);
|
|
862
|
+
} else if (b.path) {
|
|
863
|
+
const raw = this.$resolveValue(b.path, b.scope);
|
|
864
|
+
val = raw == null ? "" : String(raw);
|
|
865
|
+
} else {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (b.raw && b.rawParent) {
|
|
869
|
+
if (b.rawParent.innerHTML !== val) b.rawParent.innerHTML = val;
|
|
870
|
+
} else {
|
|
871
|
+
if (b.node.data !== val) b.node.data = val;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
$patchAttr(b) {
|
|
875
|
+
const el = b.element;
|
|
876
|
+
switch (b.kind) {
|
|
877
|
+
case ATTR_KIND_COMPLEX: {
|
|
878
|
+
const v = this.$resolveValue(b.path, b.scope);
|
|
879
|
+
el[b.name] = v;
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
case ATTR_KIND_BOOLEAN: {
|
|
883
|
+
const show = b.condition[0](this.$resolver, b.scope);
|
|
884
|
+
if (show) el.setAttribute(b.name, "");
|
|
885
|
+
else el.removeAttribute(b.name);
|
|
886
|
+
if (b.name === "checked" || b.name === "selected" || b.name === "disabled") {
|
|
887
|
+
el[b.name] = show;
|
|
888
|
+
}
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
case ATTR_KIND_TEMPLATE: {
|
|
892
|
+
const v = this.$resolveParts(b.parts, b.scope);
|
|
893
|
+
if (el.getAttribute(b.name) !== v) el.setAttribute(b.name, v);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
default: {
|
|
897
|
+
const v = this.$resolveValue(b.path, b.scope);
|
|
898
|
+
const s = v == null ? "" : String(v);
|
|
899
|
+
if (b.name === "checked" || b.name === "selected") {
|
|
900
|
+
el[b.name] = !!v && v !== "false" && v !== "0";
|
|
901
|
+
} else if (b.name === "value") {
|
|
902
|
+
if (el.value !== s) el.value = s;
|
|
903
|
+
} else {
|
|
904
|
+
if (el.getAttribute(b.name) !== s) el.setAttribute(b.name, s);
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
$toggleCond(c) {
|
|
911
|
+
const show = c.condition[0](this.$resolver, c.scope);
|
|
912
|
+
if (show) {
|
|
913
|
+
if (!c.instance) {
|
|
914
|
+
c.instance = this.$createBlockInstance(c.blockIndex, c.scope);
|
|
915
|
+
if (c.instance) {
|
|
916
|
+
const frag = document.createDocumentFragment();
|
|
917
|
+
for (const n of c.instance.nodes) frag.appendChild(n);
|
|
918
|
+
c.anchor.parentNode?.insertBefore(frag, c.anchor.nextSibling);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (c.instance) this.$updateInstance(c.instance);
|
|
922
|
+
} else if (c.instance) {
|
|
923
|
+
this.$removeInstance(c.instance);
|
|
924
|
+
c.instance = null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// ── Value resolution ──────────────────────────────────────────
|
|
928
|
+
$resolveValue(path, scope) {
|
|
929
|
+
let frame = scope;
|
|
930
|
+
while (frame) {
|
|
931
|
+
if (path === frame.name) return frame.value;
|
|
932
|
+
if (path.length > frame.name.length && path.charCodeAt(frame.name.length) === 46 && path.startsWith(frame.name)) {
|
|
933
|
+
return dotWalk(frame.value, path, frame.name.length + 1);
|
|
934
|
+
}
|
|
935
|
+
frame = frame.parent;
|
|
936
|
+
}
|
|
937
|
+
const dot = path.indexOf(".");
|
|
938
|
+
if (dot === -1) return this[path];
|
|
939
|
+
return dotWalk(this[path.substring(0, dot)], path, dot + 1);
|
|
940
|
+
}
|
|
941
|
+
$resolveParts(parts, scope) {
|
|
942
|
+
let result = "";
|
|
943
|
+
for (let i = 0; i < parts.length; i++) {
|
|
944
|
+
const p = parts[i];
|
|
945
|
+
if (typeof p === "string") {
|
|
946
|
+
result += p;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const v = this.$resolveValue(p[0], scope);
|
|
950
|
+
result += v == null ? "" : String(v);
|
|
951
|
+
}
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
// ── Block instance management ─────────────────────────────────
|
|
955
|
+
$block(blockIndex) {
|
|
956
|
+
return this.$meta?.b?.[blockIndex];
|
|
957
|
+
}
|
|
958
|
+
$createBlockInstance(blockIndex, scope) {
|
|
959
|
+
const bm = this.$block(blockIndex);
|
|
960
|
+
if (!bm) return null;
|
|
961
|
+
const frag = this.$parseTemplate(bm);
|
|
962
|
+
const wrapper = document.createElement("div");
|
|
963
|
+
wrapper.appendChild(frag);
|
|
964
|
+
const inst = this.$wire(wrapper, bm, scope);
|
|
965
|
+
inst.nodes = childNodesArray(wrapper);
|
|
966
|
+
return inst;
|
|
967
|
+
}
|
|
968
|
+
$removeInstance(instance) {
|
|
969
|
+
for (const n of instance.nodes) n.parentNode?.removeChild(n);
|
|
970
|
+
for (const c of instance.conds) {
|
|
971
|
+
if (c.instance) this.$removeInstance(c.instance);
|
|
972
|
+
}
|
|
973
|
+
for (const r of instance.repeats) {
|
|
974
|
+
for (const item of r.instances) this.$removeInstance(item.instance);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
$insertInstanceAfter(cursor, container, instance) {
|
|
978
|
+
const nodes = instance.nodes;
|
|
979
|
+
if (nodes.length === 0) return cursor;
|
|
980
|
+
const ref = cursor ? cursor.nextSibling : container.firstChild;
|
|
981
|
+
if (nodes[0] === ref) return nodes[nodes.length - 1];
|
|
982
|
+
const frag = document.createDocumentFragment();
|
|
983
|
+
for (let i = 0; i < nodes.length; i++) frag.appendChild(nodes[i]);
|
|
984
|
+
container.insertBefore(frag, ref);
|
|
985
|
+
return nodes[nodes.length - 1];
|
|
986
|
+
}
|
|
987
|
+
/** Extract the single dynamic path from a compiled attr parts array. */
|
|
988
|
+
$singleDynamic(parts) {
|
|
989
|
+
let path = "";
|
|
990
|
+
let prefix = "";
|
|
991
|
+
let suffix = "";
|
|
992
|
+
let seen = false;
|
|
993
|
+
for (const p of parts) {
|
|
994
|
+
if (typeof p === "string") {
|
|
995
|
+
if (seen) suffix += p;
|
|
996
|
+
else prefix += p;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (seen) return null;
|
|
1000
|
+
path = p[0];
|
|
1001
|
+
seen = true;
|
|
1002
|
+
}
|
|
1003
|
+
return seen ? { path, prefix, suffix } : null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
export {
|
|
1007
|
+
WebUIElement
|
|
1008
|
+
};
|