assign-gingerly 0.0.57 → 0.0.59
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 +243 -131
- package/assignFrom.js +124 -129
- package/assignFrom.ts +146 -233
- package/assignFromAsync.js +118 -0
- package/assignFromAsync.ts +240 -0
- package/getValues.js +223 -0
- package/getValues.ts +255 -0
- package/handlers/join.ts +1 -1
- package/handlers/lazyLoad.js +2 -2
- package/handlers/lazyLoad.ts +3 -3
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.js +203 -0
- package/handlers/manageTemplateList.ts +240 -0
- package/handlers/microDataJoin.ts +1 -1
- package/index.js +1 -0
- package/index.ts +2 -0
- package/inferredAssignments.js +1 -1
- package/inferredAssignments.ts +2 -2
- package/markerUtils.js +136 -127
- package/package.json +18 -1
- package/processHandlerCommands.js +30 -3
- package/processHandlerCommands.ts +31 -7
- package/resolveValues.js +41 -125
- package/resolveValues.ts +131 -255
- package/transitionHelper.js +11 -5
- package/transitionHelper.ts +11 -5
- package/types/assign-gingerly/types.d.ts +51 -0
- package/waitForSettled.js +57 -0
- package/waitForSettled.ts +65 -0
package/getValues.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* getValues.ts — Synchronous value resolution for path strings.
|
|
3
|
+
*
|
|
4
|
+
* The synchronous counterpart to resolveValues. Resolves `?.`-prefixed path
|
|
5
|
+
* strings against a source object, with support for withMethods, aka aliases,
|
|
6
|
+
* synchronous protocols, arrays, and nested plain objects.
|
|
7
|
+
*
|
|
8
|
+
* For async protocol handlers, use resolveValues instead.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { getValues, getValue } from 'assign-gingerly/getValues.js';
|
|
12
|
+
*
|
|
13
|
+
* const result = getValues({
|
|
14
|
+
* name: '?.user?.name',
|
|
15
|
+
* greeting: '?.messages?.hello',
|
|
16
|
+
* count: 42
|
|
17
|
+
* }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { GetValuesOptions } from './types/assign-gingerly/types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Apply alias substitutions to a path string.
|
|
24
|
+
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
25
|
+
*/
|
|
26
|
+
function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
27
|
+
if (aliasMap.size === 0) return path;
|
|
28
|
+
const parts = path.split('?.');
|
|
29
|
+
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
30
|
+
return substituted.join('?.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Path cache for parsed path strings.
|
|
35
|
+
* Avoids re-splitting the same path on repeated calls.
|
|
36
|
+
*/
|
|
37
|
+
const pathCache = new Map<string, string[]>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a `?.`-delimited path string into segments, with caching.
|
|
41
|
+
*/
|
|
42
|
+
function parseCachedPath(path: string): string[] {
|
|
43
|
+
let parts = pathCache.get(path);
|
|
44
|
+
if (!parts) {
|
|
45
|
+
parts = path.split('?.').filter(p => p.length > 0);
|
|
46
|
+
pathCache.set(path, parts);
|
|
47
|
+
}
|
|
48
|
+
return parts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Navigate a path against a source object, optionally calling methods.
|
|
53
|
+
* Returns the resolved value at the end of the path.
|
|
54
|
+
*/
|
|
55
|
+
function navigatePath(
|
|
56
|
+
source: any,
|
|
57
|
+
parts: string[],
|
|
58
|
+
withMethods: Set<string> | undefined
|
|
59
|
+
): any {
|
|
60
|
+
let current = source;
|
|
61
|
+
let i = 0;
|
|
62
|
+
|
|
63
|
+
while (i < parts.length) {
|
|
64
|
+
if (current == null) return current;
|
|
65
|
+
|
|
66
|
+
const part = parts[i];
|
|
67
|
+
|
|
68
|
+
if (withMethods && withMethods.has(part)) {
|
|
69
|
+
const method = current[part];
|
|
70
|
+
if (typeof method === 'function') {
|
|
71
|
+
const nextPart = parts[i + 1];
|
|
72
|
+
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
73
|
+
current = method.call(current, nextPart);
|
|
74
|
+
i += 2;
|
|
75
|
+
} else {
|
|
76
|
+
current = method.call(current);
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
current = current[part];
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
current = current[part];
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks if a string value looks like a protocol reference.
|
|
94
|
+
*/
|
|
95
|
+
function hasProtocol(value: string): boolean {
|
|
96
|
+
return value.includes('://');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a protocol-prefixed value synchronously.
|
|
101
|
+
*/
|
|
102
|
+
function getProtocolValue(
|
|
103
|
+
value: string,
|
|
104
|
+
protocols: Record<string, (key: string) => any>,
|
|
105
|
+
options?: GetValuesOptions
|
|
106
|
+
): any {
|
|
107
|
+
const protoEnd = value.indexOf('://');
|
|
108
|
+
const protocol = value.substring(0, protoEnd);
|
|
109
|
+
|
|
110
|
+
const handler = protocols[protocol];
|
|
111
|
+
if (!handler) return value; // not a recognized protocol
|
|
112
|
+
|
|
113
|
+
const rest = value.substring(protoEnd + 3);
|
|
114
|
+
|
|
115
|
+
const pathStart = rest.indexOf('?.');
|
|
116
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
117
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
118
|
+
|
|
119
|
+
const resolved = handler(key);
|
|
120
|
+
|
|
121
|
+
if (path) {
|
|
122
|
+
return getValue(path, resolved, options);
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve path strings and protocols within an array (synchronous).
|
|
129
|
+
* Recurses into nested arrays and plain objects.
|
|
130
|
+
*/
|
|
131
|
+
function getArray(
|
|
132
|
+
arr: any[],
|
|
133
|
+
source: any,
|
|
134
|
+
aliasMap: Map<string, string>,
|
|
135
|
+
withMethods: Set<string> | undefined,
|
|
136
|
+
protocols: Record<string, (key: string) => any> | undefined,
|
|
137
|
+
options?: GetValuesOptions
|
|
138
|
+
): any[] {
|
|
139
|
+
const result: any[] = [];
|
|
140
|
+
for (const item of arr) {
|
|
141
|
+
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
142
|
+
const aliased = applyAliases(item, aliasMap);
|
|
143
|
+
const parts = parseCachedPath(aliased);
|
|
144
|
+
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
145
|
+
} else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
146
|
+
result.push(getProtocolValue(item, protocols, options));
|
|
147
|
+
} else if (Array.isArray(item)) {
|
|
148
|
+
result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
|
|
149
|
+
} else if (item && typeof item === 'object') {
|
|
150
|
+
const proto = Object.getPrototypeOf(item);
|
|
151
|
+
if (proto === Object.prototype || proto === null) {
|
|
152
|
+
result.push(getValues(item, source, options));
|
|
153
|
+
} else {
|
|
154
|
+
result.push(item);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
result.push(item);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Synchronously resolve RHS path strings in a pattern object against a source object.
|
|
165
|
+
*
|
|
166
|
+
* Any value that is a string starting with `?.` is treated as a path
|
|
167
|
+
* and resolved against the source object. Non-string values and strings
|
|
168
|
+
* not starting with `?.` pass through unchanged.
|
|
169
|
+
*
|
|
170
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
171
|
+
* @param source - Object to resolve paths against
|
|
172
|
+
* @param options - Optional withMethods, aka, and synchronous protocols
|
|
173
|
+
* @returns New object with path strings replaced by resolved values
|
|
174
|
+
*/
|
|
175
|
+
export function getValues(
|
|
176
|
+
pattern: Record<string, any>,
|
|
177
|
+
source: any,
|
|
178
|
+
options?: GetValuesOptions
|
|
179
|
+
): Record<string, any> {
|
|
180
|
+
// Build alias map
|
|
181
|
+
const aliasMap = new Map<string, string>();
|
|
182
|
+
if (options?.aka) {
|
|
183
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
184
|
+
aliasMap.set(alias, target);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build methods set
|
|
189
|
+
const withMethods = options?.withMethods
|
|
190
|
+
? options.withMethods instanceof Set
|
|
191
|
+
? options.withMethods
|
|
192
|
+
: new Set(options.withMethods)
|
|
193
|
+
: undefined;
|
|
194
|
+
|
|
195
|
+
const protocols = options?.protocols;
|
|
196
|
+
|
|
197
|
+
const result: Record<string, any> = {};
|
|
198
|
+
for (const [key, value] of Object.entries(pattern)) {
|
|
199
|
+
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
200
|
+
const aliased = applyAliases(value, aliasMap);
|
|
201
|
+
const parts = parseCachedPath(aliased);
|
|
202
|
+
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
203
|
+
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
204
|
+
result[key] = getProtocolValue(value, protocols, options);
|
|
205
|
+
} else if (Array.isArray(value)) {
|
|
206
|
+
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
|
|
207
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
208
|
+
const proto = Object.getPrototypeOf(value);
|
|
209
|
+
if (proto === Object.prototype || proto === null) {
|
|
210
|
+
result[key] = getValues(value, source, options);
|
|
211
|
+
} else {
|
|
212
|
+
result[key] = value;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
result[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Synchronously resolve a single `?.`-delimited path string against a source object.
|
|
223
|
+
*
|
|
224
|
+
* @param path - A `?.`-delimited path string (e.g., '?.user?.name')
|
|
225
|
+
* @param source - Object to resolve the path against
|
|
226
|
+
* @param options - Optional withMethods and aka
|
|
227
|
+
* @returns The resolved value, or undefined if any segment is nullish
|
|
228
|
+
*/
|
|
229
|
+
export function getValue(
|
|
230
|
+
path: string,
|
|
231
|
+
source: any,
|
|
232
|
+
options?: GetValuesOptions
|
|
233
|
+
): any {
|
|
234
|
+
if (!path.startsWith('?.')) return path;
|
|
235
|
+
|
|
236
|
+
let aliased = path;
|
|
237
|
+
if (options?.aka) {
|
|
238
|
+
const aliasMap = new Map<string, string>();
|
|
239
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
240
|
+
aliasMap.set(alias, target);
|
|
241
|
+
}
|
|
242
|
+
aliased = applyAliases(path, aliasMap);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parts = parseCachedPath(aliased);
|
|
246
|
+
if (parts.length === 0) return source;
|
|
247
|
+
|
|
248
|
+
const withMethods = options?.withMethods
|
|
249
|
+
? options.withMethods instanceof Set
|
|
250
|
+
? options.withMethods
|
|
251
|
+
: new Set(options.withMethods)
|
|
252
|
+
: undefined;
|
|
253
|
+
|
|
254
|
+
return navigatePath(source, parts, withMethods);
|
|
255
|
+
}
|
package/handlers/join.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import type { AssignFromHandler } from '../
|
|
34
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Process nested arrays with all-or-nothing null semantics.
|
package/handlers/lazyLoad.js
CHANGED
|
@@ -73,7 +73,7 @@ export class LazyLoadHandler {
|
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
75
|
if (transitional) {
|
|
76
|
-
ensureHideStyle(lhsTarget.getRootNode());
|
|
76
|
+
ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
|
|
77
77
|
withTransition(startMarker, 'show', true, () => {
|
|
78
78
|
this.cloneAndInsertSync(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
|
|
79
79
|
});
|
|
@@ -89,7 +89,7 @@ export class LazyLoadHandler {
|
|
|
89
89
|
[startMarker, endMarker] = createMarkers(lhsTarget, name, method);
|
|
90
90
|
}
|
|
91
91
|
if (transitional) {
|
|
92
|
-
ensureHideStyle(lhsTarget.getRootNode());
|
|
92
|
+
ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
|
|
93
93
|
withTransition(startMarker, 'show', true, () => {
|
|
94
94
|
this.cloneAndInsertSync(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
|
|
95
95
|
});
|
package/handlers/lazyLoad.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* }, { withMethods: ['querySelector'], from: myVM });
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import type { AssignFromHandler } from '../
|
|
22
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
23
23
|
import type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
|
|
24
24
|
import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
|
|
25
25
|
import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
|
|
@@ -102,7 +102,7 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
102
102
|
} else {
|
|
103
103
|
// Content was removed (forget mode) — re-clone
|
|
104
104
|
if (transitional) {
|
|
105
|
-
ensureHideStyle(lhsTarget.getRootNode());
|
|
105
|
+
ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
|
|
106
106
|
withTransition(startMarker, 'show', true, () => {
|
|
107
107
|
this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
|
|
108
108
|
});
|
|
@@ -118,7 +118,7 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
118
118
|
[startMarker, endMarker] = createMarkers(lhsTarget, name, method);
|
|
119
119
|
}
|
|
120
120
|
if (transitional) {
|
|
121
|
-
ensureHideStyle(lhsTarget.getRootNode());
|
|
121
|
+
ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
|
|
122
122
|
withTransition(startMarker, 'show', true, () => {
|
|
123
123
|
this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
|
|
124
124
|
});
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { LazyLoadHandler } from './lazyLoad.js';
|
|
24
|
-
import type { AssignFromHandler } from '../
|
|
24
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
25
25
|
import type { LazyLoadSwitchResolvedParams } from '../types/assign-gingerly/types.js';
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.manageTemplateList handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Clones a template once per item in an iterable, distributing each item's
|
|
5
|
+
* properties into its clone via assignFrom. Manages the list over time —
|
|
6
|
+
* reconciling by key to add, remove, and update-in-place.
|
|
7
|
+
*
|
|
8
|
+
* This handler is auto-loaded by processHandlerCommands when
|
|
9
|
+
* `do: 'builtIns.manageTemplateList'` is encountered.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* assignFrom(document.body, {
|
|
13
|
+
* '?.querySelector?.tbody =>': {
|
|
14
|
+
* do: 'builtIns.manageTemplateList',
|
|
15
|
+
* resolve: {
|
|
16
|
+
* forEach: '?.rankings',
|
|
17
|
+
* instantiate: 'globalThis://country-ranking',
|
|
18
|
+
* },
|
|
19
|
+
* fromEachItem: {
|
|
20
|
+
* assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
21
|
+
* withOptions: { withMethods: ['querySelector'], inferredAssignments: true },
|
|
22
|
+
* resolve: { key: '?.rank' }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
26
|
+
*/
|
|
27
|
+
import { findMarkers, createMarkers } from '../markerUtils.js';
|
|
28
|
+
import { resolveValue } from '../resolveValues.js';
|
|
29
|
+
import { assignFrom } from '../assignFrom.js';
|
|
30
|
+
import { processInferredAssignments } from '../inferredAssignments.js';
|
|
31
|
+
const listStateMap = new WeakMap();
|
|
32
|
+
/**
|
|
33
|
+
* ManageTemplateListHandler — clones a template per iterable item with keyed reconciliation.
|
|
34
|
+
*/
|
|
35
|
+
export class ManageTemplateListHandler {
|
|
36
|
+
config;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
}
|
|
40
|
+
async assign(lhsTarget, resolvedParams, options) {
|
|
41
|
+
//return;
|
|
42
|
+
const { forEach: items, instantiate, method = 'appendChild', forget = false, markerName, yieldEvery, } = resolvedParams;
|
|
43
|
+
if (!(lhsTarget instanceof Element)) {
|
|
44
|
+
throw new Error('builtIns.manageTemplateList: lhsTarget must be a DOM Element');
|
|
45
|
+
}
|
|
46
|
+
if (!items || typeof items[Symbol.iterator] !== 'function') {
|
|
47
|
+
return; // Nothing to iterate
|
|
48
|
+
}
|
|
49
|
+
const fromEachItem = this.config.fromEachItem;
|
|
50
|
+
const assignToFragment = fromEachItem?.assignToFragment ?? {};
|
|
51
|
+
const withOptions = fromEachItem?.withOptions ?? {};
|
|
52
|
+
const perItemResolve = fromEachItem?.resolve ?? {};
|
|
53
|
+
const keyPath = perItemResolve.key; // e.g., '?.rank'
|
|
54
|
+
// fromSource config — assigns from the outer `from` (parent VM) to each clone
|
|
55
|
+
const fromSource = this.config.fromSource;
|
|
56
|
+
const sourceAssignToFragment = fromSource?.assignToFragment;
|
|
57
|
+
const sourceWithOptions = fromSource?.withOptions ?? {};
|
|
58
|
+
// Detect fast path: no assignToFragment patterns, just inferredAssignments
|
|
59
|
+
const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
|
|
60
|
+
const inferredConfig = withOptions.inferredAssignments;
|
|
61
|
+
const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
62
|
+
const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
|
|
63
|
+
// Find or create markers
|
|
64
|
+
let [startMarker, endMarker] = findMarkers(lhsTarget, name);
|
|
65
|
+
if (!startMarker || !endMarker) {
|
|
66
|
+
[startMarker, endMarker] = createMarkers(lhsTarget, name, method);
|
|
67
|
+
}
|
|
68
|
+
// Get or create list state
|
|
69
|
+
let state = listStateMap.get(startMarker);
|
|
70
|
+
if (!state) {
|
|
71
|
+
state = { keyToNodes: new Map(), keyOrder: [] };
|
|
72
|
+
listStateMap.set(startMarker, state);
|
|
73
|
+
}
|
|
74
|
+
// Convert iterable to array
|
|
75
|
+
const itemsArray = Array.from(items);
|
|
76
|
+
// Fast path: use processInferredAssignments directly when possible
|
|
77
|
+
const processInferred = useFastPath ? processInferredAssignments : null;
|
|
78
|
+
const newKeys = itemsArray.map((item, index) => {
|
|
79
|
+
if (keyPath) {
|
|
80
|
+
return resolveValue(keyPath, item);
|
|
81
|
+
}
|
|
82
|
+
return index; // Positional fallback when no key specified
|
|
83
|
+
});
|
|
84
|
+
// Determine what changed
|
|
85
|
+
const oldKeys = new Set(state.keyOrder);
|
|
86
|
+
const newKeySet = new Set(newKeys);
|
|
87
|
+
// Keys to remove
|
|
88
|
+
for (const oldKey of state.keyOrder) {
|
|
89
|
+
if (!newKeySet.has(oldKey)) {
|
|
90
|
+
const nodes = state.keyToNodes.get(oldKey);
|
|
91
|
+
if (nodes) {
|
|
92
|
+
if (forget) {
|
|
93
|
+
for (const node of nodes)
|
|
94
|
+
node.parentNode?.removeChild(node);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const node of nodes) {
|
|
98
|
+
if (node instanceof Element)
|
|
99
|
+
node.setAttribute('hidden', '');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
state.keyToNodes.delete(oldKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Process items — clone new ones, update existing
|
|
107
|
+
const fragment = document.createDocumentFragment();
|
|
108
|
+
const newKeyToNodes = new Map();
|
|
109
|
+
for (let i = 0; i < itemsArray.length; i++) {
|
|
110
|
+
const item = itemsArray[i];
|
|
111
|
+
const key = newKeys[i];
|
|
112
|
+
if (state.keyToNodes.has(key) && oldKeys.has(key)) {
|
|
113
|
+
// Existing item — update in place
|
|
114
|
+
const existingNodes = state.keyToNodes.get(key);
|
|
115
|
+
const rootEl = existingNodes.find(n => n instanceof Element);
|
|
116
|
+
if (rootEl) {
|
|
117
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
118
|
+
if (shouldYield)
|
|
119
|
+
await new Promise(r => setTimeout(r, 0));
|
|
120
|
+
if (processInferred) {
|
|
121
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
125
|
+
}
|
|
126
|
+
if (sourceAssignToFragment && options?.from) {
|
|
127
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
newKeyToNodes.set(key, existingNodes);
|
|
131
|
+
for (const node of existingNodes) {
|
|
132
|
+
if (node instanceof Element)
|
|
133
|
+
node.removeAttribute('hidden');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// New item — clone template
|
|
138
|
+
let content;
|
|
139
|
+
if (instantiate instanceof HTMLTemplateElement) {
|
|
140
|
+
content = instantiate.content.cloneNode(true);
|
|
141
|
+
}
|
|
142
|
+
else if (instantiate instanceof DocumentFragment) {
|
|
143
|
+
content = instantiate.cloneNode(true);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw new Error('builtIns.manageTemplateList: instantiate must be an HTMLTemplateElement or DocumentFragment');
|
|
147
|
+
}
|
|
148
|
+
const clonedNodes = Array.from(content.childNodes);
|
|
149
|
+
// Apply per-item assignments to the cloned fragment
|
|
150
|
+
const rootEl = clonedNodes.find(n => n instanceof Element);
|
|
151
|
+
if (rootEl) {
|
|
152
|
+
const tempContainer = document.createDocumentFragment();
|
|
153
|
+
tempContainer.appendChild(content);
|
|
154
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
155
|
+
if (shouldYield)
|
|
156
|
+
await new Promise(r => setTimeout(r, 0));
|
|
157
|
+
if (processInferred) {
|
|
158
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
162
|
+
}
|
|
163
|
+
if (sourceAssignToFragment && options?.from) {
|
|
164
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const node of clonedNodes) {
|
|
168
|
+
fragment.appendChild(node);
|
|
169
|
+
}
|
|
170
|
+
newKeyToNodes.set(key, clonedNodes);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Wait for async rendering in the fragment to settle before committing
|
|
174
|
+
if (fragment.childNodes.length > 0) {
|
|
175
|
+
const waitOpt = resolvedParams.waitForSettled;
|
|
176
|
+
if (waitOpt) {
|
|
177
|
+
const { waitForSettled } = await import('../waitForSettled.js');
|
|
178
|
+
const idleMs = typeof waitOpt === 'object' ? waitOpt.idleMs : 100;
|
|
179
|
+
const timeout = typeof waitOpt === 'object' ? waitOpt.timeout : undefined;
|
|
180
|
+
try {
|
|
181
|
+
await waitForSettled(fragment, idleMs, timeout);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
console.warn('builtIns.manageTemplateList:', e.message, '— inserting fragment anyway');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Insert new fragment before end marker
|
|
188
|
+
endMarker.parentNode.insertBefore(fragment, endMarker);
|
|
189
|
+
}
|
|
190
|
+
// Update state
|
|
191
|
+
state.keyToNodes = newKeyToNodes;
|
|
192
|
+
state.keyOrder = newKeys;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get marker name from a template element.
|
|
197
|
+
*/
|
|
198
|
+
function getMarkerName(templateEl) {
|
|
199
|
+
if (templateEl instanceof HTMLTemplateElement) {
|
|
200
|
+
return templateEl.id || undefined;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|