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