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/resolveValues.ts
CHANGED
|
@@ -1,55 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* resolveValues.ts — Async value resolution for path strings.
|
|
3
|
+
*
|
|
4
|
+
* Thin async wrapper around getValues that adds support for async protocol handlers.
|
|
5
|
+
* For synchronous-only use cases, import getValues/getValue directly for better performance.
|
|
6
|
+
*
|
|
7
|
+
* Re-exports ResolveValuesOptions for backward compatibility.
|
|
3
8
|
*/
|
|
4
|
-
export interface ResolveValuesOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Method names that should be called instead of accessed as properties.
|
|
7
|
-
* When a path segment matches, it's called as a method with the next segment as argument.
|
|
8
|
-
*/
|
|
9
|
-
withMethods?: string[] | Set<string>;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Alias mappings for path segments.
|
|
13
|
-
* Substituted before path resolution, matching complete tokens between `?.` delimiters.
|
|
14
|
-
*/
|
|
15
|
-
aka?: Record<string, string>;
|
|
16
9
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
protocols?: Record<string, (key: string) => any | Promise<any>>;
|
|
10
|
+
import { getValue } from './getValues.js';
|
|
11
|
+
import type { ResolveValuesOptions } from './types/assign-gingerly/types.js';
|
|
12
|
+
|
|
13
|
+
export type { ResolveValuesOptions };
|
|
14
|
+
|
|
15
|
+
// Re-export getValue as resolveValue for backward compatibility
|
|
16
|
+
export { getValue as resolveValue };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a string value looks like a protocol reference.
|
|
20
|
+
*/
|
|
21
|
+
function hasProtocol(value: string): boolean {
|
|
22
|
+
return value.includes('://');
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
/**
|
|
34
26
|
* Apply alias substitutions to a path string.
|
|
35
|
-
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
36
27
|
*/
|
|
37
28
|
function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
if (aliasMap.size === 0) return path;
|
|
30
|
+
const parts = path.split('?.');
|
|
31
|
+
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
32
|
+
return substituted.join('?.');
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
/**
|
|
45
36
|
* Path cache for parsed path strings.
|
|
46
|
-
* Avoids re-splitting the same path on repeated calls.
|
|
47
37
|
*/
|
|
48
38
|
const pathCache = new Map<string, string[]>();
|
|
49
39
|
|
|
50
|
-
/**
|
|
51
|
-
* Parse a `?.`-delimited path string into segments, with caching.
|
|
52
|
-
*/
|
|
53
40
|
function parseCachedPath(path: string): string[] {
|
|
54
41
|
let parts = pathCache.get(path);
|
|
55
42
|
if (!parts) {
|
|
@@ -60,265 +47,154 @@ function parseCachedPath(path: string): string[] {
|
|
|
60
47
|
}
|
|
61
48
|
|
|
62
49
|
/**
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
50
|
+
* Navigate a path against a source object, optionally calling methods.
|
|
51
|
+
*/
|
|
52
|
+
function navigatePath(
|
|
53
|
+
source: any,
|
|
54
|
+
parts: string[],
|
|
55
|
+
withMethods: Set<string> | undefined
|
|
56
|
+
): any {
|
|
57
|
+
let current = source;
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < parts.length) {
|
|
60
|
+
if (current == null) return current;
|
|
61
|
+
const part = parts[i];
|
|
62
|
+
if (withMethods && withMethods.has(part)) {
|
|
63
|
+
const method = current[part];
|
|
64
|
+
if (typeof method === 'function') {
|
|
65
|
+
const nextPart = parts[i + 1];
|
|
66
|
+
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
67
|
+
current = method.call(current, nextPart);
|
|
68
|
+
i += 2;
|
|
69
|
+
} else {
|
|
70
|
+
current = method.call(current);
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
current = current[part];
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
current = current[part];
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolves a protocol-prefixed value asynchronously.
|
|
70
87
|
*/
|
|
71
88
|
async function resolveProtocolValue(
|
|
72
89
|
value: string,
|
|
73
90
|
protocols: Record<string, (key: string) => any | Promise<any>>,
|
|
74
91
|
options?: ResolveValuesOptions
|
|
75
92
|
): Promise<any> {
|
|
76
|
-
// Extract protocol name (before ://)
|
|
77
93
|
const protoEnd = value.indexOf('://');
|
|
78
94
|
const protocol = value.substring(0, protoEnd);
|
|
79
95
|
|
|
80
|
-
// Resolve via protocol handler
|
|
81
96
|
const handler = protocols[protocol];
|
|
82
|
-
if (!handler) return value;
|
|
97
|
+
if (!handler) return value;
|
|
83
98
|
|
|
84
99
|
const rest = value.substring(protoEnd + 3);
|
|
85
|
-
|
|
86
|
-
// Split at first ?. to separate key from path
|
|
87
100
|
const pathStart = rest.indexOf('?.');
|
|
88
101
|
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
89
102
|
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
90
103
|
|
|
91
104
|
const resolved = await handler(key);
|
|
92
105
|
|
|
93
|
-
// If there's a remaining path, resolve it against the result
|
|
94
106
|
if (path) {
|
|
95
|
-
return
|
|
107
|
+
return getValue(path, resolved, options);
|
|
96
108
|
}
|
|
97
109
|
return resolved;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
/**
|
|
101
|
-
*
|
|
102
|
-
*/
|
|
103
|
-
function hasProtocol(value: string): boolean {
|
|
104
|
-
return value.includes('://');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Navigate a path against a source object, optionally calling methods.
|
|
109
|
-
* Returns the resolved value at the end of the path.
|
|
110
|
-
*/
|
|
111
|
-
function navigatePath(
|
|
112
|
-
source: any,
|
|
113
|
-
parts: string[],
|
|
114
|
-
withMethods: Set<string> | undefined
|
|
115
|
-
): any {
|
|
116
|
-
let current = source;
|
|
117
|
-
let i = 0;
|
|
118
|
-
|
|
119
|
-
while (i < parts.length) {
|
|
120
|
-
if (current == null) return current;
|
|
121
|
-
|
|
122
|
-
const part = parts[i];
|
|
123
|
-
|
|
124
|
-
if (withMethods && withMethods.has(part)) {
|
|
125
|
-
const method = current[part];
|
|
126
|
-
if (typeof method === 'function') {
|
|
127
|
-
const nextPart = parts[i + 1];
|
|
128
|
-
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
129
|
-
// Call method with next segment as argument, consume it
|
|
130
|
-
current = method.call(current, nextPart);
|
|
131
|
-
i += 2;
|
|
132
|
-
} else {
|
|
133
|
-
// Consecutive methods or last segment — call with no args
|
|
134
|
-
current = method.call(current);
|
|
135
|
-
i++;
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
current = current[part];
|
|
139
|
-
i++;
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
current = current[part];
|
|
143
|
-
i++;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return current;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve path strings and protocol references within an array.
|
|
152
|
-
* Recurses into nested arrays and plain objects. Non-string elements,
|
|
153
|
-
* class instances, and other non-plain objects pass through unchanged.
|
|
113
|
+
* Resolve path strings and protocol references within an array (async).
|
|
154
114
|
*/
|
|
155
115
|
async function resolveArray(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
116
|
+
arr: any[],
|
|
117
|
+
source: any,
|
|
118
|
+
aliasMap: Map<string, string>,
|
|
119
|
+
withMethods: Set<string> | undefined,
|
|
120
|
+
protocols: Record<string, (key: string) => any | Promise<any>> | undefined,
|
|
121
|
+
options?: ResolveValuesOptions
|
|
162
122
|
): Promise<any[]> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
123
|
+
const result: any[] = [];
|
|
124
|
+
for (const item of arr) {
|
|
125
|
+
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
126
|
+
const aliased = applyAliases(item, aliasMap);
|
|
127
|
+
const parts = parseCachedPath(aliased);
|
|
128
|
+
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
129
|
+
} else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
130
|
+
result.push(await resolveProtocolValue(item, protocols, options));
|
|
131
|
+
} else if (Array.isArray(item)) {
|
|
132
|
+
result.push(await resolveArray(item, source, aliasMap, withMethods, protocols, options));
|
|
133
|
+
} else if (item && typeof item === 'object') {
|
|
134
|
+
const proto = Object.getPrototypeOf(item);
|
|
135
|
+
if (proto === Object.prototype || proto === null) {
|
|
136
|
+
result.push(await resolveValues(item, source, options));
|
|
137
|
+
} else {
|
|
138
|
+
result.push(item);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
result.push(item);
|
|
142
|
+
}
|
|
182
143
|
}
|
|
183
|
-
|
|
184
|
-
return result;
|
|
144
|
+
return result;
|
|
185
145
|
}
|
|
186
146
|
|
|
187
147
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* Any value that is a string starting with `?.` is treated as a path
|
|
191
|
-
* and resolved against the source object using optional chaining semantics.
|
|
192
|
-
* Non-string values and strings not starting with `?.` pass through unchanged.
|
|
148
|
+
* Async resolve RHS path strings in a pattern object against a source object.
|
|
193
149
|
*
|
|
194
|
-
* Supports
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* Special case: `'?.'` (empty path) resolves to the source object itself.
|
|
150
|
+
* Supports async protocol handlers (e.g., fetch, IndexedDB).
|
|
151
|
+
* For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
|
|
198
152
|
*
|
|
199
153
|
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
200
154
|
* @param source - Object to resolve paths against
|
|
201
|
-
* @param options - Optional withMethods
|
|
155
|
+
* @param options - Optional withMethods, aka, and protocol handlers
|
|
202
156
|
* @returns New object with path strings replaced by resolved values
|
|
203
|
-
*
|
|
204
|
-
* @example
|
|
205
|
-
* const result = resolveValues({
|
|
206
|
-
* hello: '?.myPropContainer?.stringProp',
|
|
207
|
-
* foo: '?.myFooString',
|
|
208
|
-
* literal: 42
|
|
209
|
-
* }, source);
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* // With methods and aliases
|
|
213
|
-
* const result = resolveValues({
|
|
214
|
-
* text: '?.q?..username?.textContent'
|
|
215
|
-
* }, source, {
|
|
216
|
-
* withMethods: ['querySelector'],
|
|
217
|
-
* aka: { 'q': 'querySelector' }
|
|
218
|
-
* });
|
|
219
157
|
*/
|
|
220
158
|
export async function resolveValues(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
159
|
+
pattern: Record<string, any>,
|
|
160
|
+
source: any,
|
|
161
|
+
options?: ResolveValuesOptions
|
|
224
162
|
): Promise<Record<string, any>> {
|
|
225
|
-
// Build alias map
|
|
226
|
-
const aliasMap = new Map<string, string>();
|
|
227
|
-
if (options?.aka) {
|
|
228
|
-
for (const [alias, target] of Object.entries(options.aka)) {
|
|
229
|
-
aliasMap.set(alias, target);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Build methods set
|
|
234
|
-
const withMethods = options?.withMethods
|
|
235
|
-
? options.withMethods instanceof Set
|
|
236
|
-
? options.withMethods
|
|
237
|
-
: new Set(options.withMethods)
|
|
238
|
-
: undefined;
|
|
239
|
-
|
|
240
|
-
const protocols = options?.protocols;
|
|
241
|
-
|
|
242
|
-
const result: Record<string, any> = {};
|
|
243
|
-
for (const [key, value] of Object.entries(pattern)) {
|
|
244
|
-
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
245
|
-
// Apply aliases to the RHS path
|
|
246
|
-
const aliased = applyAliases(value, aliasMap);
|
|
247
|
-
|
|
248
|
-
// Parse path with caching
|
|
249
|
-
const parts = parseCachedPath(aliased);
|
|
250
|
-
|
|
251
|
-
// Navigate with method support
|
|
252
|
-
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
253
|
-
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
254
|
-
// Protocol-prefixed value — resolve asynchronously
|
|
255
|
-
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
256
|
-
} else if (Array.isArray(value)) {
|
|
257
|
-
// Resolve path strings and protocols within arrays (recursing into nested arrays)
|
|
258
|
-
result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
|
|
259
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
260
|
-
// Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
|
|
261
|
-
// Only recurse into plain objects — skip DOM elements, class instances, etc.
|
|
262
|
-
const proto = Object.getPrototypeOf(value);
|
|
263
|
-
if (proto === Object.prototype || proto === null) {
|
|
264
|
-
result[key] = await resolveValues(value, source, options);
|
|
265
|
-
} else {
|
|
266
|
-
result[key] = value;
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
result[key] = value;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return result;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Resolve a single `?.`-delimited path string against a source object.
|
|
277
|
-
*
|
|
278
|
-
* This is a lighter-weight alternative to `resolveValues` when you only need
|
|
279
|
-
* to resolve one path and don't want the overhead of creating wrapper objects.
|
|
280
|
-
*
|
|
281
|
-
* @param path - A `?.`-delimited path string (e.g., '?.behaviors?.command')
|
|
282
|
-
* @param source - Object to resolve the path against
|
|
283
|
-
* @param options - Optional withMethods and aka for method calls and aliases
|
|
284
|
-
* @returns The resolved value, or undefined if any segment is nullish
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* const value = resolveValue('?.behaviors?.commandBehavior?.command', el);
|
|
288
|
-
*
|
|
289
|
-
* @example
|
|
290
|
-
* const value = resolveValue('?.q?.myEl?.textContent', el, {
|
|
291
|
-
* withMethods: ['querySelector'],
|
|
292
|
-
* aka: { 'q': 'querySelector' }
|
|
293
|
-
* });
|
|
294
|
-
*/
|
|
295
|
-
export function resolveValue(
|
|
296
|
-
path: string,
|
|
297
|
-
source: any,
|
|
298
|
-
options?: ResolveValuesOptions
|
|
299
|
-
): any {
|
|
300
|
-
if (!path.startsWith('?.')) return path;
|
|
301
|
-
|
|
302
|
-
// Build alias map
|
|
303
|
-
let aliased = path;
|
|
304
|
-
if (options?.aka) {
|
|
305
163
|
const aliasMap = new Map<string, string>();
|
|
306
|
-
|
|
307
|
-
|
|
164
|
+
if (options?.aka) {
|
|
165
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
166
|
+
aliasMap.set(alias, target);
|
|
167
|
+
}
|
|
308
168
|
}
|
|
309
|
-
aliased = applyAliases(path, aliasMap);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Parse path with caching
|
|
313
|
-
const parts = parseCachedPath(aliased);
|
|
314
|
-
if (parts.length === 0) return source;
|
|
315
169
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
170
|
+
const withMethods = options?.withMethods
|
|
171
|
+
? options.withMethods instanceof Set
|
|
172
|
+
? options.withMethods
|
|
173
|
+
: new Set(options.withMethods)
|
|
174
|
+
: undefined;
|
|
175
|
+
|
|
176
|
+
const protocols = options?.protocols;
|
|
177
|
+
|
|
178
|
+
const result: Record<string, any> = {};
|
|
179
|
+
for (const [key, value] of Object.entries(pattern)) {
|
|
180
|
+
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
181
|
+
const aliased = applyAliases(value, aliasMap);
|
|
182
|
+
const parts = parseCachedPath(aliased);
|
|
183
|
+
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
184
|
+
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
185
|
+
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
186
|
+
} else if (Array.isArray(value)) {
|
|
187
|
+
result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
|
|
188
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
189
|
+
const proto = Object.getPrototypeOf(value);
|
|
190
|
+
if (proto === Object.prototype || proto === null) {
|
|
191
|
+
result[key] = await resolveValues(value, source, options);
|
|
192
|
+
} else {
|
|
193
|
+
result[key] = value;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
result[key] = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
324
200
|
}
|
package/transitionHelper.js
CHANGED
|
@@ -77,9 +77,9 @@ export function withTransition(markerNode, direction, transitional, domMutation)
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Track which
|
|
80
|
+
* Track which (rootNode, className) combos already have styles injected.
|
|
81
81
|
*/
|
|
82
|
-
const styleInjected = new
|
|
82
|
+
const styleInjected = new WeakMap();
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Default CSS class name for hidden elements during transitions.
|
|
@@ -87,7 +87,7 @@ const styleInjected = new WeakSet();
|
|
|
87
87
|
export const DEFAULT_HIDE_CLASS = 'ag-hide';
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Ensure the hide class style is injected into the rootNode (once per rootNode).
|
|
90
|
+
* Ensure the hide class style is injected into the rootNode (once per rootNode + className combo).
|
|
91
91
|
*
|
|
92
92
|
* @param {any} rootNode - The Document, ShadowRoot, or element root to inject into
|
|
93
93
|
* @param {string} [hideClass='ag-hide'] - CSS class name
|
|
@@ -100,8 +100,14 @@ export function ensureHideStyle(rootNode, hideClass = DEFAULT_HIDE_CLASS, hideCs
|
|
|
100
100
|
target = document.head;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
let injectedClasses = styleInjected.get(target);
|
|
104
|
+
if (!injectedClasses) {
|
|
105
|
+
injectedClasses = new Set();
|
|
106
|
+
styleInjected.set(target, injectedClasses);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (injectedClasses.has(hideClass)) return;
|
|
110
|
+
injectedClasses.add(hideClass);
|
|
105
111
|
|
|
106
112
|
const style = document.createElement('style');
|
|
107
113
|
style.textContent = `.${hideClass} { ${hideCss} }`;
|
package/transitionHelper.ts
CHANGED
|
@@ -91,9 +91,9 @@ export function withTransition(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Track which
|
|
94
|
+
* Track which (rootNode, className) combos already have styles injected.
|
|
95
95
|
*/
|
|
96
|
-
const styleInjected = new
|
|
96
|
+
const styleInjected = new WeakMap<object, Set<string>>();
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Default CSS class name for hidden elements during transitions.
|
|
@@ -106,7 +106,7 @@ export const DEFAULT_HIDE_CLASS = 'ag-hide';
|
|
|
106
106
|
const DEFAULT_HIDE_CSS = `display: none`;
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Ensure the hide class style is injected into the rootNode (once per rootNode).
|
|
109
|
+
* Ensure the hide class style is injected into the rootNode (once per rootNode + className combo).
|
|
110
110
|
*
|
|
111
111
|
* @param rootNode - The Document, ShadowRoot, or element root to inject into
|
|
112
112
|
* @param hideClass - CSS class name (default: 'ag-hide')
|
|
@@ -123,8 +123,14 @@ export function ensureHideStyle(
|
|
|
123
123
|
target = document.head;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
let injectedClasses = styleInjected.get(target);
|
|
127
|
+
if (!injectedClasses) {
|
|
128
|
+
injectedClasses = new Set();
|
|
129
|
+
styleInjected.set(target, injectedClasses);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (injectedClasses.has(hideClass)) return;
|
|
133
|
+
injectedClasses.add(hideClass);
|
|
128
134
|
|
|
129
135
|
const style = document.createElement('style');
|
|
130
136
|
style.textContent = `.${hideClass} { ${hideCss} }`;
|
|
@@ -261,6 +261,37 @@ export interface IAssignGingerlyOptions {
|
|
|
261
261
|
signal?: AbortSignal;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Options for synchronous value resolution (getValues / getValue).
|
|
266
|
+
* Extends IAssignGingerlyOptions with synchronous protocol handlers.
|
|
267
|
+
*/
|
|
268
|
+
export interface GetValuesOptions extends IAssignGingerlyOptions {
|
|
269
|
+
/**
|
|
270
|
+
* Synchronous protocol handlers for resolving protocol-prefixed values.
|
|
271
|
+
* Each handler receives the key portion and MUST return synchronously.
|
|
272
|
+
* For async protocols, use ResolveValuesOptions / resolveValues instead.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* protocols: {
|
|
276
|
+
* globalThis: (key) => globalThis[key],
|
|
277
|
+
* localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
|
|
278
|
+
* }
|
|
279
|
+
*/
|
|
280
|
+
protocols?: Record<string, (key: string) => any>;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Options for async value resolution (resolveValues).
|
|
285
|
+
* Same as GetValuesOptions but protocol handlers may return Promises.
|
|
286
|
+
*/
|
|
287
|
+
export interface ResolveValuesOptions extends IAssignGingerlyOptions {
|
|
288
|
+
/**
|
|
289
|
+
* Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
|
|
290
|
+
* Each handler receives the key portion and returns the resolved value (sync or async).
|
|
291
|
+
*/
|
|
292
|
+
protocols?: Record<string, (key: string) => any | Promise<any>>;
|
|
293
|
+
}
|
|
294
|
+
|
|
264
295
|
/**
|
|
265
296
|
* Event dispatched when enhancement configs are registered
|
|
266
297
|
*/
|
|
@@ -628,6 +659,26 @@ export interface LazyLoadSwitchResolvedParams extends Omit<LazyLoadResolvedParam
|
|
|
628
659
|
rhs: any;
|
|
629
660
|
}
|
|
630
661
|
|
|
662
|
+
/**
|
|
663
|
+
* Resolved parameters received by ManageTemplateListHandler.assign().
|
|
664
|
+
*/
|
|
665
|
+
export interface ManageTemplateListResolvedParams {
|
|
666
|
+
/** The iterable to loop over (resolved from VM) */
|
|
667
|
+
forEach: Iterable<any>;
|
|
668
|
+
/** Template element to clone per item */
|
|
669
|
+
instantiate: HTMLTemplateElement | DocumentFragment;
|
|
670
|
+
/** Insertion method (default: 'appendChild') */
|
|
671
|
+
method?: 'appendChild' | 'prepend' | 'after';
|
|
672
|
+
/** Remove nodes on hide instead of using hidden attribute */
|
|
673
|
+
forget?: boolean;
|
|
674
|
+
/** Override auto-derived marker name */
|
|
675
|
+
markerName?: string;
|
|
676
|
+
/** Wait for async rendering before DOM commit */
|
|
677
|
+
waitForSettled?: boolean | { idleMs?: number; timeout?: number };
|
|
678
|
+
/** Yield to browser every N items to prevent jank (default: undefined = no yielding) */
|
|
679
|
+
yieldEvery?: number;
|
|
680
|
+
}
|
|
681
|
+
|
|
631
682
|
/**
|
|
632
683
|
* Context passed to onInstantiated callbacks after template cloning.
|
|
633
684
|
*/
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* waitForSettled.ts — Waits for a DOM subtree to "settle" (mutations stop cascading).
|
|
3
|
+
*
|
|
4
|
+
* Observes a node for DOM mutations and debounces: each mutation resets an idle timer.
|
|
5
|
+
* When no mutations have occurred for `idleMs` milliseconds, the promise resolves.
|
|
6
|
+
*
|
|
7
|
+
* Useful for waiting for async rendering (itemscope managers, enhancements, features)
|
|
8
|
+
* to complete inside a DocumentFragment before committing to the live DOM.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { waitForSettled } from 'assign-gingerly/waitForSettled.js';
|
|
12
|
+
*
|
|
13
|
+
* const fragment = document.createDocumentFragment();
|
|
14
|
+
* // ... clone and assign into fragment ...
|
|
15
|
+
* await waitForSettled(fragment, 100, 2000);
|
|
16
|
+
* target.appendChild(fragment);
|
|
17
|
+
*
|
|
18
|
+
* @param root - The node to observe (typically a DocumentFragment or Element)
|
|
19
|
+
* @param idleMs - Debounce window in milliseconds. Default: 100
|
|
20
|
+
* @param timeout - Maximum wait time in milliseconds. If exceeded, rejects. Default: none (infinite)
|
|
21
|
+
*/
|
|
22
|
+
export function waitForSettled(root, idleMs = 100, timeout) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let timer;
|
|
25
|
+
let maxTimer;
|
|
26
|
+
const mo = new MutationObserver(() => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
timer = setTimeout(() => {
|
|
29
|
+
mo.disconnect();
|
|
30
|
+
if (maxTimer)
|
|
31
|
+
clearTimeout(maxTimer);
|
|
32
|
+
resolve();
|
|
33
|
+
}, idleMs);
|
|
34
|
+
});
|
|
35
|
+
mo.observe(root, {
|
|
36
|
+
childList: true,
|
|
37
|
+
subtree: true,
|
|
38
|
+
attributes: true,
|
|
39
|
+
characterData: true
|
|
40
|
+
});
|
|
41
|
+
// Initial timer — resolves if no mutations happen at all
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
mo.disconnect();
|
|
44
|
+
if (maxTimer)
|
|
45
|
+
clearTimeout(maxTimer);
|
|
46
|
+
resolve();
|
|
47
|
+
}, idleMs);
|
|
48
|
+
// Maximum timeout — rejects if mutations never quiesce
|
|
49
|
+
if (timeout !== undefined) {
|
|
50
|
+
maxTimer = setTimeout(() => {
|
|
51
|
+
mo.disconnect();
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
reject(new Error(`waitForSettled: mutations did not quiesce within ${timeout}ms`));
|
|
54
|
+
}, timeout);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|