@microsoft/webui-framework 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -33
- package/dist/component-asset.d.ts +30 -0
- package/dist/component-asset.js +213 -0
- package/dist/decorators.d.ts +2 -53
- package/dist/decorators.js +93 -171
- package/dist/element/diff.d.ts +0 -20
- package/dist/element/diff.js +25 -62
- package/dist/element/markers.d.ts +0 -44
- package/dist/element/markers.js +5 -54
- package/dist/element/styles.d.ts +0 -10
- package/dist/element/styles.js +2 -63
- package/dist/element/types.d.ts +4 -39
- package/dist/element/types.js +0 -3
- package/dist/element.d.ts +13 -131
- package/dist/element.js +101 -1355
- package/dist/hydration-mismatch.d.ts +22 -0
- package/dist/hydration-mismatch.js +87 -0
- package/dist/index.d.ts +0 -19
- package/dist/index.js +2 -21
- package/dist/lifecycle.d.ts +0 -8
- package/dist/lifecycle.js +0 -32
- package/dist/static-host.d.ts +1 -0
- package/dist/static-host.js +58 -0
- package/dist/template-element.d.ts +76 -0
- package/dist/template-element.js +1252 -0
- package/dist/template-events.d.ts +6 -0
- package/dist/template-events.js +23 -0
- package/dist/template-roots.d.ts +5 -0
- package/dist/template-roots.js +35 -0
- package/dist/template-types.d.ts +11 -15
- package/dist/template-types.js +0 -2
- package/dist/template.d.ts +2 -18
- package/dist/template.js +9 -2
- package/package.json +11 -5
package/dist/decorators.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT license.
|
|
3
|
-
/**
|
|
4
|
-
* Reactive decorators for WebUIElement properties.
|
|
5
|
-
*
|
|
6
|
-
* Uses TypeScript's `experimentalDecorators` emit, matching the FAST ecosystem
|
|
7
|
-
* conventions.
|
|
8
|
-
*/
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Internal helpers
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
/**
|
|
13
|
-
* Map of camelCase property names to their HTML attribute names.
|
|
14
|
-
*
|
|
15
|
-
* ARIA attributes (`ariaXxxYyy → aria-` + lowercase remainder) are handled
|
|
16
|
-
* algorithmically in `toKebabCase`. Only HTML global/element attributes
|
|
17
|
-
* with irregular mappings (concatenated lowercase) need explicit entries.
|
|
18
|
-
*/
|
|
19
1
|
const propertyToAttribute = Object.assign(Object.create(null), {
|
|
20
|
-
// --- HTML global/element attributes ---
|
|
21
2
|
accessKey: 'accesskey',
|
|
22
3
|
autoCapitalize: 'autocapitalize',
|
|
23
4
|
contentEditable: 'contenteditable',
|
|
@@ -40,41 +21,11 @@ const propertyToAttribute = Object.assign(Object.create(null), {
|
|
|
40
21
|
tabIndex: 'tabindex',
|
|
41
22
|
useMap: 'usemap',
|
|
42
23
|
});
|
|
43
|
-
/**
|
|
44
|
-
* Convert a camelCase DOM property name into its kebab-case HTML attribute form.
|
|
45
|
-
*
|
|
46
|
-
* This function is optimized for framework-level hot paths where attribute
|
|
47
|
-
* normalization may run thousands of times per render. It performs three
|
|
48
|
-
* progressively cheaper checks:
|
|
49
|
-
*
|
|
50
|
-
* 1. **Direct lookup for irregular mappings**
|
|
51
|
-
* Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
|
|
52
|
-
* follow simple camelCase → kebab-case rules. These are resolved through a
|
|
53
|
-
* precomputed `propertyToAttribute` map for O(1) returns with no string
|
|
54
|
-
* processing.
|
|
55
|
-
*
|
|
56
|
-
* 2. **Fast path for ARIA attributes**
|
|
57
|
-
* ARIA properties always begin with `aria` followed by an uppercase letter
|
|
58
|
-
* (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
|
|
59
|
-
* This branch avoids the general loop and uses the engine-optimized
|
|
60
|
-
* `.toLowerCase()` for the suffix.
|
|
61
|
-
*
|
|
62
|
-
* 3. **General camelCase → kebab-case conversion**
|
|
63
|
-
* For all other inputs, the function performs a tight ASCII-only scan:
|
|
64
|
-
* uppercase A–Z (65–90) are converted to lowercase and prefixed with `-`,
|
|
65
|
-
* while all other characters are copied as-is. This avoids regex engines,
|
|
66
|
-
* callback allocations, and match objects, producing predictable,
|
|
67
|
-
* allocation-minimal performance ideal for DOM attribute reflection.
|
|
68
|
-
*
|
|
69
|
-
* The result is a predictable, JIT-friendly transformation suitable for
|
|
70
|
-
* attribute diffing, SSR serialization, and runtime DOM patching.
|
|
71
|
-
*/
|
|
72
24
|
export function toKebabCase(str) {
|
|
73
25
|
const mapped = propertyToAttribute[str];
|
|
74
26
|
if (mapped)
|
|
75
27
|
return mapped;
|
|
76
|
-
|
|
77
|
-
if (str.length > 4 && str.charCodeAt(0) === 97 /* a */ && str.startsWith('aria') && str.charCodeAt(4) >= 65 && str.charCodeAt(4) <= 90) {
|
|
28
|
+
if (str.length > 4 && str.charCodeAt(0) === 97 && str.startsWith('aria') && str.charCodeAt(4) >= 65 && str.charCodeAt(4) <= 90) {
|
|
78
29
|
return 'aria-' + str.slice(4).toLowerCase();
|
|
79
30
|
}
|
|
80
31
|
let out = '';
|
|
@@ -85,96 +36,12 @@ export function toKebabCase(str) {
|
|
|
85
36
|
return out;
|
|
86
37
|
}
|
|
87
38
|
const reflectingAttribute = Symbol('webui.reflectingAttribute');
|
|
39
|
+
const EMPTY_SET = Object.freeze(new Set());
|
|
88
40
|
function parentConstructor(ctor) {
|
|
89
41
|
const parent = Object.getPrototypeOf(ctor);
|
|
90
42
|
return typeof parent === 'function' && parent !== Function.prototype ? parent : null;
|
|
91
43
|
}
|
|
92
|
-
function createReactiveProperty(proto, name, attrDefinition) {
|
|
93
|
-
const backingKey = `_${name}`;
|
|
94
|
-
const changedKey = `${name}Changed`;
|
|
95
|
-
Object.defineProperty(proto, name, {
|
|
96
|
-
get() {
|
|
97
|
-
return this[backingKey];
|
|
98
|
-
},
|
|
99
|
-
set(newValue) {
|
|
100
|
-
const oldValue = this[backingKey];
|
|
101
|
-
if (Object.is(oldValue, newValue))
|
|
102
|
-
return;
|
|
103
|
-
this[backingKey] = newValue;
|
|
104
|
-
if (attrDefinition && this['$ready'] === true) {
|
|
105
|
-
reflectPropertyToAttribute(this, attrDefinition, newValue);
|
|
106
|
-
}
|
|
107
|
-
const cb = this[changedKey];
|
|
108
|
-
if (typeof cb === 'function') {
|
|
109
|
-
cb.call(this, oldValue, newValue);
|
|
110
|
-
}
|
|
111
|
-
if (this.isConnected) {
|
|
112
|
-
const upd = this['$update'];
|
|
113
|
-
if (upd)
|
|
114
|
-
upd.call(this, name);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
enumerable: true,
|
|
118
|
-
configurable: true,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
function reflectPropertyToAttribute(instance, definition, value) {
|
|
122
|
-
const element = instance;
|
|
123
|
-
const attrName = definition.attribute;
|
|
124
|
-
if (definition.boolean) {
|
|
125
|
-
const shouldHaveAttribute = Boolean(value);
|
|
126
|
-
if (element.hasAttribute(attrName) === shouldHaveAttribute)
|
|
127
|
-
return;
|
|
128
|
-
setReflectingAttribute(instance, attrName);
|
|
129
|
-
try {
|
|
130
|
-
if (shouldHaveAttribute)
|
|
131
|
-
element.setAttribute(attrName, '');
|
|
132
|
-
else
|
|
133
|
-
element.removeAttribute(attrName);
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
restoreReflectingAttribute(instance);
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (value == null) {
|
|
141
|
-
if (!element.hasAttribute(attrName))
|
|
142
|
-
return;
|
|
143
|
-
setReflectingAttribute(instance, attrName);
|
|
144
|
-
try {
|
|
145
|
-
element.removeAttribute(attrName);
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
restoreReflectingAttribute(instance);
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const attrValue = typeof value === 'string' ? value : String(value);
|
|
153
|
-
if (element.getAttribute(attrName) === attrValue)
|
|
154
|
-
return;
|
|
155
|
-
setReflectingAttribute(instance, attrName);
|
|
156
|
-
try {
|
|
157
|
-
element.setAttribute(attrName, attrValue);
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
restoreReflectingAttribute(instance);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
function setReflectingAttribute(instance, attrName) {
|
|
164
|
-
instance[reflectingAttribute] = attrName;
|
|
165
|
-
}
|
|
166
|
-
function restoreReflectingAttribute(instance) {
|
|
167
|
-
instance[reflectingAttribute] = undefined;
|
|
168
|
-
}
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
// @observable
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
/** Per-class registry of @observable property names. */
|
|
173
44
|
const observableRegistry = new WeakMap();
|
|
174
|
-
/**
|
|
175
|
-
* Get the set of @observable property names registered for a class.
|
|
176
|
-
*/
|
|
177
|
-
const EMPTY_SET = Object.freeze(new Set());
|
|
178
45
|
export function getObservableNames(ctor) {
|
|
179
46
|
const names = observableRegistry.get(ctor);
|
|
180
47
|
if (names)
|
|
@@ -192,26 +59,7 @@ function registerObservableProperty(ctor, name) {
|
|
|
192
59
|
}
|
|
193
60
|
names.add(name);
|
|
194
61
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Marks a property as observable. When the value changes the decorator will:
|
|
197
|
-
* 1. Call `this.<prop>Changed(oldValue, newValue)` if defined.
|
|
198
|
-
* 2. Call `this.$update(name)` if the element is connected, targeting
|
|
199
|
-
* only bindings that reference this property.
|
|
200
|
-
*/
|
|
201
|
-
export function observable(target, name) {
|
|
202
|
-
const ctor = target.constructor;
|
|
203
|
-
registerObservableProperty(ctor, name);
|
|
204
|
-
createReactiveProperty(target, name);
|
|
205
|
-
}
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
// @attr
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
/**
|
|
210
|
-
* Registry of attribute-name → property-name mappings per constructor.
|
|
211
|
-
* Used by `attributeChangedCallback` to route attribute changes to properties.
|
|
212
|
-
*/
|
|
213
62
|
const attrByAttribute = new WeakMap();
|
|
214
|
-
/** Registry of property-name → attribute metadata, used for mount-time sync. */
|
|
215
63
|
const attrByProperty = new WeakMap();
|
|
216
64
|
function inheritedAttrMap(registry, ctor) {
|
|
217
65
|
let current = parentConstructor(ctor);
|
|
@@ -246,6 +94,97 @@ function attrPropertyMapFor(ctor) {
|
|
|
246
94
|
export function isAttributeProperty(ctor, property) {
|
|
247
95
|
return attrPropertyMapFor(ctor)?.has(property) === true;
|
|
248
96
|
}
|
|
97
|
+
function setReflectingAttribute(instance, attrName) {
|
|
98
|
+
instance[reflectingAttribute] = attrName;
|
|
99
|
+
}
|
|
100
|
+
function restoreReflectingAttribute(instance) {
|
|
101
|
+
instance[reflectingAttribute] = undefined;
|
|
102
|
+
}
|
|
103
|
+
function reflectPropertyToAttribute(instance, definition, value) {
|
|
104
|
+
const element = instance;
|
|
105
|
+
const attrName = definition.attribute;
|
|
106
|
+
if (definition.boolean) {
|
|
107
|
+
const shouldHaveAttribute = Boolean(value);
|
|
108
|
+
if (element.hasAttribute(attrName) === shouldHaveAttribute)
|
|
109
|
+
return;
|
|
110
|
+
setReflectingAttribute(instance, attrName);
|
|
111
|
+
try {
|
|
112
|
+
if (shouldHaveAttribute)
|
|
113
|
+
element.setAttribute(attrName, '');
|
|
114
|
+
else
|
|
115
|
+
element.removeAttribute(attrName);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
restoreReflectingAttribute(instance);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (value == null) {
|
|
123
|
+
if (!element.hasAttribute(attrName))
|
|
124
|
+
return;
|
|
125
|
+
setReflectingAttribute(instance, attrName);
|
|
126
|
+
try {
|
|
127
|
+
element.removeAttribute(attrName);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
restoreReflectingAttribute(instance);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const attrValue = typeof value === 'string' ? value : String(value);
|
|
135
|
+
if (element.getAttribute(attrName) === attrValue)
|
|
136
|
+
return;
|
|
137
|
+
setReflectingAttribute(instance, attrName);
|
|
138
|
+
try {
|
|
139
|
+
element.setAttribute(attrName, attrValue);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
restoreReflectingAttribute(instance);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function syncAttrProperties(instance, ctor) {
|
|
146
|
+
const attrs = attrPropertyMapFor(ctor);
|
|
147
|
+
if (!attrs)
|
|
148
|
+
return;
|
|
149
|
+
const reactiveInstance = instance;
|
|
150
|
+
for (const definition of attrs.values()) {
|
|
151
|
+
reflectPropertyToAttribute(reactiveInstance, definition, reactiveInstance[definition.property]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function createReactiveProperty(proto, name, attrDefinition) {
|
|
155
|
+
const backingKey = `_${name}`;
|
|
156
|
+
const changedKey = `${name}Changed`;
|
|
157
|
+
Object.defineProperty(proto, name, {
|
|
158
|
+
get() {
|
|
159
|
+
return this[backingKey];
|
|
160
|
+
},
|
|
161
|
+
set(newValue) {
|
|
162
|
+
const oldValue = this[backingKey];
|
|
163
|
+
if (Object.is(oldValue, newValue))
|
|
164
|
+
return;
|
|
165
|
+
this[backingKey] = newValue;
|
|
166
|
+
if (attrDefinition && this['$ready'] === true) {
|
|
167
|
+
reflectPropertyToAttribute(this, attrDefinition, newValue);
|
|
168
|
+
}
|
|
169
|
+
const cb = this[changedKey];
|
|
170
|
+
if (typeof cb === 'function') {
|
|
171
|
+
cb.call(this, oldValue, newValue);
|
|
172
|
+
}
|
|
173
|
+
if (this.isConnected) {
|
|
174
|
+
const upd = this['$update'];
|
|
175
|
+
if (upd)
|
|
176
|
+
upd.call(this, name);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
enumerable: true,
|
|
180
|
+
configurable: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
export function observable(target, name) {
|
|
184
|
+
const ctor = target.constructor;
|
|
185
|
+
registerObservableProperty(ctor, name);
|
|
186
|
+
createReactiveProperty(target, name);
|
|
187
|
+
}
|
|
249
188
|
function applyAttr(target, name, options) {
|
|
250
189
|
const proto = target;
|
|
251
190
|
const ctor = proto.constructor;
|
|
@@ -255,11 +194,8 @@ function applyAttr(target, name, options) {
|
|
|
255
194
|
property: name,
|
|
256
195
|
boolean: options?.mode === 'boolean',
|
|
257
196
|
};
|
|
258
|
-
// 1. Install the reactive getter/setter (same as @observable), with
|
|
259
|
-
// attribute reflection enabled after the element finishes hydration.
|
|
260
197
|
registerObservableProperty(ctor, name);
|
|
261
198
|
createReactiveProperty(proto, name, definition);
|
|
262
|
-
// 2. Register the attribute mapping.
|
|
263
199
|
let byAttribute = attrByAttribute.get(ctor);
|
|
264
200
|
if (!byAttribute) {
|
|
265
201
|
const inherited = inheritedAttrMap(attrByAttribute, ctor);
|
|
@@ -274,21 +210,17 @@ function applyAttr(target, name, options) {
|
|
|
274
210
|
attrByProperty.set(ctor, byProperty);
|
|
275
211
|
}
|
|
276
212
|
byProperty.set(name, definition);
|
|
277
|
-
// 3. Accumulate observed attributes on the constructor.
|
|
278
213
|
if (!Object.prototype.hasOwnProperty.call(ctor, '_observedAttrs')) {
|
|
279
214
|
const inheritedAttrs = ctor._observedAttrs;
|
|
280
215
|
ctor._observedAttrs = inheritedAttrs ? inheritedAttrs.slice() : [];
|
|
281
|
-
// Define the static getter that `customElements.define` inspects.
|
|
282
216
|
Object.defineProperty(ctor, 'observedAttributes', {
|
|
283
217
|
get() {
|
|
284
218
|
return ctor._observedAttrs ?? [];
|
|
285
219
|
},
|
|
286
220
|
configurable: true,
|
|
287
221
|
});
|
|
288
|
-
// Patch `attributeChangedCallback` once per class.
|
|
289
222
|
const origACB = proto['attributeChangedCallback'];
|
|
290
223
|
proto['attributeChangedCallback'] = function (attribute, oldVal, newVal) {
|
|
291
|
-
// Route the attribute change to the corresponding property.
|
|
292
224
|
const definition = attrDefinitionFor(this.constructor, attribute);
|
|
293
225
|
if (definition !== undefined &&
|
|
294
226
|
this[reflectingAttribute] !== attribute) {
|
|
@@ -296,7 +228,6 @@ function applyAttr(target, name, options) {
|
|
|
296
228
|
? newVal !== null
|
|
297
229
|
: newVal;
|
|
298
230
|
}
|
|
299
|
-
// Preserve any pre-existing attributeChangedCallback.
|
|
300
231
|
if (origACB) {
|
|
301
232
|
origACB.call(this, attribute, oldVal, newVal);
|
|
302
233
|
}
|
|
@@ -304,15 +235,6 @@ function applyAttr(target, name, options) {
|
|
|
304
235
|
}
|
|
305
236
|
ctor._observedAttrs.push(attrName);
|
|
306
237
|
}
|
|
307
|
-
export function syncAttrProperties(instance, ctor) {
|
|
308
|
-
const attrs = attrPropertyMapFor(ctor);
|
|
309
|
-
if (!attrs)
|
|
310
|
-
return;
|
|
311
|
-
const reactiveInstance = instance;
|
|
312
|
-
for (const definition of attrs.values()) {
|
|
313
|
-
reflectPropertyToAttribute(reactiveInstance, definition, reactiveInstance[definition.property]);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
238
|
export function attr(targetOrOptions, name) {
|
|
317
239
|
if (typeof name === 'string') {
|
|
318
240
|
applyAttr(targetOrOptions, name);
|
package/dist/element/diff.d.ts
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keyed child reconciliation for `@for(item of items)` repeat blocks.
|
|
3
|
-
*
|
|
4
|
-
* Diff that matches old instances by key, reuses what it
|
|
5
|
-
* can, creates/removes the rest, then reorders DOM nodes in one forward pass.
|
|
6
|
-
*/
|
|
7
1
|
import type { RepeatBinding, RepeatHost } from './types.js';
|
|
8
|
-
/** Resolve a dotted path from a start offset without allocating. */
|
|
9
2
|
export declare function dotWalk(cursor: unknown, path: string, from: number): unknown;
|
|
10
|
-
/**
|
|
11
|
-
* Resolve a dotted path against a repeat scope variable.
|
|
12
|
-
*
|
|
13
|
-
* When a binding inside `@for(item of items)` references `item.title`,
|
|
14
|
-
* this function looks up `title` on the current scope value.
|
|
15
|
-
*/
|
|
16
|
-
export declare function resolveRepeatValue(scopeVar: string, scope: unknown, path: string): unknown;
|
|
17
|
-
/**
|
|
18
|
-
* Reconcile a repeat binding against its current collection value.
|
|
19
|
-
*
|
|
20
|
-
* Called by `$updateInstance` on every reactive update. Resolves the
|
|
21
|
-
* collection path, diffs old vs. new items by key, and patches the DOM.
|
|
22
|
-
*/
|
|
23
3
|
export declare function syncRepeat(host: RepeatHost, rep: RepeatBinding): void;
|
package/dist/element/diff.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT license.
|
|
3
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
4
1
|
function asParent(node) {
|
|
5
2
|
if (!node)
|
|
6
3
|
return null;
|
|
7
4
|
return 'childNodes' in node ? node : null;
|
|
8
5
|
}
|
|
9
|
-
/** Resolve a dotted path from a start offset without allocating. */
|
|
10
6
|
export function dotWalk(cursor, path, from) {
|
|
11
7
|
let start = from;
|
|
12
8
|
for (let i = from; i <= path.length; i++) {
|
|
13
|
-
if (i === path.length || path.charCodeAt(i) === 46
|
|
9
|
+
if (i === path.length || path.charCodeAt(i) === 46) {
|
|
14
10
|
if (cursor == null || typeof cursor !== 'object')
|
|
15
11
|
return undefined;
|
|
16
12
|
cursor = cursor[path.slice(start, i)];
|
|
@@ -19,53 +15,27 @@ export function dotWalk(cursor, path, from) {
|
|
|
19
15
|
}
|
|
20
16
|
return cursor;
|
|
21
17
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Resolve a dotted path against a repeat scope variable.
|
|
24
|
-
*
|
|
25
|
-
* When a binding inside `@for(item of items)` references `item.title`,
|
|
26
|
-
* this function looks up `title` on the current scope value.
|
|
27
|
-
*/
|
|
28
|
-
export function resolveRepeatValue(scopeVar, scope, path) {
|
|
29
|
-
if (path === scopeVar)
|
|
30
|
-
return scope;
|
|
31
|
-
if (path.length <= scopeVar.length || path.charCodeAt(scopeVar.length) !== 46 /* '.' */ || !path.startsWith(scopeVar))
|
|
32
|
-
return undefined;
|
|
33
|
-
return dotWalk(scope, path, scopeVar.length + 1);
|
|
34
|
-
}
|
|
35
|
-
/** Compute a key for an item using the cached key path, or null. */
|
|
36
18
|
function itemKey(item, keyPath) {
|
|
37
19
|
if (keyPath === undefined || keyPath === '')
|
|
38
20
|
return null;
|
|
39
21
|
const v = dotWalk(item, keyPath, 0);
|
|
40
22
|
return v != null ? String(v) : '';
|
|
41
23
|
}
|
|
42
|
-
/** Build a scope frame for a repeat item. */
|
|
43
24
|
function itemScope(rep, item) {
|
|
44
25
|
return { name: rep.itemVar, value: item, parent: rep.scope };
|
|
45
26
|
}
|
|
46
|
-
// ── Reconciliation ──────────────────────────────────────────────────
|
|
47
|
-
/**
|
|
48
|
-
* Reconcile a repeat binding against its current collection value.
|
|
49
|
-
*
|
|
50
|
-
* Called by `$updateInstance` on every reactive update. Resolves the
|
|
51
|
-
* collection path, diffs old vs. new items by key, and patches the DOM.
|
|
52
|
-
*/
|
|
53
27
|
export function syncRepeat(host, rep) {
|
|
54
28
|
const resolved = host.$resolveValue(rep.collection, rep.scope);
|
|
55
29
|
const items = Array.isArray(resolved) ? resolved : [];
|
|
56
|
-
// Locate the container once and cache it.
|
|
57
30
|
let container = rep.container
|
|
58
31
|
?? (rep.start ? asParent(rep.start.parentNode) : null)
|
|
59
32
|
?? (rep.owner.nodes[0] ? asParent(rep.owner.nodes[0].parentNode) : null);
|
|
60
33
|
if (!container)
|
|
61
34
|
return;
|
|
62
35
|
rep.container = container;
|
|
63
|
-
// Before the first client-side sync, bail if the collection hasn't
|
|
64
|
-
// been explicitly set but SSR children already exist.
|
|
65
36
|
if (!rep.synced && items.length === 0 && rep.instances.length > 0)
|
|
66
37
|
return;
|
|
67
38
|
rep.synced = true;
|
|
68
|
-
// If there are no items, just tear down everything.
|
|
69
39
|
if (items.length === 0) {
|
|
70
40
|
for (let i = 0; i < rep.instances.length; i += 1) {
|
|
71
41
|
host.$removeInstance(rep.instances[i].instance);
|
|
@@ -73,45 +43,40 @@ export function syncRepeat(host, rep) {
|
|
|
73
43
|
rep.instances = [];
|
|
74
44
|
return;
|
|
75
45
|
}
|
|
76
|
-
const keyPath =
|
|
46
|
+
const keyPath = rep.keyPath;
|
|
77
47
|
const hasKeys = keyPath !== undefined && keyPath !== '';
|
|
78
48
|
const oldInstances = rep.instances;
|
|
79
|
-
// ── Fast path for unkeyed (index-based) repeats ────────────────
|
|
80
49
|
if (!hasKeys) {
|
|
81
|
-
const
|
|
82
|
-
const reuseCount = Math.min(
|
|
83
|
-
|
|
50
|
+
const oldLength = oldInstances.length;
|
|
51
|
+
const reuseCount = Math.min(oldLength, items.length);
|
|
52
|
+
let nextCount = reuseCount;
|
|
84
53
|
for (let i = 0; i < reuseCount; i += 1) {
|
|
85
54
|
const entry = oldInstances[i];
|
|
86
55
|
entry.value = items[i];
|
|
87
56
|
if (entry.instance.scope)
|
|
88
57
|
entry.instance.scope.value = items[i];
|
|
89
|
-
next.push(entry);
|
|
90
58
|
}
|
|
91
|
-
// Create new instances for items beyond old length
|
|
92
59
|
for (let i = reuseCount; i < items.length; i += 1) {
|
|
93
60
|
const scope = itemScope(rep, items[i]);
|
|
94
61
|
const instance = host.$createBlockInstance(rep.blockIndex, scope);
|
|
95
62
|
if (instance) {
|
|
96
|
-
|
|
63
|
+
oldInstances[nextCount] = { key: null, value: items[i], instance };
|
|
64
|
+
nextCount += 1;
|
|
97
65
|
}
|
|
98
66
|
}
|
|
99
|
-
|
|
100
|
-
for (let i = reuseCount; i < oldInstances.length; i += 1) {
|
|
67
|
+
for (let i = reuseCount; i < oldLength; i += 1) {
|
|
101
68
|
host.$removeInstance(oldInstances[i].instance);
|
|
102
69
|
}
|
|
103
|
-
|
|
70
|
+
oldInstances.length = nextCount;
|
|
104
71
|
let cursor = rep.start;
|
|
105
|
-
for (let i = 0; i <
|
|
106
|
-
cursor = host.$insertInstanceAfter(cursor, container,
|
|
72
|
+
for (let i = 0; i < oldInstances.length; i += 1) {
|
|
73
|
+
cursor = host.$insertInstanceAfter(cursor, container, oldInstances[i].instance);
|
|
107
74
|
}
|
|
108
75
|
for (let i = 0; i < reuseCount; i += 1) {
|
|
109
|
-
host.$updateInstance(
|
|
76
|
+
host.$updateInstance(oldInstances[i].instance);
|
|
110
77
|
}
|
|
111
78
|
return;
|
|
112
79
|
}
|
|
113
|
-
// ── Keyed diff ─────────────────────────────────────────────────
|
|
114
|
-
// ── Build old-key → instance map ────────────────────────────────
|
|
115
80
|
const oldByKey = new Map();
|
|
116
81
|
for (let i = 0; i < oldInstances.length; i += 1) {
|
|
117
82
|
const entry = oldInstances[i];
|
|
@@ -119,44 +84,42 @@ export function syncRepeat(host, rep) {
|
|
|
119
84
|
if (k != null)
|
|
120
85
|
oldByKey.set(k, entry);
|
|
121
86
|
}
|
|
122
|
-
|
|
123
|
-
const next = [];
|
|
87
|
+
let nextCount = 0;
|
|
124
88
|
for (let i = 0; i < items.length; i += 1) {
|
|
125
89
|
const item = items[i];
|
|
126
90
|
const key = itemKey(item, keyPath);
|
|
127
91
|
const existing = key != null ? oldByKey.get(key) : undefined;
|
|
128
92
|
if (existing) {
|
|
129
|
-
oldByKey.
|
|
93
|
+
oldByKey.set(key, undefined);
|
|
130
94
|
existing.value = item;
|
|
131
95
|
existing.key = key;
|
|
132
96
|
if (existing.instance.scope)
|
|
133
97
|
existing.instance.scope.value = item;
|
|
134
|
-
|
|
98
|
+
oldInstances[nextCount] = existing;
|
|
99
|
+
nextCount += 1;
|
|
135
100
|
}
|
|
136
101
|
else {
|
|
137
102
|
const scope = itemScope(rep, item);
|
|
138
103
|
const instance = host.$createBlockInstance(rep.blockIndex, scope);
|
|
139
104
|
if (instance) {
|
|
140
|
-
|
|
105
|
+
oldInstances[nextCount] = { key: key ?? null, value: item, instance };
|
|
106
|
+
nextCount += 1;
|
|
141
107
|
}
|
|
142
108
|
}
|
|
143
109
|
}
|
|
144
|
-
// ── Remove unmatched old instances ──────────────────────────────
|
|
145
110
|
for (const leftover of oldByKey.values()) {
|
|
146
|
-
|
|
111
|
+
if (leftover)
|
|
112
|
+
host.$removeInstance(leftover.instance);
|
|
147
113
|
}
|
|
148
|
-
|
|
149
|
-
// ── Reorder DOM (forward pass) ──────────────────────────────────
|
|
150
|
-
// Newly-created instances were patched while detached. Reused instances
|
|
151
|
-
// update after moving so nested structural nodes stay with the item.
|
|
114
|
+
oldInstances.length = nextCount;
|
|
152
115
|
let cursor = rep.start;
|
|
153
|
-
for (let i = 0; i <
|
|
154
|
-
cursor = host.$insertInstanceAfter(cursor, container,
|
|
116
|
+
for (let i = 0; i < oldInstances.length; i += 1) {
|
|
117
|
+
cursor = host.$insertInstanceAfter(cursor, container, oldInstances[i].instance);
|
|
155
118
|
}
|
|
156
119
|
for (let i = 0; i < oldInstances.length; i += 1) {
|
|
157
120
|
const entry = oldInstances[i];
|
|
158
|
-
|
|
159
|
-
if (k != null && !oldByKey.has(k))
|
|
121
|
+
if (entry.key != null && oldByKey.has(entry.key) && oldByKey.get(entry.key) === undefined) {
|
|
160
122
|
host.$updateInstance(entry.instance);
|
|
123
|
+
}
|
|
161
124
|
}
|
|
162
125
|
}
|
|
@@ -1,54 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hydration marker utilities for zero-DOM-mutation in-place hydration.
|
|
3
|
-
*
|
|
4
|
-
* The WebUI handler plugin emits lightweight HTML comment markers around
|
|
5
|
-
* structural boundaries (for-loops and if-conditions). These utilities
|
|
6
|
-
* walk markers and locate elements so the hydration path can wire
|
|
7
|
-
* bindings in-place without reparenting DOM nodes.
|
|
8
|
-
*
|
|
9
|
-
* Marker format:
|
|
10
|
-
* <!--wr--> repeat block start
|
|
11
|
-
* <!--/wr--> repeat block end
|
|
12
|
-
* <!--wi--> repeat item boundary
|
|
13
|
-
* <!--wc--> conditional block start
|
|
14
|
-
* <!--/wc--> conditional block end
|
|
15
|
-
*/
|
|
16
1
|
export declare const MARKER_REPEAT_START = "wr";
|
|
17
2
|
export declare const MARKER_REPEAT_END = "/wr";
|
|
18
3
|
export declare const MARKER_COND_START = "wc";
|
|
19
4
|
export declare const MARKER_COND_END = "/wc";
|
|
20
|
-
/**
|
|
21
|
-
* Collect the item markers (<!--wi-->) within a repeat range.
|
|
22
|
-
*
|
|
23
|
-
* Walks siblings from the repeat start marker to the repeat end marker.
|
|
24
|
-
* Returns an array of <!--wi--> comment nodes that delineate items.
|
|
25
|
-
*/
|
|
26
5
|
export declare function collectItemMarkers(repeatStart: Comment): {
|
|
27
6
|
items: Comment[];
|
|
28
7
|
end: Comment | null;
|
|
29
8
|
};
|
|
30
|
-
/**
|
|
31
|
-
* Get the next element sibling after a marker comment, skipping
|
|
32
|
-
* whitespace text nodes and other comments.
|
|
33
|
-
*/
|
|
34
9
|
export declare function nextElement(marker: Comment): Element | null;
|
|
35
|
-
/**
|
|
36
|
-
* Find the Nth child of a given nodeType, skipping structural block ranges.
|
|
37
|
-
*
|
|
38
|
-
* The compiled template static HTML (`meta.h`) does not contain conditional
|
|
39
|
-
* or repeat block content — those are stored as separate block metadata.
|
|
40
|
-
* But the SSR DOM has this content rendered inline between marker pairs
|
|
41
|
-
* (`<!--wc-->...<!--/wc-->` and `<!--wr-->...<!--/wr-->`).
|
|
42
|
-
*
|
|
43
|
-
* This function walks `parent.firstChild` → siblings, counting only
|
|
44
|
-
* children of the requested `nodeType` that are NOT inside a structural
|
|
45
|
-
* block range. Nested blocks of the same type are handled via depth
|
|
46
|
-
* tracking. Returns the child at the given `ordinal`, or null.
|
|
47
|
-
*
|
|
48
|
-
* Used by `$resolveSSR` (element ordinals) and `$findSSRText` (text
|
|
49
|
-
* ordinals) to keep SSR DOM ordinals aligned with template metadata.
|
|
50
|
-
*
|
|
51
|
-
* **Requires closing markers to still be in the DOM** — caller must
|
|
52
|
-
* not remove `<!--/wc-->` or `<!--/wr-->` before all resolution is done.
|
|
53
|
-
*/
|
|
54
10
|
export declare function findByOrdinal(parent: Node, nodeType: number, ordinal: number): Node | null;
|