@microsoft/webui-framework 0.0.17 → 0.0.19
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 +164 -70
- package/dist/component-asset.d.ts +30 -0
- package/dist/component-asset.js +213 -0
- package/dist/decorators.d.ts +3 -53
- package/dist/decorators.js +96 -171
- package/dist/element/diff.d.ts +3 -21
- package/dist/element/diff.js +201 -131
- package/dist/element/markers.d.ts +0 -44
- package/dist/element/markers.js +18 -59
- package/dist/element/styles.d.ts +0 -10
- package/dist/element/styles.js +2 -63
- package/dist/element/types.d.ts +20 -47
- package/dist/element/types.js +3 -3
- package/dist/element.d.ts +13 -131
- package/dist/element.js +102 -1355
- package/dist/hydration-mismatch.d.ts +22 -0
- package/dist/hydration-mismatch.js +88 -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 +66 -0
- package/dist/template-element.d.ts +87 -0
- package/dist/template-element.js +1392 -0
- package/dist/template-events.d.ts +6 -0
- package/dist/template-events.js +23 -0
- package/dist/template-roots.d.ts +4 -0
- package/dist/template-roots.js +24 -0
- package/dist/template-types.d.ts +18 -16
- package/dist/template-types.js +0 -2
- package/dist/template.d.ts +3 -18
- package/dist/template.js +9 -2
- package/package.json +11 -5
package/dist/element/diff.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT license.
|
|
3
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
4
|
-
function asParent(node) {
|
|
5
|
-
if (!node)
|
|
6
|
-
return null;
|
|
7
|
-
return 'childNodes' in node ? node : null;
|
|
8
|
-
}
|
|
9
|
-
/** Resolve a dotted path from a start offset without allocating. */
|
|
10
1
|
export function dotWalk(cursor, path, from) {
|
|
11
2
|
let start = from;
|
|
12
3
|
for (let i = from; i <= path.length; i++) {
|
|
13
|
-
if (i === path.length || path.charCodeAt(i) === 46
|
|
4
|
+
if (i === path.length || path.charCodeAt(i) === 46) {
|
|
14
5
|
if (cursor == null || typeof cursor !== 'object')
|
|
15
6
|
return undefined;
|
|
16
7
|
cursor = cursor[path.slice(start, i)];
|
|
@@ -19,144 +10,223 @@ export function dotWalk(cursor, path, from) {
|
|
|
19
10
|
}
|
|
20
11
|
return cursor;
|
|
21
12
|
}
|
|
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
|
-
function itemKey(item, keyPath) {
|
|
37
|
-
if (keyPath === undefined || keyPath === '')
|
|
38
|
-
return null;
|
|
39
|
-
const v = dotWalk(item, keyPath, 0);
|
|
40
|
-
return v != null ? String(v) : '';
|
|
41
|
-
}
|
|
42
|
-
/** Build a scope frame for a repeat item. */
|
|
43
13
|
function itemScope(rep, item) {
|
|
44
|
-
return { name: rep.itemVar, value: item, parent: rep.scope };
|
|
14
|
+
return { name: rep.itemVar, value: item, parent: rep.scope, known: true };
|
|
45
15
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
?? (rep.start ? asParent(rep.start.parentNode) : null)
|
|
59
|
-
?? (rep.owner.nodes[0] ? asParent(rep.owner.nodes[0].parentNode) : null);
|
|
60
|
-
if (!container)
|
|
16
|
+
export function createRepeatKeyState(path) {
|
|
17
|
+
return {
|
|
18
|
+
path,
|
|
19
|
+
warned: false,
|
|
20
|
+
keys: [],
|
|
21
|
+
nextKeys: [],
|
|
22
|
+
map: new Map(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function seedHydratedRepeatKeys(rep, items) {
|
|
26
|
+
const state = rep.keyState;
|
|
27
|
+
if (!state || items.length !== rep.instances.length)
|
|
61
28
|
return;
|
|
62
|
-
|
|
63
|
-
// Before the first client-side sync, bail if the collection hasn't
|
|
64
|
-
// been explicitly set but SSR children already exist.
|
|
65
|
-
if (!rep.synced && items.length === 0 && rep.instances.length > 0)
|
|
29
|
+
if (!collectRepeatKeys(items, state))
|
|
66
30
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
host.$removeInstance(rep.instances[i].instance);
|
|
72
|
-
}
|
|
73
|
-
rep.instances = [];
|
|
31
|
+
commitKeyIdentity(state);
|
|
32
|
+
}
|
|
33
|
+
function setItemScope(instance, item) {
|
|
34
|
+
if (!instance.scope)
|
|
74
35
|
return;
|
|
36
|
+
instance.scope.value = item;
|
|
37
|
+
instance.scope.known = true;
|
|
38
|
+
}
|
|
39
|
+
function syncPositional(host, rep, items, container) {
|
|
40
|
+
const instances = rep.instances;
|
|
41
|
+
const oldLength = instances.length;
|
|
42
|
+
const reuseCount = Math.min(oldLength, items.length);
|
|
43
|
+
let nextCount = reuseCount;
|
|
44
|
+
let created = false;
|
|
45
|
+
for (let i = 0; i < reuseCount; i += 1) {
|
|
46
|
+
setItemScope(instances[i], items[i]);
|
|
75
47
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const reuseCount = Math.min(oldInstances.length, items.length);
|
|
83
|
-
// Reuse existing instances by index
|
|
84
|
-
for (let i = 0; i < reuseCount; i += 1) {
|
|
85
|
-
const entry = oldInstances[i];
|
|
86
|
-
entry.value = items[i];
|
|
87
|
-
if (entry.instance.scope)
|
|
88
|
-
entry.instance.scope.value = items[i];
|
|
89
|
-
next.push(entry);
|
|
90
|
-
}
|
|
91
|
-
// Create new instances for items beyond old length
|
|
92
|
-
for (let i = reuseCount; i < items.length; i += 1) {
|
|
93
|
-
const scope = itemScope(rep, items[i]);
|
|
94
|
-
const instance = host.$createBlockInstance(rep.blockIndex, scope);
|
|
95
|
-
if (instance) {
|
|
96
|
-
next.push({ key: null, value: items[i], instance });
|
|
97
|
-
}
|
|
48
|
+
for (let i = reuseCount; i < items.length; i += 1) {
|
|
49
|
+
const instance = host.$createBlockInstance(rep.blockIndex, itemScope(rep, items[i]), rep.owner, container);
|
|
50
|
+
if (instance) {
|
|
51
|
+
instances[nextCount] = instance;
|
|
52
|
+
nextCount += 1;
|
|
53
|
+
created = true;
|
|
98
54
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
55
|
+
}
|
|
56
|
+
for (let i = reuseCount; i < oldLength; i += 1) {
|
|
57
|
+
host.$removeInstance(instances[i]);
|
|
58
|
+
}
|
|
59
|
+
instances.length = nextCount;
|
|
60
|
+
const removed = oldLength > reuseCount;
|
|
61
|
+
let cursor = rep.start;
|
|
62
|
+
for (let i = 0; i < instances.length; i += 1) {
|
|
63
|
+
cursor = host.$insertInstanceAfter(cursor, container, instances[i]);
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < reuseCount; i += 1) {
|
|
66
|
+
host.$updateInstance(instances[i]);
|
|
67
|
+
}
|
|
68
|
+
if (created || removed)
|
|
69
|
+
host.$changeStructure(removed ? rep.owner : undefined);
|
|
70
|
+
}
|
|
71
|
+
function readRepeatKey(item, path) {
|
|
72
|
+
if (path.length === 0)
|
|
73
|
+
return item;
|
|
74
|
+
return dotWalk(item, path, 0);
|
|
75
|
+
}
|
|
76
|
+
function isRepeatKey(value) {
|
|
77
|
+
return (typeof value === 'string' ||
|
|
78
|
+
(typeof value === 'number' && Number.isFinite(value)));
|
|
79
|
+
}
|
|
80
|
+
function collectRepeatKeys(items, state) {
|
|
81
|
+
const keys = state.nextKeys;
|
|
82
|
+
const seen = state.map;
|
|
83
|
+
keys.length = items.length;
|
|
84
|
+
seen.clear();
|
|
85
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = readRepeatKey(items[i], state.path);
|
|
102
89
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
90
|
+
catch {
|
|
91
|
+
keys.length = 0;
|
|
92
|
+
seen.clear();
|
|
93
|
+
return false;
|
|
107
94
|
}
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
if (!isRepeatKey(value) || seen.has(value)) {
|
|
96
|
+
keys.length = 0;
|
|
97
|
+
seen.clear();
|
|
98
|
+
return false;
|
|
110
99
|
}
|
|
111
|
-
|
|
100
|
+
keys[i] = value;
|
|
101
|
+
seen.set(value, null);
|
|
112
102
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
103
|
+
seen.clear();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function commitKeyIdentity(state) {
|
|
107
|
+
const oldKeys = state.keys;
|
|
108
|
+
state.keys = state.nextKeys;
|
|
109
|
+
state.nextKeys = oldKeys;
|
|
110
|
+
state.nextKeys.length = 0;
|
|
111
|
+
}
|
|
112
|
+
function clearKeyIdentity(state) {
|
|
113
|
+
state.keys.length = 0;
|
|
114
|
+
state.nextKeys.length = 0;
|
|
115
|
+
state.map.clear();
|
|
116
|
+
}
|
|
117
|
+
function warnKeyFallback(rep, state) {
|
|
118
|
+
if (state.warned)
|
|
119
|
+
return;
|
|
120
|
+
state.warned = true;
|
|
121
|
+
const key = state.path.length === 0
|
|
122
|
+
? rep.itemVar
|
|
123
|
+
: `${rep.itemVar}.${state.path}`;
|
|
124
|
+
console.warn(`[webui] repeat "${rep.collection}" produced duplicate or invalid values for child key="${key}"; using positional reconciliation`);
|
|
125
|
+
}
|
|
126
|
+
function reconcileByKey(host, rep, items, container, state) {
|
|
127
|
+
const instances = rep.instances;
|
|
128
|
+
const map = state.map;
|
|
129
|
+
let nextCount = 0;
|
|
130
|
+
let created = false;
|
|
131
|
+
let removed = false;
|
|
124
132
|
for (let i = 0; i < items.length; i += 1) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
existing.value = item;
|
|
131
|
-
existing.key = key;
|
|
132
|
-
if (existing.instance.scope)
|
|
133
|
-
existing.instance.scope.value = item;
|
|
134
|
-
next.push(existing);
|
|
133
|
+
const key = state.nextKeys[i];
|
|
134
|
+
let instance = map.get(key);
|
|
135
|
+
if (instance) {
|
|
136
|
+
map.set(key, null);
|
|
137
|
+
setItemScope(instance, items[i]);
|
|
135
138
|
}
|
|
136
139
|
else {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
instance = host.$createBlockInstance(rep.blockIndex, itemScope(rep, items[i]), rep.owner, container) ?? undefined;
|
|
141
|
+
if (instance)
|
|
142
|
+
created = true;
|
|
143
|
+
}
|
|
144
|
+
if (instance) {
|
|
145
|
+
instances[nextCount] = instance;
|
|
146
|
+
state.nextKeys[nextCount] = key;
|
|
147
|
+
nextCount += 1;
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
|
-
|
|
145
|
-
for (const
|
|
146
|
-
|
|
150
|
+
state.nextKeys.length = nextCount;
|
|
151
|
+
for (const instance of map.values()) {
|
|
152
|
+
if (instance) {
|
|
153
|
+
host.$removeInstance(instance);
|
|
154
|
+
removed = true;
|
|
155
|
+
}
|
|
147
156
|
}
|
|
148
|
-
rep.instances = next;
|
|
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.
|
|
152
157
|
let cursor = rep.start;
|
|
153
|
-
for (let i = 0; i <
|
|
154
|
-
cursor = host.$insertInstanceAfter(cursor, container,
|
|
155
|
-
}
|
|
156
|
-
for (let i = 0; i <
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
for (let i = 0; i < nextCount; i += 1) {
|
|
159
|
+
cursor = host.$insertInstanceAfter(cursor, container, instances[i]);
|
|
160
|
+
}
|
|
161
|
+
for (let i = 0; i < nextCount; i += 1) {
|
|
162
|
+
if (map.get(state.nextKeys[i]) === null) {
|
|
163
|
+
host.$updateInstance(instances[i]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
map.clear();
|
|
167
|
+
instances.length = nextCount;
|
|
168
|
+
commitKeyIdentity(state);
|
|
169
|
+
if (created || removed)
|
|
170
|
+
host.$changeStructure(removed ? rep.owner : undefined);
|
|
171
|
+
}
|
|
172
|
+
function syncKeyed(host, rep, items, container, state) {
|
|
173
|
+
if (!collectRepeatKeys(items, state)) {
|
|
174
|
+
warnKeyFallback(rep, state);
|
|
175
|
+
clearKeyIdentity(state);
|
|
176
|
+
syncPositional(host, rep, items, container);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let sharedOrder = true;
|
|
180
|
+
const sharedLength = Math.min(state.keys.length, state.nextKeys.length);
|
|
181
|
+
for (let i = 0; sharedOrder && i < sharedLength; i += 1) {
|
|
182
|
+
sharedOrder = state.keys[i] === state.nextKeys[i];
|
|
183
|
+
}
|
|
184
|
+
if (state.keys.length !== rep.instances.length || sharedOrder) {
|
|
185
|
+
syncPositional(host, rep, items, container);
|
|
186
|
+
if (rep.instances.length === items.length) {
|
|
187
|
+
commitKeyIdentity(state);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
clearKeyIdentity(state);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (let i = 0; i < state.keys.length; i += 1) {
|
|
195
|
+
state.map.set(state.keys[i], rep.instances[i]);
|
|
196
|
+
}
|
|
197
|
+
reconcileByKey(host, rep, items, container, state);
|
|
198
|
+
}
|
|
199
|
+
export function syncRepeat(host, rep) {
|
|
200
|
+
const resolved = host.$resolveValue(rep.collection, rep.scope);
|
|
201
|
+
const items = Array.isArray(resolved) ? resolved : [];
|
|
202
|
+
const container = (rep.container
|
|
203
|
+
?? rep.start?.parentNode
|
|
204
|
+
?? rep.owner.nodes[0]?.parentNode);
|
|
205
|
+
if (!container)
|
|
206
|
+
return;
|
|
207
|
+
rep.container = container;
|
|
208
|
+
if (!rep.synced
|
|
209
|
+
&& rep.instances.length > 0
|
|
210
|
+
&& !host.$hasStateRoot(rep.collection, rep.scope))
|
|
211
|
+
return;
|
|
212
|
+
rep.synced = true;
|
|
213
|
+
if (items.length === 0) {
|
|
214
|
+
const hadInstances = rep.instances.length !== 0;
|
|
215
|
+
for (let i = 0; i < rep.instances.length; i += 1) {
|
|
216
|
+
host.$removeInstance(rep.instances[i]);
|
|
217
|
+
}
|
|
218
|
+
rep.instances.length = 0;
|
|
219
|
+
if (hadInstances)
|
|
220
|
+
host.$changeStructure(rep.owner);
|
|
221
|
+
if (rep.keyState) {
|
|
222
|
+
clearKeyIdentity(rep.keyState);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (rep.keyState) {
|
|
227
|
+
syncKeyed(host, rep, items, container, rep.keyState);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
syncPositional(host, rep, items, container);
|
|
161
231
|
}
|
|
162
232
|
}
|
|
@@ -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;
|
package/dist/element/markers.js
CHANGED
|
@@ -1,100 +1,60 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT license.
|
|
3
|
-
/**
|
|
4
|
-
* Hydration marker utilities for zero-DOM-mutation in-place hydration.
|
|
5
|
-
*
|
|
6
|
-
* The WebUI handler plugin emits lightweight HTML comment markers around
|
|
7
|
-
* structural boundaries (for-loops and if-conditions). These utilities
|
|
8
|
-
* walk markers and locate elements so the hydration path can wire
|
|
9
|
-
* bindings in-place without reparenting DOM nodes.
|
|
10
|
-
*
|
|
11
|
-
* Marker format:
|
|
12
|
-
* <!--wr--> repeat block start
|
|
13
|
-
* <!--/wr--> repeat block end
|
|
14
|
-
* <!--wi--> repeat item boundary
|
|
15
|
-
* <!--wc--> conditional block start
|
|
16
|
-
* <!--/wc--> conditional block end
|
|
17
|
-
*/
|
|
18
|
-
// Marker data constants matching the handler plugin output.
|
|
19
1
|
export const MARKER_REPEAT_START = 'wr';
|
|
20
2
|
export const MARKER_REPEAT_END = '/wr';
|
|
21
3
|
export const MARKER_COND_START = 'wc';
|
|
22
4
|
export const MARKER_COND_END = '/wc';
|
|
23
5
|
const MARKER_REPEAT_ITEM = 'wi';
|
|
24
|
-
/**
|
|
25
|
-
* Collect the item markers (<!--wi-->) within a repeat range.
|
|
26
|
-
*
|
|
27
|
-
* Walks siblings from the repeat start marker to the repeat end marker.
|
|
28
|
-
* Returns an array of <!--wi--> comment nodes that delineate items.
|
|
29
|
-
*/
|
|
30
6
|
export function collectItemMarkers(repeatStart) {
|
|
31
7
|
const items = [];
|
|
32
8
|
let end = null;
|
|
9
|
+
let depth = 1;
|
|
33
10
|
let node = repeatStart.nextSibling;
|
|
34
11
|
while (node) {
|
|
35
|
-
if (node.nodeType === 8
|
|
12
|
+
if (node.nodeType === 8) {
|
|
36
13
|
const data = node.data;
|
|
37
|
-
if (data ===
|
|
38
|
-
|
|
39
|
-
break;
|
|
14
|
+
if (data === MARKER_REPEAT_START) {
|
|
15
|
+
depth++;
|
|
40
16
|
}
|
|
41
|
-
if (data ===
|
|
17
|
+
else if (data === MARKER_REPEAT_END) {
|
|
18
|
+
depth--;
|
|
19
|
+
if (depth === 0) {
|
|
20
|
+
end = node;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (data === MARKER_REPEAT_ITEM && depth === 1) {
|
|
42
25
|
items.push(node);
|
|
26
|
+
}
|
|
43
27
|
}
|
|
44
28
|
node = node.nextSibling;
|
|
45
29
|
}
|
|
46
30
|
return { items, end };
|
|
47
31
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Get the next element sibling after a marker comment, skipping
|
|
50
|
-
* whitespace text nodes and other comments.
|
|
51
|
-
*/
|
|
52
32
|
export function nextElement(marker) {
|
|
53
33
|
let node = marker.nextSibling;
|
|
54
34
|
while (node) {
|
|
55
|
-
if (node.nodeType === 1
|
|
35
|
+
if (node.nodeType === 1)
|
|
56
36
|
return node;
|
|
57
|
-
if (node.nodeType === 8
|
|
37
|
+
if (node.nodeType === 8) {
|
|
58
38
|
const data = node.data;
|
|
59
|
-
if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM)
|
|
39
|
+
if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM || data === MARKER_COND_END)
|
|
60
40
|
return null;
|
|
61
41
|
}
|
|
62
42
|
node = node.nextSibling;
|
|
63
43
|
}
|
|
64
44
|
return null;
|
|
65
45
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Find the Nth child of a given nodeType, skipping structural block ranges.
|
|
68
|
-
*
|
|
69
|
-
* The compiled template static HTML (`meta.h`) does not contain conditional
|
|
70
|
-
* or repeat block content — those are stored as separate block metadata.
|
|
71
|
-
* But the SSR DOM has this content rendered inline between marker pairs
|
|
72
|
-
* (`<!--wc-->...<!--/wc-->` and `<!--wr-->...<!--/wr-->`).
|
|
73
|
-
*
|
|
74
|
-
* This function walks `parent.firstChild` → siblings, counting only
|
|
75
|
-
* children of the requested `nodeType` that are NOT inside a structural
|
|
76
|
-
* block range. Nested blocks of the same type are handled via depth
|
|
77
|
-
* tracking. Returns the child at the given `ordinal`, or null.
|
|
78
|
-
*
|
|
79
|
-
* Used by `$resolveSSR` (element ordinals) and `$findSSRText` (text
|
|
80
|
-
* ordinals) to keep SSR DOM ordinals aligned with template metadata.
|
|
81
|
-
*
|
|
82
|
-
* **Requires closing markers to still be in the DOM** — caller must
|
|
83
|
-
* not remove `<!--/wc-->` or `<!--/wr-->` before all resolution is done.
|
|
84
|
-
*/
|
|
85
46
|
export function findByOrdinal(parent, nodeType, ordinal) {
|
|
86
47
|
let count = 0;
|
|
87
48
|
let child = parent.firstChild;
|
|
88
49
|
while (child) {
|
|
89
|
-
|
|
90
|
-
if (child.nodeType === 8 /* COMMENT_NODE */) {
|
|
50
|
+
if (child.nodeType === 8) {
|
|
91
51
|
const data = child.data;
|
|
92
52
|
if (data === MARKER_COND_START || data === MARKER_REPEAT_START) {
|
|
93
53
|
const endTag = data === MARKER_COND_START ? MARKER_COND_END : MARKER_REPEAT_END;
|
|
94
54
|
let depth = 1;
|
|
95
55
|
child = child.nextSibling;
|
|
96
56
|
while (child && depth > 0) {
|
|
97
|
-
if (child.nodeType === 8
|
|
57
|
+
if (child.nodeType === 8) {
|
|
98
58
|
const d = child.data;
|
|
99
59
|
if (d === data)
|
|
100
60
|
depth++;
|
|
@@ -104,7 +64,6 @@ export function findByOrdinal(parent, nodeType, ordinal) {
|
|
|
104
64
|
if (depth > 0)
|
|
105
65
|
child = child.nextSibling;
|
|
106
66
|
}
|
|
107
|
-
// Advance past the closing marker itself
|
|
108
67
|
if (child)
|
|
109
68
|
child = child.nextSibling;
|
|
110
69
|
continue;
|
package/dist/element/styles.d.ts
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adopt a CSS module stylesheet onto a shadow root, or inject into `<head>`
|
|
3
|
-
* for light DOM components.
|
|
4
|
-
*
|
|
5
|
-
* For shadow DOM: uses `import(specifier, { with: { type: "css" } })` to
|
|
6
|
-
* retrieve the browser-registered CSSStyleSheet from the module registry.
|
|
7
|
-
* The browser caches the sheet internally — no application-level cache needed.
|
|
8
|
-
*
|
|
9
|
-
* For light DOM: appends a `<style>` element to `<head>` (once per specifier).
|
|
10
|
-
*/
|
|
11
1
|
export declare function injectModuleStyle(specifier: string, shadowRoot: ShadowRoot | null): void;
|
package/dist/element/styles.js
CHANGED
|
@@ -1,78 +1,17 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT license.
|
|
3
|
-
/**
|
|
4
|
-
* Stylesheet management for WebUI components.
|
|
5
|
-
*
|
|
6
|
-
* Three CSS strategies are supported:
|
|
7
|
-
*
|
|
8
|
-
* - **Link**: `<link rel="stylesheet">` tags in each component's shadow template.
|
|
9
|
-
* The browser deduplicates fetches by URL. No JS-side style management needed.
|
|
10
|
-
*
|
|
11
|
-
* - **Style**: Inline `<style>` tags inside each shadow template.
|
|
12
|
-
*
|
|
13
|
-
* - **Module**: Uses CSS Modules registered via Import Maps. During SSR, the
|
|
14
|
-
* handler emits a `<script type="importmap">{"imports":{"<tag>":"data:text/css,..."}}</script>`
|
|
15
|
-
* in each rendered component's light DOM. The browser registers the
|
|
16
|
-
* stylesheet globally under `<tag>` and automatically adopts it via
|
|
17
|
-
* `shadowrootadoptedstylesheets` on declarative shadow roots.
|
|
18
|
-
*
|
|
19
|
-
* During SPA navigation, the router appends new importmap script tags to
|
|
20
|
-
* `<head>` via `templateStyles[]`. The framework uses
|
|
21
|
-
* `import(specifier, { with: { type: "css" } })` to retrieve the browser's
|
|
22
|
-
* registered CSSStyleSheet and adopts it onto the shadow root. This is a
|
|
23
|
-
* direct hash-map lookup in the browser's module registry - no DOM queries,
|
|
24
|
-
* no manual CSSStyleSheet construction.
|
|
25
|
-
*
|
|
26
|
-
* For light DOM components (no shadow root), Module mode injects a `<style>`
|
|
27
|
-
* element in `<head>`, deduplicated by `headInjected`.
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* Specifiers already injected into `<head>` via the light DOM path.
|
|
31
|
-
* Prevents duplicate `<style>` elements for non-shadow components.
|
|
32
|
-
*/
|
|
33
1
|
const headInjected = new Set();
|
|
34
|
-
/**
|
|
35
|
-
* Adopt a CSS module stylesheet onto a shadow root, or inject into `<head>`
|
|
36
|
-
* for light DOM components.
|
|
37
|
-
*
|
|
38
|
-
* For shadow DOM: uses `import(specifier, { with: { type: "css" } })` to
|
|
39
|
-
* retrieve the browser-registered CSSStyleSheet from the module registry.
|
|
40
|
-
* The browser caches the sheet internally — no application-level cache needed.
|
|
41
|
-
*
|
|
42
|
-
* For light DOM: appends a `<style>` element to `<head>` (once per specifier).
|
|
43
|
-
*/
|
|
44
2
|
export function injectModuleStyle(specifier, shadowRoot) {
|
|
45
3
|
if (shadowRoot) {
|
|
46
|
-
// SSR hydration: the browser already adopted the sheet from
|
|
47
|
-
// shadowrootadoptedstylesheets on the declarative shadow root.
|
|
48
4
|
if (shadowRoot.adoptedStyleSheets.length > 0)
|
|
49
5
|
return;
|
|
50
|
-
// SPA path: import the CSS module from the browser's registry.
|
|
51
|
-
// The specifier was registered via a `<script type="importmap">` tag
|
|
52
|
-
// (either inlined at SSR time or appended to <head> by the router during
|
|
53
|
-
// partial navigation). The import resolves to the same CSSStyleSheet the
|
|
54
|
-
// browser registered.
|
|
55
6
|
import(specifier, { with: { type: 'css' } }).then((mod) => {
|
|
56
|
-
shadowRoot.adoptedStyleSheets
|
|
57
|
-
...shadowRoot.adoptedStyleSheets,
|
|
58
|
-
mod.default,
|
|
59
|
-
];
|
|
7
|
+
shadowRoot.adoptedStyleSheets.push(mod.default);
|
|
60
8
|
}, () => {
|
|
61
|
-
// Specifier not registered — component has no CSS module definition.
|
|
62
|
-
// This is expected for Link/Style strategies or components without CSS.
|
|
63
9
|
});
|
|
64
10
|
}
|
|
65
11
|
else if (!headInjected.has(specifier)) {
|
|
66
12
|
headInjected.add(specifier);
|
|
67
13
|
import(specifier, { with: { type: 'css' } }).then((mod) => {
|
|
68
|
-
|
|
69
|
-
const rules = mod.default.cssRules;
|
|
70
|
-
let cssText = '';
|
|
71
|
-
for (let i = 0; i < rules.length; i++) {
|
|
72
|
-
cssText += rules[i].cssText;
|
|
73
|
-
}
|
|
74
|
-
style.textContent = cssText;
|
|
75
|
-
document.head.appendChild(style);
|
|
14
|
+
document.adoptedStyleSheets.push(mod.default);
|
|
76
15
|
}, () => { });
|
|
77
16
|
}
|
|
78
17
|
}
|