@ruledwdl/dom 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/wdl-dom.min.js +2 -2
- package/package.json +1 -1
- package/src/index.js +737 -169
package/src/index.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* No innerHTML on updates.
|
|
7
7
|
*
|
|
8
8
|
* State events handled (from @ruledwdl/state):
|
|
9
|
-
* layers:change actions: set | append | before | after | wrap | remove | update
|
|
9
|
+
* layers:change actions: set | append | prepend | before | after | wrap | unwrap | move | remove | update
|
|
10
10
|
* attr:change actions: set | update | remove
|
|
11
|
-
* data:change actions: set | remove
|
|
11
|
+
* data:change actions: set | update | remove (surgical loop child reconciliation & item-scoped binding)
|
|
12
12
|
* variant:change actions: set
|
|
13
13
|
* registry:change actions: set | update | addRule | removeRule
|
|
14
14
|
*
|
|
@@ -23,59 +23,109 @@
|
|
|
23
23
|
* }
|
|
24
24
|
*
|
|
25
25
|
* Usage:
|
|
26
|
-
* import { createWdlDom } from '
|
|
26
|
+
* import { createWdlDom } from '@ruledwdl/dom';
|
|
27
27
|
* // or: import { ComponentManager } from '@ruledwdl/state';
|
|
28
28
|
*
|
|
29
|
-
* const
|
|
29
|
+
* const comp = manager.create('pricing-card', {
|
|
30
|
+
* layers: 'ul.features > li.feature*features',
|
|
31
|
+
* attr: { '.feature': { text: '${text}' } },
|
|
32
|
+
* data: { features: [{ text: 'First' }, { text: 'Second' }] }
|
|
33
|
+
* });
|
|
30
34
|
* const dom = createWdlDom({
|
|
31
35
|
* container: document.getElementById('app'),
|
|
32
|
-
* component:
|
|
33
|
-
* // optional:
|
|
34
|
-
* // onDataBind: (el, path, value) => { ... },
|
|
35
|
-
* // styleTarget: document.head,
|
|
36
|
+
* component: comp
|
|
36
37
|
* });
|
|
37
38
|
*
|
|
38
|
-
* // later:
|
|
39
|
+
* // later: comp.data.set('features', [{ text: 'Updated' }]); // → surgical row reconciliation
|
|
39
40
|
* // destroy: dom.destroy();
|
|
40
41
|
*/
|
|
41
42
|
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
|
-
// Helpers
|
|
44
|
+
// Helpers & Data Resolvers
|
|
44
45
|
// ---------------------------------------------------------------------------
|
|
45
46
|
|
|
46
|
-
const ATTR_SKIP = new Set(['text', 'class', 'html']);
|
|
47
|
+
const ATTR_SKIP = new Set(['text', 'class', 'html', 'alpine', 'htmx', 'attr-ref']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve dot-notated path on an object (e.g. "user.name" or "text")
|
|
51
|
+
*/
|
|
52
|
+
export function resolvePath(obj, path) {
|
|
53
|
+
if (!path || obj == null) return obj ?? '';
|
|
54
|
+
return String(path).split('.').reduce((a, k) => (a != null ? a[k] : ''), obj) ?? '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve template strings with ${path} or {{path}} expressions
|
|
59
|
+
*/
|
|
60
|
+
export function resolveStr(str, data) {
|
|
61
|
+
if (typeof str !== 'string') return str;
|
|
62
|
+
return str
|
|
63
|
+
.replace(/\$\{([\w.]+)\}/g, (_, p) => {
|
|
64
|
+
const val = resolvePath(data, p);
|
|
65
|
+
return val != null ? String(val) : '';
|
|
66
|
+
})
|
|
67
|
+
.replace(/\{\{([\w.]+)\}\}/g, (_, p) => {
|
|
68
|
+
const val = resolvePath(data, p);
|
|
69
|
+
return val != null ? String(val) : '';
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deeply resolve string templates within objects/arrays
|
|
75
|
+
*/
|
|
76
|
+
export function resolveAll(obj, data) {
|
|
77
|
+
if (obj == null) return obj;
|
|
78
|
+
if (typeof obj === 'function') return '';
|
|
79
|
+
if (typeof obj === 'string') return resolveStr(obj, data);
|
|
80
|
+
if (Array.isArray(obj)) return obj.map((v) => resolveAll(v, data));
|
|
81
|
+
if (typeof obj === 'object') {
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
Object.entries(obj).map(([k, v]) => [k, resolveAll(v, data)])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return obj;
|
|
87
|
+
}
|
|
47
88
|
|
|
48
89
|
/**
|
|
49
|
-
* Parse a single layer token: "button.cta" | "
|
|
50
|
-
* @returns {{ tag: string, semanticId: string }}
|
|
90
|
+
* Parse a single layer token: "button.cta" | "li.feature*features" | "div.card*3"
|
|
91
|
+
* @returns {{ tag: string, semanticId: string, repeator: string | null }}
|
|
51
92
|
*/
|
|
52
|
-
function parseLayerToken(expr) {
|
|
93
|
+
export function parseLayerToken(expr) {
|
|
53
94
|
if (typeof expr !== 'string') {
|
|
54
95
|
if (expr && typeof expr === 'object') {
|
|
55
96
|
return {
|
|
56
97
|
tag: String(expr.tag || 'div').toLowerCase(),
|
|
57
98
|
semanticId: String(expr.semanticId || expr.id || '').replace(/^\./, ''),
|
|
99
|
+
repeator: expr.repeator || expr.loopKey || null,
|
|
58
100
|
};
|
|
59
101
|
}
|
|
60
102
|
throw new Error('[wdl-dom] invalid layer expression');
|
|
61
103
|
}
|
|
62
104
|
const trimmed = expr.trim();
|
|
63
|
-
|
|
105
|
+
let str = trimmed;
|
|
106
|
+
let repeator = null;
|
|
107
|
+
const multIdx = str.indexOf('*');
|
|
108
|
+
if (multIdx !== -1) {
|
|
109
|
+
repeator = str.slice(multIdx + 1);
|
|
110
|
+
str = str.slice(0, multIdx);
|
|
111
|
+
}
|
|
112
|
+
const m = str.match(/^([a-zA-Z][a-zA-Z0-9_-]*)(?:\.([a-zA-Z0-9_-]+))?$/);
|
|
64
113
|
if (!m) {
|
|
65
114
|
// fallback: treat whole string as tag
|
|
66
|
-
return { tag:
|
|
115
|
+
return { tag: str.toLowerCase() || 'div', semanticId: '', repeator };
|
|
67
116
|
}
|
|
68
117
|
return {
|
|
69
118
|
tag: m[1].toLowerCase(),
|
|
70
119
|
semanticId: (m[2] || '').replace(/^\./, ''),
|
|
120
|
+
repeator,
|
|
71
121
|
};
|
|
72
122
|
}
|
|
73
123
|
|
|
74
124
|
/**
|
|
75
|
-
*
|
|
125
|
+
* Simple layers string → tree parser (supports >, +, <, <*N, <@N, and *repeator).
|
|
76
126
|
* For full WDL grammar prefer tree from component.layers.tree().
|
|
77
127
|
*/
|
|
78
|
-
function parseLayersSimple(str) {
|
|
128
|
+
export function parseLayersSimple(str) {
|
|
79
129
|
if (Array.isArray(str)) return structuredClone(str);
|
|
80
130
|
if (typeof str !== 'string' || !str.trim()) return [];
|
|
81
131
|
|
|
@@ -87,20 +137,57 @@ function parseLayersSimple(str) {
|
|
|
87
137
|
|
|
88
138
|
while (i < str.length) {
|
|
89
139
|
const ch = str[i];
|
|
90
|
-
if (/\s/.test(ch)) {
|
|
140
|
+
if (/\s/.test(ch)) {
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
91
144
|
if (ch === '>') {
|
|
92
145
|
const last = top().children[top().children.length - 1];
|
|
93
146
|
if (last) stack.push(last);
|
|
94
147
|
i++;
|
|
95
148
|
continue;
|
|
96
149
|
}
|
|
97
|
-
if (ch === '+') {
|
|
150
|
+
if (ch === '+') {
|
|
151
|
+
i++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
98
154
|
if (ch === '<') {
|
|
99
155
|
i++;
|
|
100
|
-
if (
|
|
156
|
+
if (i < str.length && str[i] === '*') {
|
|
157
|
+
i++;
|
|
158
|
+
let numStr = '';
|
|
159
|
+
while (i < str.length && /\d/.test(str[i])) {
|
|
160
|
+
numStr += str[i++];
|
|
161
|
+
}
|
|
162
|
+
const count = numStr ? parseInt(numStr, 10) : 1;
|
|
163
|
+
for (let k = 0; k < count; k++) {
|
|
164
|
+
if (stack.length > 1) stack.pop();
|
|
165
|
+
}
|
|
166
|
+
} else if (i < str.length && str[i] === '@') {
|
|
167
|
+
i++;
|
|
168
|
+
let numStr = '';
|
|
169
|
+
while (i < str.length && /\d/.test(str[i])) {
|
|
170
|
+
numStr += str[i++];
|
|
171
|
+
}
|
|
172
|
+
const targetDepth = numStr ? parseInt(numStr, 10) : 0;
|
|
173
|
+
const targetStackLen = targetDepth + 1;
|
|
174
|
+
while (stack.length > targetStackLen && stack.length > 1) {
|
|
175
|
+
stack.pop();
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
let count = 1;
|
|
179
|
+
while (i < str.length && str[i] === '<') {
|
|
180
|
+
count++;
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
for (let k = 0; k < count; k++) {
|
|
184
|
+
if (stack.length > 1) stack.pop();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
101
187
|
continue;
|
|
102
188
|
}
|
|
103
|
-
|
|
189
|
+
|
|
190
|
+
// Element token
|
|
104
191
|
let tag = '';
|
|
105
192
|
while (i < str.length && /[a-zA-Z0-9_-]/.test(str[i])) tag += str[i++];
|
|
106
193
|
let semanticId = '';
|
|
@@ -108,12 +195,24 @@ function parseLayersSimple(str) {
|
|
|
108
195
|
i++;
|
|
109
196
|
while (i < str.length && /[a-zA-Z0-9_-]/.test(str[i])) semanticId += str[i++];
|
|
110
197
|
}
|
|
111
|
-
|
|
198
|
+
let repeator = null;
|
|
199
|
+
if (str[i] === '*') {
|
|
200
|
+
i++;
|
|
201
|
+
let rep = '';
|
|
202
|
+
while (i < str.length && /[a-zA-Z0-9_.]/.test(str[i])) rep += str[i++];
|
|
203
|
+
if (rep) repeator = rep;
|
|
204
|
+
}
|
|
205
|
+
top().children.push({
|
|
206
|
+
tag: tag.toLowerCase() || 'div',
|
|
207
|
+
semanticId,
|
|
208
|
+
repeator,
|
|
209
|
+
children: [],
|
|
210
|
+
});
|
|
112
211
|
}
|
|
113
212
|
return root.children;
|
|
114
213
|
}
|
|
115
214
|
|
|
116
|
-
function normalizeId(id) {
|
|
215
|
+
export function normalizeId(id) {
|
|
117
216
|
if (id == null) return '';
|
|
118
217
|
return String(id).replace(/^\./, '');
|
|
119
218
|
}
|
|
@@ -126,7 +225,7 @@ export class WdlDom {
|
|
|
126
225
|
/**
|
|
127
226
|
* @param {object} options
|
|
128
227
|
* @param {HTMLElement|string} options.container
|
|
129
|
-
* @param {object} options.component ComponentState instance (has .on, .layers, .attr, .getSnapshot)
|
|
228
|
+
* @param {object} options.component ComponentState instance (has .on, .layers, .attr, .data, .getSnapshot)
|
|
130
229
|
* @param {(el: HTMLElement, path: string, value: any) => void} [options.onDataBind]
|
|
131
230
|
* @param {HTMLElement|Document} [options.styleTarget=document.head]
|
|
132
231
|
* @param {boolean} [options.debug=false]
|
|
@@ -154,9 +253,18 @@ export class WdlDom {
|
|
|
154
253
|
this.styleTarget = styleTarget;
|
|
155
254
|
this.debug = debug;
|
|
156
255
|
|
|
157
|
-
/** @type {Map<string, HTMLElement>} semanticId → element */
|
|
256
|
+
/** @type {Map<string, HTMLElement>} semanticId → first live element (backward compatible) */
|
|
158
257
|
this.liveMap = new Map();
|
|
159
258
|
|
|
259
|
+
/** @type {Map<string, HTMLElement[]>} semanticId → all live elements */
|
|
260
|
+
this.liveNodes = new Map();
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Active loop bindings for surgical reconciliation
|
|
264
|
+
* @type {Array<{ parentEl: HTMLElement, node: any, repeator: string, elements: HTMLElement[], anchor: any }>}
|
|
265
|
+
*/
|
|
266
|
+
this._loopBindings = [];
|
|
267
|
+
|
|
160
268
|
/** @type {Array<() => void>} */
|
|
161
269
|
this._unsubs = [];
|
|
162
270
|
|
|
@@ -176,25 +284,212 @@ export class WdlDom {
|
|
|
176
284
|
this._mount();
|
|
177
285
|
}
|
|
178
286
|
|
|
179
|
-
/** Current live map (read-only view) */
|
|
287
|
+
/** Current live map of first elements (read-only view) */
|
|
180
288
|
getLiveMap() {
|
|
181
289
|
return new Map(this.liveMap);
|
|
182
290
|
}
|
|
183
291
|
|
|
292
|
+
/** Get all live elements matching a semantic ID */
|
|
293
|
+
getLiveNodes(semanticId) {
|
|
294
|
+
const id = normalizeId(semanticId);
|
|
295
|
+
return [...(this.liveNodes.get(id) || [])];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Get single element by semantic ID and optional index */
|
|
299
|
+
getNode(semanticId, index = 0) {
|
|
300
|
+
const list = this.getLiveNodes(semanticId);
|
|
301
|
+
return list[index] || null;
|
|
302
|
+
}
|
|
303
|
+
|
|
184
304
|
/** Clean up listeners and DOM */
|
|
185
305
|
destroy() {
|
|
186
306
|
this._unsubs.forEach((u) => {
|
|
187
|
-
try {
|
|
307
|
+
try {
|
|
308
|
+
u();
|
|
309
|
+
} catch (_) {}
|
|
188
310
|
});
|
|
189
311
|
this._unsubs = [];
|
|
190
312
|
this.container.replaceChildren();
|
|
191
313
|
this.liveMap.clear();
|
|
314
|
+
this.liveNodes.clear();
|
|
315
|
+
this._loopBindings = [];
|
|
192
316
|
if (this._styleEl && this._styleEl.parentNode) {
|
|
193
317
|
this._styleEl.parentNode.removeChild(this._styleEl);
|
|
194
318
|
}
|
|
195
319
|
this._styleEl = null;
|
|
196
320
|
}
|
|
197
321
|
|
|
322
|
+
// -----------------------------------------------------------------------
|
|
323
|
+
// Node Registry Management
|
|
324
|
+
// -----------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
_registerNode(id, el) {
|
|
327
|
+
if (!id) return;
|
|
328
|
+
const norm = normalizeId(id);
|
|
329
|
+
if (!this.liveNodes.has(norm)) {
|
|
330
|
+
this.liveNodes.set(norm, []);
|
|
331
|
+
}
|
|
332
|
+
const list = this.liveNodes.get(norm);
|
|
333
|
+
if (!list.includes(el)) {
|
|
334
|
+
list.push(el);
|
|
335
|
+
}
|
|
336
|
+
this.liveMap.set(norm, list[0]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_unregisterNode(id, el) {
|
|
340
|
+
if (!id) return;
|
|
341
|
+
const norm = normalizeId(id);
|
|
342
|
+
if (this.liveNodes.has(norm)) {
|
|
343
|
+
const list = this.liveNodes.get(norm);
|
|
344
|
+
const idx = list.indexOf(el);
|
|
345
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
346
|
+
if (list.length === 0) {
|
|
347
|
+
this.liveNodes.delete(norm);
|
|
348
|
+
this.liveMap.delete(norm);
|
|
349
|
+
} else {
|
|
350
|
+
this.liveMap.set(norm, list[0]);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
this.liveMap.delete(norm);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_unregisterElementHierarchy(el) {
|
|
358
|
+
if (!el) return;
|
|
359
|
+
const compId = el.getAttribute?.('wdl-comp');
|
|
360
|
+
if (compId) {
|
|
361
|
+
this._unregisterNode(compId, el);
|
|
362
|
+
}
|
|
363
|
+
if (Array.isArray(el.children)) {
|
|
364
|
+
for (const child of el.children) {
|
|
365
|
+
this._unregisterElementHierarchy(child);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// -----------------------------------------------------------------------
|
|
371
|
+
// Data and Scope Helpers
|
|
372
|
+
// -----------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
_getComponentData() {
|
|
375
|
+
return (
|
|
376
|
+
this.component.data?.get?.() ??
|
|
377
|
+
this.component.getSnapshot?.()?.data ??
|
|
378
|
+
{}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_getComponentAttrMap() {
|
|
383
|
+
return (
|
|
384
|
+
this.component.attr?.list?.() ??
|
|
385
|
+
this.component.getSnapshot?.()?.attr ??
|
|
386
|
+
{}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_getComponentRegistry() {
|
|
391
|
+
return (
|
|
392
|
+
this.component.registry?.get?.() ??
|
|
393
|
+
this.component.getSnapshot?.()?.registry ??
|
|
394
|
+
{}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
_getRegistryClasses(id, tag, dataScope, el) {
|
|
399
|
+
const registry = this._getComponentRegistry();
|
|
400
|
+
if (!registry || typeof registry !== 'object') return '';
|
|
401
|
+
|
|
402
|
+
const entry =
|
|
403
|
+
(id ? registry[id] || registry['.' + id] : null) ||
|
|
404
|
+
(tag ? registry[tag] : null);
|
|
405
|
+
|
|
406
|
+
if (!entry) return '';
|
|
407
|
+
|
|
408
|
+
if (typeof entry === 'string') {
|
|
409
|
+
return resolveStr(entry, dataScope);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (typeof entry !== 'object' || entry === null) return '';
|
|
413
|
+
|
|
414
|
+
const classes = [];
|
|
415
|
+
|
|
416
|
+
// Base utility class string
|
|
417
|
+
if (typeof entry.base === 'string' && entry.base) {
|
|
418
|
+
classes.push(resolveStr(entry.base, dataScope));
|
|
419
|
+
} else if (typeof entry.class === 'string' && entry.class) {
|
|
420
|
+
classes.push(resolveStr(entry.class, dataScope));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Active Variant classes
|
|
424
|
+
const activeVariant =
|
|
425
|
+
el?.dataset?.variant ||
|
|
426
|
+
el?.getAttribute?.('data-variant') ||
|
|
427
|
+
this.component.variant?.get?.(id) ||
|
|
428
|
+
entry.defaultVariant ||
|
|
429
|
+
null;
|
|
430
|
+
|
|
431
|
+
if (activeVariant && entry.variants && typeof entry.variants === 'object') {
|
|
432
|
+
const vVal = entry.variants[activeVariant];
|
|
433
|
+
if (typeof vVal === 'string' && vVal) {
|
|
434
|
+
classes.push(resolveStr(vVal, dataScope));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// States (e.g. hover, focus)
|
|
439
|
+
if (entry.states && typeof entry.states === 'object') {
|
|
440
|
+
for (const [state, cls] of Object.entries(entry.states)) {
|
|
441
|
+
if (typeof cls === 'string' && cls) {
|
|
442
|
+
const resolved = resolveStr(cls, dataScope);
|
|
443
|
+
const formatted = resolved
|
|
444
|
+
.split(/\s+/)
|
|
445
|
+
.filter(Boolean)
|
|
446
|
+
.map((c) => (c.includes(':') ? c : `${state}:${c}`))
|
|
447
|
+
.join(' ');
|
|
448
|
+
classes.push(formatted);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Breakpoints (e.g. md, lg)
|
|
454
|
+
if (entry.breakpoints && typeof entry.breakpoints === 'object') {
|
|
455
|
+
for (const [bp, cls] of Object.entries(entry.breakpoints)) {
|
|
456
|
+
if (typeof cls === 'string' && cls) {
|
|
457
|
+
const resolved = resolveStr(cls, dataScope);
|
|
458
|
+
const formatted = resolved
|
|
459
|
+
.split(/\s+/)
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.map((c) => (c.includes(':') ? c : `${bp}:${c}`))
|
|
462
|
+
.join(' ');
|
|
463
|
+
classes.push(formatted);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return classes.filter(Boolean).join(' ');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_resolveLoopItems(repeator, dataScope) {
|
|
472
|
+
if (!repeator) return [];
|
|
473
|
+
if (/^\d+$/.test(String(repeator))) {
|
|
474
|
+
const count = parseInt(repeator, 10);
|
|
475
|
+
return Array.from({ length: Math.max(0, count) }, (_, idx) => ({ _index: idx }));
|
|
476
|
+
}
|
|
477
|
+
const resolved = resolvePath(dataScope, repeator);
|
|
478
|
+
if (Array.isArray(resolved)) return resolved;
|
|
479
|
+
if (typeof resolved === 'number') {
|
|
480
|
+
return Array.from({ length: Math.max(0, resolved) }, (_, idx) => ({ _index: idx }));
|
|
481
|
+
}
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_createItemScope(item, index, baseData) {
|
|
486
|
+
const data = baseData || this._getComponentData();
|
|
487
|
+
if (typeof item === 'object' && item !== null) {
|
|
488
|
+
return { ...data, ...item, item, _index: index };
|
|
489
|
+
}
|
|
490
|
+
return { ...data, value: item, item, _index: index };
|
|
491
|
+
}
|
|
492
|
+
|
|
198
493
|
// -----------------------------------------------------------------------
|
|
199
494
|
// Initial mount
|
|
200
495
|
// -----------------------------------------------------------------------
|
|
@@ -202,12 +497,18 @@ export class WdlDom {
|
|
|
202
497
|
_mount() {
|
|
203
498
|
this.container.replaceChildren();
|
|
204
499
|
this.liveMap.clear();
|
|
500
|
+
this.liveNodes.clear();
|
|
501
|
+
this._loopBindings = [];
|
|
205
502
|
|
|
206
503
|
let tree;
|
|
207
504
|
try {
|
|
208
|
-
tree =
|
|
209
|
-
|
|
210
|
-
|
|
505
|
+
tree =
|
|
506
|
+
typeof this.component.layers?.tree === 'function'
|
|
507
|
+
? this.component.layers.tree()
|
|
508
|
+
: parseLayersSimple(
|
|
509
|
+
this.component.layers?.list?.() ??
|
|
510
|
+
this.component.getSnapshot?.()?.layers
|
|
511
|
+
);
|
|
211
512
|
} catch (e) {
|
|
212
513
|
this._log('warn', 'tree() failed, falling back to simple parse', e);
|
|
213
514
|
const layers = this.component.getSnapshot?.()?.layers ?? '';
|
|
@@ -216,18 +517,10 @@ export class WdlDom {
|
|
|
216
517
|
|
|
217
518
|
if (!Array.isArray(tree)) tree = [];
|
|
218
519
|
|
|
219
|
-
|
|
220
|
-
const el = this._createElementFromNode(node);
|
|
221
|
-
this.container.appendChild(el);
|
|
222
|
-
}
|
|
520
|
+
const compData = this._getComponentData();
|
|
223
521
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.component.attr?.list?.() ??
|
|
227
|
-
this.component.getSnapshot?.()?.attr ??
|
|
228
|
-
{};
|
|
229
|
-
for (const [sel, props] of Object.entries(attrMap)) {
|
|
230
|
-
this._applyAttrs(normalizeId(sel), props);
|
|
522
|
+
for (const node of tree) {
|
|
523
|
+
this._mountLayerNode(this.container, node, compData);
|
|
231
524
|
}
|
|
232
525
|
|
|
233
526
|
// Apply variant if present on root
|
|
@@ -236,13 +529,50 @@ export class WdlDom {
|
|
|
236
529
|
// Registry styles
|
|
237
530
|
this._syncRegistryStyles();
|
|
238
531
|
|
|
239
|
-
this._log('mount', `mounted ${this.liveMap.size}
|
|
532
|
+
this._log('mount', `mounted ${this.liveMap.size} semantic keys, ${this._loopBindings.length} loops`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Mount a layer node into a parent container/element.
|
|
537
|
+
* Handles loop expansion vs single element mounting.
|
|
538
|
+
*/
|
|
539
|
+
_mountLayerNode(parentEl, node, dataScope) {
|
|
540
|
+
if (node.repeator) {
|
|
541
|
+
const items = this._resolveLoopItems(node.repeator, dataScope);
|
|
542
|
+
const elements = [];
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < items.length; i++) {
|
|
545
|
+
const itemScope = this._createItemScope(items[i], i, dataScope);
|
|
546
|
+
const el = this._createSingleElement(node, itemScope, i);
|
|
547
|
+
parentEl.appendChild(el);
|
|
548
|
+
elements.push(el);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Create comment anchor to keep track of loop boundary
|
|
552
|
+
let anchor = null;
|
|
553
|
+
if (typeof document.createComment === 'function') {
|
|
554
|
+
anchor = document.createComment(`wdl-loop:${node.repeator}`);
|
|
555
|
+
parentEl.appendChild(anchor);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this._loopBindings.push({
|
|
559
|
+
parentEl,
|
|
560
|
+
node,
|
|
561
|
+
repeator: node.repeator,
|
|
562
|
+
elements,
|
|
563
|
+
anchor,
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const el = this._createSingleElement(node, dataScope);
|
|
569
|
+
parentEl.appendChild(el);
|
|
240
570
|
}
|
|
241
571
|
|
|
242
572
|
/**
|
|
243
|
-
*
|
|
573
|
+
* Create a single DOM element for a layer node with item scope and attributes.
|
|
244
574
|
*/
|
|
245
|
-
|
|
575
|
+
_createSingleElement(node, dataScope, index) {
|
|
246
576
|
const tag = (node.tag || 'div').toLowerCase();
|
|
247
577
|
const id = normalizeId(node.semanticId || node.id || '');
|
|
248
578
|
const el = document.createElement(tag);
|
|
@@ -250,17 +580,146 @@ export class WdlDom {
|
|
|
250
580
|
if (id) {
|
|
251
581
|
el.classList.add(id);
|
|
252
582
|
el.setAttribute('wdl-comp', id);
|
|
253
|
-
this.
|
|
583
|
+
this._registerNode(id, el);
|
|
254
584
|
}
|
|
255
585
|
|
|
586
|
+
const effectiveIndex = index !== undefined ? index : dataScope?._index;
|
|
587
|
+
if (effectiveIndex !== undefined) {
|
|
588
|
+
const idxStr = String(effectiveIndex);
|
|
589
|
+
el.setAttribute('data-wdl-index', idxStr);
|
|
590
|
+
if (el.dataset) el.dataset.wdlIndex = idxStr;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this._applyNodeAttrs(el, node, dataScope);
|
|
594
|
+
|
|
256
595
|
if (Array.isArray(node.children)) {
|
|
257
596
|
for (const child of node.children) {
|
|
258
|
-
|
|
597
|
+
this._mountLayerNode(el, child, dataScope);
|
|
259
598
|
}
|
|
260
599
|
}
|
|
600
|
+
|
|
261
601
|
return el;
|
|
262
602
|
}
|
|
263
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Apply matching attributes to an element using resolved dataScope.
|
|
606
|
+
*/
|
|
607
|
+
_applyNodeAttrs(el, node, dataScope) {
|
|
608
|
+
const attrMap = this._getComponentAttrMap();
|
|
609
|
+
const id = normalizeId(node.semanticId || node.id || '');
|
|
610
|
+
const tag = (node.tag || 'div').toLowerCase();
|
|
611
|
+
|
|
612
|
+
// Priority: tag fallback -> semanticId (.id or id)
|
|
613
|
+
const tagAttrs = attrMap[tag] || {};
|
|
614
|
+
const idDotAttrs = id ? attrMap['.' + id] || {} : {};
|
|
615
|
+
const idAttrs = id ? attrMap[id] || {} : {};
|
|
616
|
+
|
|
617
|
+
const merged = { ...tagAttrs, ...idDotAttrs, ...idAttrs };
|
|
618
|
+
const resolved = resolveAll(merged, dataScope);
|
|
619
|
+
|
|
620
|
+
// Merge semantic ID, registry classes, and attr classes
|
|
621
|
+
const registryClasses = this._getRegistryClasses(id, tag, dataScope, el);
|
|
622
|
+
const attrClasses = typeof resolved.class === 'string' ? resolved.class : '';
|
|
623
|
+
|
|
624
|
+
const combinedClasses = [
|
|
625
|
+
id,
|
|
626
|
+
registryClasses,
|
|
627
|
+
attrClasses,
|
|
628
|
+
]
|
|
629
|
+
.filter(Boolean)
|
|
630
|
+
.join(' ')
|
|
631
|
+
.split(/\s+/)
|
|
632
|
+
.filter(Boolean);
|
|
633
|
+
|
|
634
|
+
const uniqueClasses = Array.from(new Set(combinedClasses)).join(' ');
|
|
635
|
+
|
|
636
|
+
this._applyPropsToElement(el, id, { ...resolved, class: uniqueClasses });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
_refreshElementClasses(el, id) {
|
|
640
|
+
if (!el) return;
|
|
641
|
+
const normId = normalizeId(id || el.getAttribute('wdl-comp') || '');
|
|
642
|
+
const compData = this._getComponentData();
|
|
643
|
+
const attrMap = this._getComponentAttrMap();
|
|
644
|
+
const tag = (el.tagName || 'div').toLowerCase();
|
|
645
|
+
|
|
646
|
+
const idx = el.getAttribute('data-wdl-index');
|
|
647
|
+
let scope = compData;
|
|
648
|
+
if (idx !== null && idx !== undefined) {
|
|
649
|
+
const indexNum = parseInt(idx, 10);
|
|
650
|
+
const loop = this._findLoopForElement(el);
|
|
651
|
+
if (loop) {
|
|
652
|
+
const items = resolvePath(compData, loop.repeator);
|
|
653
|
+
if (Array.isArray(items) && items[indexNum] !== undefined) {
|
|
654
|
+
scope = this._createItemScope(items[indexNum], indexNum, compData);
|
|
655
|
+
} else {
|
|
656
|
+
scope = { ...compData, _index: indexNum };
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
scope = { ...compData, _index: indexNum };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const tagAttrs = attrMap[tag] || {};
|
|
664
|
+
const idDotAttrs = normId ? attrMap['.' + normId] || {} : {};
|
|
665
|
+
const idAttrs = normId ? attrMap[normId] || {} : {};
|
|
666
|
+
const merged = { ...tagAttrs, ...idDotAttrs, ...idAttrs };
|
|
667
|
+
const resolved = resolveAll(merged, scope);
|
|
668
|
+
|
|
669
|
+
const registryClasses = this._getRegistryClasses(normId, tag, scope, el);
|
|
670
|
+
const attrClasses = typeof resolved.class === 'string' ? resolved.class : '';
|
|
671
|
+
|
|
672
|
+
const combinedClasses = [
|
|
673
|
+
normId,
|
|
674
|
+
registryClasses,
|
|
675
|
+
attrClasses,
|
|
676
|
+
]
|
|
677
|
+
.filter(Boolean)
|
|
678
|
+
.join(' ')
|
|
679
|
+
.split(/\s+/)
|
|
680
|
+
.filter(Boolean);
|
|
681
|
+
|
|
682
|
+
el.className = Array.from(new Set(combinedClasses)).join(' ');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
_refreshAllElementClasses() {
|
|
686
|
+
for (const [id, nodes] of this.liveNodes) {
|
|
687
|
+
for (const el of nodes) {
|
|
688
|
+
this._refreshElementClasses(el, id);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Directly apply a resolved properties dictionary onto an element.
|
|
695
|
+
*/
|
|
696
|
+
_applyPropsToElement(el, id, props) {
|
|
697
|
+
if (!props || typeof props !== 'object') return;
|
|
698
|
+
|
|
699
|
+
if (props.text !== undefined) {
|
|
700
|
+
el.textContent = String(props.text);
|
|
701
|
+
}
|
|
702
|
+
if (props.html !== undefined) {
|
|
703
|
+
el.innerHTML = String(props.html);
|
|
704
|
+
}
|
|
705
|
+
if (props.class !== undefined) {
|
|
706
|
+
el.className = String(props.class).trim();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
for (const [key, value] of Object.entries(props)) {
|
|
710
|
+
if (ATTR_SKIP.has(key)) continue;
|
|
711
|
+
if (key === 'style' && value && typeof value === 'object') {
|
|
712
|
+
Object.assign(el.style, value);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (value == null) {
|
|
716
|
+
el.removeAttribute(key);
|
|
717
|
+
} else {
|
|
718
|
+
el.setAttribute(key, String(value));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
264
723
|
// -----------------------------------------------------------------------
|
|
265
724
|
// State event binding
|
|
266
725
|
// -----------------------------------------------------------------------
|
|
@@ -274,7 +733,6 @@ export class WdlDom {
|
|
|
274
733
|
|
|
275
734
|
const handler = (event) => this._onStateEvent(event);
|
|
276
735
|
|
|
277
|
-
// All events documented by @ruledwdl/state
|
|
278
736
|
const types = [
|
|
279
737
|
'layers:change',
|
|
280
738
|
'attr:change',
|
|
@@ -285,7 +743,6 @@ export class WdlDom {
|
|
|
285
743
|
|
|
286
744
|
for (const type of types) {
|
|
287
745
|
const off = c.on(type, handler);
|
|
288
|
-
// state.on may return unsubscribe fn or nothing
|
|
289
746
|
if (typeof off === 'function') this._unsubs.push(off);
|
|
290
747
|
else this._unsubs.push(() => c.off?.(type, handler));
|
|
291
748
|
}
|
|
@@ -321,68 +778,54 @@ export class WdlDom {
|
|
|
321
778
|
|
|
322
779
|
// -----------------------------------------------------------------------
|
|
323
780
|
// layers:change
|
|
324
|
-
// actions: set | append | before | after | wrap | remove | update
|
|
781
|
+
// actions: set | append | prepend | before | after | wrap | unwrap | move | remove | update
|
|
325
782
|
// -----------------------------------------------------------------------
|
|
326
783
|
|
|
327
784
|
_handleLayers(event) {
|
|
328
785
|
const { action, targetId, payload } = event;
|
|
329
786
|
const id = normalizeId(targetId);
|
|
787
|
+
const compData = this._getComponentData();
|
|
330
788
|
|
|
331
789
|
switch (action) {
|
|
332
790
|
case 'set': {
|
|
333
|
-
// Full layers replacement → remount
|
|
334
791
|
this._mount();
|
|
335
792
|
break;
|
|
336
793
|
}
|
|
337
794
|
case 'append': {
|
|
338
|
-
|
|
339
|
-
const parentEl = this.liveMap.get(id);
|
|
795
|
+
const parentEl = this.getNode(id);
|
|
340
796
|
if (!parentEl) {
|
|
341
797
|
this._log('warn', `append: parent "${id}" not in liveMap — remounting`);
|
|
342
798
|
this._mount();
|
|
343
799
|
return;
|
|
344
800
|
}
|
|
345
801
|
const token = parseLayerToken(payload);
|
|
346
|
-
|
|
347
|
-
tag: token.tag,
|
|
348
|
-
semanticId: token.semanticId,
|
|
349
|
-
children: [],
|
|
350
|
-
});
|
|
351
|
-
parentEl.appendChild(el);
|
|
802
|
+
this._mountLayerNode(parentEl, token, compData);
|
|
352
803
|
this._log('op', `append <${token.tag}.${token.semanticId}> → #${id}`);
|
|
353
804
|
break;
|
|
354
805
|
}
|
|
355
806
|
case 'prepend': {
|
|
356
|
-
const parentEl = this.
|
|
807
|
+
const parentEl = this.getNode(id);
|
|
357
808
|
if (!parentEl) {
|
|
358
809
|
this._log('warn', `prepend: parent "${id}" not in liveMap — remounting`);
|
|
359
810
|
this._mount();
|
|
360
811
|
return;
|
|
361
812
|
}
|
|
362
813
|
const token = parseLayerToken(payload);
|
|
363
|
-
const el = this.
|
|
364
|
-
tag: token.tag,
|
|
365
|
-
semanticId: token.semanticId,
|
|
366
|
-
children: [],
|
|
367
|
-
});
|
|
814
|
+
const el = this._createSingleElement(token, compData);
|
|
368
815
|
parentEl.insertBefore(el, parentEl.firstChild);
|
|
369
816
|
this._log('op', `prepend <${token.tag}.${token.semanticId}> → #${id}`);
|
|
370
817
|
break;
|
|
371
818
|
}
|
|
372
819
|
case 'before':
|
|
373
820
|
case 'after': {
|
|
374
|
-
const targetEl = this.
|
|
821
|
+
const targetEl = this.getNode(id);
|
|
375
822
|
if (!targetEl || !targetEl.parentNode) {
|
|
376
823
|
this._log('warn', `${action}: target "${id}" missing — remounting`);
|
|
377
824
|
this._mount();
|
|
378
825
|
return;
|
|
379
826
|
}
|
|
380
827
|
const token = parseLayerToken(payload);
|
|
381
|
-
const el = this.
|
|
382
|
-
tag: token.tag,
|
|
383
|
-
semanticId: token.semanticId,
|
|
384
|
-
children: [],
|
|
385
|
-
});
|
|
828
|
+
const el = this._createSingleElement(token, compData);
|
|
386
829
|
if (action === 'before') {
|
|
387
830
|
targetEl.parentNode.insertBefore(el, targetEl);
|
|
388
831
|
} else {
|
|
@@ -392,26 +835,21 @@ export class WdlDom {
|
|
|
392
835
|
break;
|
|
393
836
|
}
|
|
394
837
|
case 'wrap': {
|
|
395
|
-
|
|
396
|
-
const targetEl = this.liveMap.get(id);
|
|
838
|
+
const targetEl = this.getNode(id);
|
|
397
839
|
if (!targetEl || !targetEl.parentNode) {
|
|
398
840
|
this._log('warn', `wrap: target "${id}" missing — remounting`);
|
|
399
841
|
this._mount();
|
|
400
842
|
return;
|
|
401
843
|
}
|
|
402
844
|
const token = parseLayerToken(payload);
|
|
403
|
-
const wrapper = this.
|
|
404
|
-
tag: token.tag,
|
|
405
|
-
semanticId: token.semanticId,
|
|
406
|
-
children: [],
|
|
407
|
-
});
|
|
845
|
+
const wrapper = this._createSingleElement(token, compData);
|
|
408
846
|
targetEl.parentNode.insertBefore(wrapper, targetEl);
|
|
409
847
|
wrapper.appendChild(targetEl);
|
|
410
848
|
this._log('op', `wrap #${id} with <${token.tag}.${token.semanticId}>`);
|
|
411
849
|
break;
|
|
412
850
|
}
|
|
413
851
|
case 'unwrap': {
|
|
414
|
-
const wrapperEl = this.
|
|
852
|
+
const wrapperEl = this.getNode(id);
|
|
415
853
|
if (!wrapperEl || !wrapperEl.parentNode) {
|
|
416
854
|
this._log('warn', `unwrap: target "${id}" missing — remounting`);
|
|
417
855
|
this._mount();
|
|
@@ -421,16 +859,16 @@ export class WdlDom {
|
|
|
421
859
|
while (wrapperEl.firstChild) {
|
|
422
860
|
parent.insertBefore(wrapperEl.firstChild, wrapperEl);
|
|
423
861
|
}
|
|
862
|
+
this._unregisterElementHierarchy(wrapperEl);
|
|
424
863
|
wrapperEl.remove();
|
|
425
|
-
this.liveMap.delete(id);
|
|
426
864
|
this._log('op', `unwrap #${id}`);
|
|
427
865
|
break;
|
|
428
866
|
}
|
|
429
867
|
case 'move': {
|
|
430
|
-
const sourceEl = this.
|
|
868
|
+
const sourceEl = this.getNode(id);
|
|
431
869
|
const { targetSemanticId, position } = payload || {};
|
|
432
870
|
const targetId = normalizeId(targetSemanticId);
|
|
433
|
-
const targetEl = this.
|
|
871
|
+
const targetEl = this.getNode(targetId);
|
|
434
872
|
|
|
435
873
|
if (!sourceEl || !targetEl) {
|
|
436
874
|
this._log('warn', `move: source "${id}" or target "${targetId}" missing — remounting`);
|
|
@@ -449,23 +887,22 @@ export class WdlDom {
|
|
|
449
887
|
break;
|
|
450
888
|
}
|
|
451
889
|
case 'remove': {
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
890
|
+
const nodes = this.getLiveNodes(id);
|
|
891
|
+
if (nodes.length === 0) {
|
|
454
892
|
this._log('warn', `remove: #${id} not in liveMap`);
|
|
455
893
|
return;
|
|
456
894
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
895
|
+
for (const el of nodes) {
|
|
896
|
+
this._unregisterElementHierarchy(el);
|
|
897
|
+
el.remove();
|
|
460
898
|
}
|
|
461
|
-
|
|
462
|
-
this.
|
|
899
|
+
// Also cleanup loop bindings associated with this semantic ID
|
|
900
|
+
this._loopBindings = this._loopBindings.filter((l) => l.node.semanticId !== id);
|
|
463
901
|
this._log('op', `remove #${id}`);
|
|
464
902
|
break;
|
|
465
903
|
}
|
|
466
904
|
case 'update': {
|
|
467
|
-
|
|
468
|
-
const el = this.liveMap.get(id);
|
|
905
|
+
const el = this.getNode(id);
|
|
469
906
|
if (!el) {
|
|
470
907
|
this._log('warn', `update: #${id} missing — remounting`);
|
|
471
908
|
this._mount();
|
|
@@ -473,28 +910,25 @@ export class WdlDom {
|
|
|
473
910
|
}
|
|
474
911
|
const patch = payload || {};
|
|
475
912
|
if (patch.tag && patch.tag.toLowerCase() !== el.tagName.toLowerCase()) {
|
|
476
|
-
// Tag change requires recreate
|
|
477
913
|
const next = document.createElement(String(patch.tag).toLowerCase());
|
|
478
|
-
// copy attributes & children
|
|
479
914
|
for (const attr of el.attributes) next.setAttribute(attr.name, attr.value);
|
|
480
915
|
while (el.firstChild) next.appendChild(el.firstChild);
|
|
481
916
|
el.parentNode?.replaceChild(next, el);
|
|
482
|
-
this.
|
|
483
|
-
|
|
484
|
-
|
|
917
|
+
this._unregisterNode(id, el);
|
|
918
|
+
const finalId = patch.semanticId ? normalizeId(patch.semanticId) : id;
|
|
919
|
+
if (patch.semanticId && finalId !== id) {
|
|
485
920
|
next.classList.remove(id);
|
|
486
|
-
next.classList.add(
|
|
487
|
-
next.setAttribute('wdl-comp',
|
|
488
|
-
this.liveMap.delete(id);
|
|
489
|
-
this.liveMap.set(newId, next);
|
|
921
|
+
next.classList.add(finalId);
|
|
922
|
+
next.setAttribute('wdl-comp', finalId);
|
|
490
923
|
}
|
|
924
|
+
this._registerNode(finalId, next);
|
|
491
925
|
} else if (patch.semanticId && normalizeId(patch.semanticId) !== id) {
|
|
492
926
|
const newId = normalizeId(patch.semanticId);
|
|
493
927
|
el.classList.remove(id);
|
|
494
928
|
el.classList.add(newId);
|
|
495
929
|
el.setAttribute('wdl-comp', newId);
|
|
496
|
-
this.
|
|
497
|
-
this.
|
|
930
|
+
this._unregisterNode(id, el);
|
|
931
|
+
this._registerNode(newId, el);
|
|
498
932
|
}
|
|
499
933
|
this._log('op', `update #${id}`, patch);
|
|
500
934
|
break;
|
|
@@ -515,89 +949,114 @@ export class WdlDom {
|
|
|
515
949
|
const id = normalizeId(targetId);
|
|
516
950
|
|
|
517
951
|
if (action === 'set' || action === 'update') {
|
|
518
|
-
|
|
519
|
-
|
|
952
|
+
const nodes = this.getLiveNodes(id);
|
|
953
|
+
if (nodes.length === 0) {
|
|
954
|
+
this._log('warn', `attr: #${id} not in liveMap`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const compData = this._getComponentData();
|
|
958
|
+
for (const el of nodes) {
|
|
959
|
+
const idx = el.getAttribute('data-wdl-index');
|
|
960
|
+
let scope = compData;
|
|
961
|
+
if (idx !== null && idx !== undefined) {
|
|
962
|
+
const indexNum = parseInt(idx, 10);
|
|
963
|
+
const loop = this._findLoopForElement(el);
|
|
964
|
+
if (loop) {
|
|
965
|
+
const items = resolvePath(compData, loop.repeator);
|
|
966
|
+
if (Array.isArray(items) && items[indexNum] !== undefined) {
|
|
967
|
+
scope = this._createItemScope(items[indexNum], indexNum, compData);
|
|
968
|
+
} else {
|
|
969
|
+
scope = { ...compData, _index: indexNum };
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
scope = { ...compData, _index: indexNum };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const resolved = resolveAll(payload || {}, scope);
|
|
976
|
+
this._applyPropsToElement(el, id, resolved);
|
|
977
|
+
}
|
|
520
978
|
this._log('op', `attr.${action} #${id}`, payload);
|
|
521
979
|
return;
|
|
522
980
|
}
|
|
523
981
|
|
|
524
982
|
if (action === 'remove') {
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
527
|
-
const attrKey = payload;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
this._log('op', `attr.remove html from #${id}`);
|
|
542
|
-
} else {
|
|
543
|
-
el.removeAttribute(attrKey);
|
|
544
|
-
this._log('op', `attr.remove ${attrKey} from #${id}`);
|
|
983
|
+
const nodes = this.getLiveNodes(id);
|
|
984
|
+
if (nodes.length === 0) return;
|
|
985
|
+
const attrKey = payload;
|
|
986
|
+
for (const el of nodes) {
|
|
987
|
+
if (attrKey == null || attrKey === '') {
|
|
988
|
+
el.textContent = '';
|
|
989
|
+
el.className = id;
|
|
990
|
+
} else if (attrKey === 'text') {
|
|
991
|
+
el.textContent = '';
|
|
992
|
+
} else if (attrKey === 'class') {
|
|
993
|
+
el.className = id;
|
|
994
|
+
} else if (attrKey === 'html') {
|
|
995
|
+
el.innerHTML = '';
|
|
996
|
+
} else {
|
|
997
|
+
el.removeAttribute(attrKey);
|
|
998
|
+
}
|
|
545
999
|
}
|
|
1000
|
+
this._log('op', `attr.remove ${attrKey || 'all'} from #${id}`);
|
|
546
1001
|
}
|
|
547
1002
|
}
|
|
548
1003
|
|
|
549
|
-
/**
|
|
550
|
-
* Apply attribute object onto live element.
|
|
551
|
-
* Supports: text, class, html, data-*, style object, arbitrary attrs.
|
|
552
|
-
*/
|
|
553
1004
|
_applyAttrs(id, props) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (!el) {
|
|
1005
|
+
const nodes = this.getLiveNodes(id);
|
|
1006
|
+
if (nodes.length === 0) {
|
|
557
1007
|
this._log('warn', `attr: #${id} not in liveMap`);
|
|
558
1008
|
return;
|
|
559
1009
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
Object.assign(el.style, value);
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
if (value == null) {
|
|
579
|
-
el.removeAttribute(key);
|
|
580
|
-
} else {
|
|
581
|
-
el.setAttribute(key, String(value));
|
|
1010
|
+
const compData = this._getComponentData();
|
|
1011
|
+
for (const el of nodes) {
|
|
1012
|
+
const idx = el.getAttribute('data-wdl-index');
|
|
1013
|
+
let scope = compData;
|
|
1014
|
+
if (idx !== null && idx !== undefined) {
|
|
1015
|
+
const indexNum = parseInt(idx, 10);
|
|
1016
|
+
const loop = this._findLoopForElement(el);
|
|
1017
|
+
if (loop) {
|
|
1018
|
+
const items = resolvePath(compData, loop.repeator);
|
|
1019
|
+
if (Array.isArray(items) && items[indexNum] !== undefined) {
|
|
1020
|
+
scope = this._createItemScope(items[indexNum], indexNum, compData);
|
|
1021
|
+
} else {
|
|
1022
|
+
scope = { ...compData, _index: indexNum };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
582
1025
|
}
|
|
1026
|
+
const resolved = resolveAll(props || {}, scope);
|
|
1027
|
+
this._applyPropsToElement(el, id, resolved);
|
|
583
1028
|
}
|
|
584
1029
|
}
|
|
585
1030
|
|
|
1031
|
+
_findLoopForElement(el) {
|
|
1032
|
+
return this._loopBindings.find((l) => l.elements.includes(el)) || null;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
586
1035
|
// -----------------------------------------------------------------------
|
|
587
1036
|
// data:change
|
|
588
|
-
// actions: set | remove
|
|
589
|
-
//
|
|
1037
|
+
// actions: set | update | remove
|
|
1038
|
+
// Surgically reconciles repeated loop items & per-item template bindings
|
|
590
1039
|
// -----------------------------------------------------------------------
|
|
591
1040
|
|
|
592
1041
|
_handleData(event) {
|
|
593
1042
|
const { action, targetId, payload } = event;
|
|
594
|
-
// targetId is the path (e.g. "user.name"), payload is the value
|
|
595
1043
|
const path = targetId || '';
|
|
1044
|
+
const compData = this._getComponentData();
|
|
1045
|
+
|
|
1046
|
+
// 1. Surgically reconcile all matching loop bindings
|
|
1047
|
+
this._reconcileLoops(path, compData);
|
|
1048
|
+
|
|
1049
|
+
// 2. Reconcile non-loop elements bound to this path or full data
|
|
1050
|
+
this._reconcileStaticDataBindings(path, compData);
|
|
1051
|
+
|
|
1052
|
+
// 3. Optional onDataBind callback hook for external consumers
|
|
596
1053
|
if (typeof this.onDataBind === 'function') {
|
|
597
|
-
// Let consumer decide how data maps to DOM
|
|
598
|
-
// We pass the root container + path + value
|
|
599
1054
|
try {
|
|
600
|
-
this.onDataBind(
|
|
1055
|
+
this.onDataBind(
|
|
1056
|
+
this.container,
|
|
1057
|
+
path,
|
|
1058
|
+
action === 'remove' ? undefined : payload
|
|
1059
|
+
);
|
|
601
1060
|
} catch (e) {
|
|
602
1061
|
this._log('warn', 'onDataBind error', e);
|
|
603
1062
|
}
|
|
@@ -605,6 +1064,113 @@ export class WdlDom {
|
|
|
605
1064
|
this._log('op', `data.${action} ${path}`, action === 'remove' ? undefined : payload);
|
|
606
1065
|
}
|
|
607
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* Reconcile loop children surgically on data change.
|
|
1069
|
+
*/
|
|
1070
|
+
_reconcileLoops(path, compData) {
|
|
1071
|
+
for (const loop of this._loopBindings) {
|
|
1072
|
+
const matches =
|
|
1073
|
+
!path ||
|
|
1074
|
+
loop.repeator === path ||
|
|
1075
|
+
path.startsWith(loop.repeator + '.') ||
|
|
1076
|
+
loop.repeator.startsWith(path + '.');
|
|
1077
|
+
|
|
1078
|
+
if (!matches) continue;
|
|
1079
|
+
|
|
1080
|
+
const items = this._resolveLoopItems(loop.repeator, compData);
|
|
1081
|
+
const nextCount = items.length;
|
|
1082
|
+
const prevCount = loop.elements.length;
|
|
1083
|
+
const minCount = Math.min(prevCount, nextCount);
|
|
1084
|
+
|
|
1085
|
+
// 1. Update retained rows in place
|
|
1086
|
+
for (let i = 0; i < minCount; i++) {
|
|
1087
|
+
const el = loop.elements[i];
|
|
1088
|
+
const itemScope = this._createItemScope(items[i], i, compData);
|
|
1089
|
+
el.setAttribute('data-wdl-index', String(i));
|
|
1090
|
+
if (el.dataset) el.dataset.wdlIndex = String(i);
|
|
1091
|
+
this._updateElementSubtree(el, loop.node, itemScope, i);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// 2. Insert newly added rows
|
|
1095
|
+
if (nextCount > prevCount) {
|
|
1096
|
+
for (let i = prevCount; i < nextCount; i++) {
|
|
1097
|
+
const itemScope = this._createItemScope(items[i], i, compData);
|
|
1098
|
+
const newEl = this._createSingleElement(loop.node, itemScope, i);
|
|
1099
|
+
if (loop.anchor && loop.anchor.parentNode === loop.parentEl) {
|
|
1100
|
+
loop.parentEl.insertBefore(newEl, loop.anchor);
|
|
1101
|
+
} else if (loop.elements.length > 0) {
|
|
1102
|
+
const lastEl = loop.elements[loop.elements.length - 1];
|
|
1103
|
+
loop.parentEl.insertBefore(newEl, lastEl.nextSibling);
|
|
1104
|
+
} else {
|
|
1105
|
+
loop.parentEl.appendChild(newEl);
|
|
1106
|
+
}
|
|
1107
|
+
loop.elements.push(newEl);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// 3. Remove deleted rows
|
|
1112
|
+
if (nextCount < prevCount) {
|
|
1113
|
+
for (let i = prevCount - 1; i >= nextCount; i--) {
|
|
1114
|
+
const delEl = loop.elements[i];
|
|
1115
|
+
this._unregisterElementHierarchy(delEl);
|
|
1116
|
+
delEl.remove();
|
|
1117
|
+
loop.elements.pop();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Update an element and its non-loop children with new itemScope.
|
|
1125
|
+
*/
|
|
1126
|
+
_updateElementSubtree(el, node, itemScope, index) {
|
|
1127
|
+
this._applyNodeAttrs(el, node, itemScope);
|
|
1128
|
+
|
|
1129
|
+
if (Array.isArray(node.children) && Array.isArray(el.children)) {
|
|
1130
|
+
let childElIdx = 0;
|
|
1131
|
+
for (const childNode of node.children) {
|
|
1132
|
+
if (childNode.repeator) {
|
|
1133
|
+
// Nested loop: find its binding and reconcile
|
|
1134
|
+
const childLoop = this._loopBindings.find(
|
|
1135
|
+
(l) => l.parentEl === el && l.node === childNode
|
|
1136
|
+
);
|
|
1137
|
+
if (childLoop) {
|
|
1138
|
+
this._reconcileLoops(childLoop.repeator, itemScope);
|
|
1139
|
+
}
|
|
1140
|
+
} else if (el.children[childElIdx]) {
|
|
1141
|
+
this._updateElementSubtree(
|
|
1142
|
+
el.children[childElIdx],
|
|
1143
|
+
childNode,
|
|
1144
|
+
itemScope,
|
|
1145
|
+
index
|
|
1146
|
+
);
|
|
1147
|
+
childElIdx++;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Update non-loop live elements that reference template data bindings.
|
|
1155
|
+
*/
|
|
1156
|
+
_reconcileStaticDataBindings(path, compData) {
|
|
1157
|
+
const attrMap = this._getComponentAttrMap();
|
|
1158
|
+
for (const [key, props] of Object.entries(attrMap)) {
|
|
1159
|
+
const id = normalizeId(key);
|
|
1160
|
+
const str = JSON.stringify(props);
|
|
1161
|
+
if (!str.includes('${') && !str.includes('{{')) continue;
|
|
1162
|
+
if (path && !str.includes(path)) continue;
|
|
1163
|
+
|
|
1164
|
+
const nodes = this.getLiveNodes(id);
|
|
1165
|
+
for (const el of nodes) {
|
|
1166
|
+
// Skip elements governed by loops as they are reconciled separately
|
|
1167
|
+
if (el.hasAttribute('data-wdl-index') || this._findLoopForElement(el)) continue;
|
|
1168
|
+
const resolved = resolveAll(props, compData);
|
|
1169
|
+
this._applyPropsToElement(el, id, resolved);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
608
1174
|
// -----------------------------------------------------------------------
|
|
609
1175
|
// variant:change
|
|
610
1176
|
// action: set targetId = semanticId (or component id) payload = variantName
|
|
@@ -613,29 +1179,26 @@ export class WdlDom {
|
|
|
613
1179
|
_handleVariant(event) {
|
|
614
1180
|
const { targetId, payload } = event;
|
|
615
1181
|
const id = normalizeId(targetId) || this._rootSemanticId();
|
|
616
|
-
const el = this.
|
|
1182
|
+
const el = this.getNode(id);
|
|
617
1183
|
if (!el) {
|
|
618
|
-
// try root
|
|
619
1184
|
const rootId = this._rootSemanticId();
|
|
620
|
-
const rootEl = rootId ? this.
|
|
1185
|
+
const rootEl = rootId ? this.getNode(rootId) : null;
|
|
621
1186
|
if (rootEl) {
|
|
622
1187
|
if (payload) rootEl.dataset.variant = String(payload);
|
|
623
1188
|
else delete rootEl.dataset.variant;
|
|
1189
|
+
this._refreshElementClasses(rootEl, rootId);
|
|
624
1190
|
this._log('op', `variant → ${payload || '(none)'} on #${rootId}`);
|
|
625
1191
|
}
|
|
626
1192
|
return;
|
|
627
1193
|
}
|
|
628
1194
|
if (payload) el.dataset.variant = String(payload);
|
|
629
1195
|
else delete el.dataset.variant;
|
|
1196
|
+
this._refreshElementClasses(el, id);
|
|
630
1197
|
this._log('op', `variant → ${payload || '(none)'} on #${id}`);
|
|
631
1198
|
}
|
|
632
1199
|
|
|
633
1200
|
_applyRootVariant() {
|
|
634
|
-
|
|
635
|
-
const attr =
|
|
636
|
-
this.component.attr?.list?.() ??
|
|
637
|
-
this.component.getSnapshot?.()?.attr ??
|
|
638
|
-
{};
|
|
1201
|
+
const attr = this._getComponentAttrMap();
|
|
639
1202
|
const rootId = this._rootSemanticId();
|
|
640
1203
|
if (!rootId) return;
|
|
641
1204
|
const rootKey = '.' + rootId;
|
|
@@ -644,12 +1207,15 @@ export class WdlDom {
|
|
|
644
1207
|
attr['.']?.['data-variant'] ??
|
|
645
1208
|
null;
|
|
646
1209
|
if (variant && this.liveMap.has(rootId)) {
|
|
647
|
-
this.
|
|
1210
|
+
const el = this.getNode(rootId);
|
|
1211
|
+
if (el && el.dataset) {
|
|
1212
|
+
el.dataset.variant = String(variant);
|
|
1213
|
+
this._refreshElementClasses(el, rootId);
|
|
1214
|
+
}
|
|
648
1215
|
}
|
|
649
1216
|
}
|
|
650
1217
|
|
|
651
1218
|
_rootSemanticId() {
|
|
652
|
-
// First entry in liveMap that is a direct child of container, or first key
|
|
653
1219
|
for (const [id, el] of this.liveMap) {
|
|
654
1220
|
if (el.parentNode === this.container) return id;
|
|
655
1221
|
}
|
|
@@ -665,6 +1231,7 @@ export class WdlDom {
|
|
|
665
1231
|
|
|
666
1232
|
_handleRegistry(event) {
|
|
667
1233
|
this._syncRegistryStyles();
|
|
1234
|
+
this._refreshAllElementClasses();
|
|
668
1235
|
this._log('op', `registry.${event.action}`);
|
|
669
1236
|
}
|
|
670
1237
|
|
|
@@ -751,3 +1318,4 @@ export function createWdlDom(options) {
|
|
|
751
1318
|
}
|
|
752
1319
|
|
|
753
1320
|
export default createWdlDom;
|
|
1321
|
+
|