assign-gingerly 0.0.64 → 0.0.66
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 +85 -26
- package/assignFrom.js +21 -19
- package/assignFrom.ts +28 -25
- package/assignFromAsync.js +10 -7
- package/assignFromAsync.ts +15 -98
- package/assignGingerly.js +54 -26
- package/assignGingerly.ts +62 -27
- package/assignTentatively.js +8 -0
- package/assignTentatively.ts +6 -0
- package/{builtInEmoji.ts → emojis.js} +46 -34
- package/emojis.ts +52 -0
- package/getValues.js +80 -25
- package/getValues.ts +85 -26
- package/handlers/addEventListener.js +153 -0
- package/handlers/addEventListener.ts +178 -0
- package/handlers/arr.js +13 -0
- package/handlers/arr.ts +13 -0
- package/handlers/lazyLoad.js +3 -3
- package/handlers/lazyLoad.ts +3 -3
- package/handlers/manageTemplateList.js +48 -21
- package/handlers/manageTemplateList.ts +51 -21
- package/handlers/nudge.js +23 -0
- package/handlers/nudge.ts +23 -0
- package/index.js +2 -0
- package/index.ts +2 -0
- package/inferencer/types/assign-gingerly/types.d.ts +327 -1
- package/inferencer/types/mount-observer/types.d.ts +10 -2
- package/inferencer/types/roundabout/types.d.ts +1 -1
- package/inferencer/types/three-peat/types.d.ts +18 -0
- package/object-extension.js +1 -1
- package/package.json +12 -4
- package/paths.ts +1 -1
- package/{withIdsCorrector.js → pinCorrector.js} +5 -5
- package/{withIdsCorrector.ts → pinCorrector.ts} +5 -5
- package/processHandlerCommands.js +4 -2
- package/processHandlerCommands.ts +4 -2
- package/resolveIdRef.js +8 -8
- package/resolveIdRef.ts +9 -9
- package/resolveValues.js +20 -88
- package/resolveValues.ts +17 -91
- package/types/assign-gingerly/types.d.ts +159 -3
- package/builtInEmoji.js +0 -26
package/getValues.js
CHANGED
|
@@ -16,6 +16,38 @@
|
|
|
16
16
|
* count: 42
|
|
17
17
|
* }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
|
|
18
18
|
*/
|
|
19
|
+
export function normalizeAliasOptions(options) {
|
|
20
|
+
const aliasMap = new Map();
|
|
21
|
+
if (options?.aka) {
|
|
22
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
23
|
+
if (alias.includes(' ') || alias.includes('`')) {
|
|
24
|
+
throw new Error(`Invalid alias '${alias}': aliases cannot contain space or backtick characters`);
|
|
25
|
+
}
|
|
26
|
+
aliasMap.set(alias, target);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (options?.akaMethods) {
|
|
30
|
+
for (const [alias, target] of Object.entries(options.akaMethods)) {
|
|
31
|
+
if (alias.includes(' ') || alias.includes('`')) {
|
|
32
|
+
throw new Error(`Invalid alias '${alias}': aliases cannot contain space or backtick characters`);
|
|
33
|
+
}
|
|
34
|
+
aliasMap.set(alias, target);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const withMethods = options?.withMethods
|
|
38
|
+
? options.withMethods instanceof Set
|
|
39
|
+
? new Set(options.withMethods)
|
|
40
|
+
: new Set(options.withMethods)
|
|
41
|
+
: options?.akaMethods
|
|
42
|
+
? new Set()
|
|
43
|
+
: undefined;
|
|
44
|
+
if (options?.akaMethods) {
|
|
45
|
+
for (const target of Object.values(options.akaMethods)) {
|
|
46
|
+
withMethods?.add(target);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { aliasMap, withMethods };
|
|
50
|
+
}
|
|
19
51
|
/**
|
|
20
52
|
* Apply alias substitutions to a path string.
|
|
21
53
|
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
@@ -27,6 +59,17 @@ function applyAliases(path, aliasMap) {
|
|
|
27
59
|
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
28
60
|
return substituted.join('?.');
|
|
29
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a special root-reference token at the start of a string.
|
|
64
|
+
* '$0' refers to the first argument passed to assignFrom / resolveValues.
|
|
65
|
+
*/
|
|
66
|
+
function resolveRootReference(path, source, root) {
|
|
67
|
+
if (path === '$0')
|
|
68
|
+
return { source: root ?? source, path: '' };
|
|
69
|
+
if (path.startsWith('$0?.'))
|
|
70
|
+
return { source: root ?? source, path: path.substring(4) };
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
30
73
|
/**
|
|
31
74
|
* Path cache for parsed path strings.
|
|
32
75
|
* Avoids re-splitting the same path on repeated calls.
|
|
@@ -116,6 +159,18 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
116
159
|
const parts = parseCachedPath(aliased);
|
|
117
160
|
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
118
161
|
}
|
|
162
|
+
else if (typeof item === 'string' && item.startsWith('$0')) {
|
|
163
|
+
const rootRef = resolveRootReference(item, source, options?.root);
|
|
164
|
+
if (rootRef === null) {
|
|
165
|
+
result.push(source);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const aliased = applyAliases(rootRef.path, aliasMap);
|
|
169
|
+
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
170
|
+
const parts = parseCachedPath(normalizedPath);
|
|
171
|
+
result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
119
174
|
else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
120
175
|
result.push(getProtocolValue(item, protocols, options));
|
|
121
176
|
}
|
|
@@ -150,19 +205,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
150
205
|
* @returns New object with path strings replaced by resolved values
|
|
151
206
|
*/
|
|
152
207
|
export function getValues(pattern, source, options) {
|
|
153
|
-
|
|
154
|
-
const aliasMap = new Map();
|
|
155
|
-
if (options?.aka) {
|
|
156
|
-
for (const [alias, target] of Object.entries(options.aka)) {
|
|
157
|
-
aliasMap.set(alias, target);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// Build methods set
|
|
161
|
-
const withMethods = options?.withMethods
|
|
162
|
-
? options.withMethods instanceof Set
|
|
163
|
-
? options.withMethods
|
|
164
|
-
: new Set(options.withMethods)
|
|
165
|
-
: undefined;
|
|
208
|
+
const { aliasMap, withMethods } = normalizeAliasOptions(options);
|
|
166
209
|
const protocols = options?.protocols;
|
|
167
210
|
const result = {};
|
|
168
211
|
for (const [key, value] of Object.entries(pattern)) {
|
|
@@ -171,6 +214,18 @@ export function getValues(pattern, source, options) {
|
|
|
171
214
|
const parts = parseCachedPath(aliased);
|
|
172
215
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
173
216
|
}
|
|
217
|
+
else if (typeof value === 'string' && value.startsWith('$0')) {
|
|
218
|
+
const rootRef = resolveRootReference(value, source, options?.root);
|
|
219
|
+
if (rootRef === null) {
|
|
220
|
+
result[key] = source;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const aliased = applyAliases(rootRef.path, aliasMap);
|
|
224
|
+
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
225
|
+
const parts = parseCachedPath(normalizedPath);
|
|
226
|
+
result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
174
229
|
else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
175
230
|
result[key] = getProtocolValue(value, protocols, options);
|
|
176
231
|
}
|
|
@@ -201,23 +256,23 @@ export function getValues(pattern, source, options) {
|
|
|
201
256
|
* @returns The resolved value, or undefined if any segment is nullish
|
|
202
257
|
*/
|
|
203
258
|
export function getValue(path, source, options) {
|
|
204
|
-
|
|
259
|
+
const rootRef = resolveRootReference(path, source, options?.root);
|
|
260
|
+
if (rootRef) {
|
|
261
|
+
path = rootRef.path;
|
|
262
|
+
source = rootRef.source;
|
|
263
|
+
}
|
|
264
|
+
else if (!path.startsWith('?.')) {
|
|
205
265
|
return path;
|
|
266
|
+
}
|
|
206
267
|
let aliased = path;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
for (const [alias, target] of Object.entries(options.aka)) {
|
|
210
|
-
aliasMap.set(alias, target);
|
|
211
|
-
}
|
|
268
|
+
const { aliasMap } = normalizeAliasOptions(options);
|
|
269
|
+
if (aliasMap.size > 0) {
|
|
212
270
|
aliased = applyAliases(path, aliasMap);
|
|
213
271
|
}
|
|
214
|
-
const
|
|
272
|
+
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
273
|
+
const parts = parseCachedPath(normalizedPath);
|
|
215
274
|
if (parts.length === 0)
|
|
216
275
|
return source;
|
|
217
|
-
const withMethods = options
|
|
218
|
-
? options.withMethods instanceof Set
|
|
219
|
-
? options.withMethods
|
|
220
|
-
: new Set(options.withMethods)
|
|
221
|
-
: undefined;
|
|
276
|
+
const { withMethods } = normalizeAliasOptions(options);
|
|
222
277
|
return navigatePath(source, parts, withMethods);
|
|
223
278
|
}
|
package/getValues.ts
CHANGED
|
@@ -19,6 +19,48 @@
|
|
|
19
19
|
|
|
20
20
|
import type { GetValuesOptions } from './types/assign-gingerly/types.js';
|
|
21
21
|
|
|
22
|
+
export function normalizeAliasOptions(options?: {
|
|
23
|
+
aka?: Record<string, string>;
|
|
24
|
+
akaMethods?: Record<string, string>;
|
|
25
|
+
withMethods?: string[] | Set<string>;
|
|
26
|
+
}): { aliasMap: Map<string, string>; withMethods: Set<string> | undefined } {
|
|
27
|
+
const aliasMap = new Map<string, string>();
|
|
28
|
+
|
|
29
|
+
if (options?.aka) {
|
|
30
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
31
|
+
if (alias.includes(' ') || alias.includes('`')) {
|
|
32
|
+
throw new Error(`Invalid alias '${alias}': aliases cannot contain space or backtick characters`);
|
|
33
|
+
}
|
|
34
|
+
aliasMap.set(alias, target);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options?.akaMethods) {
|
|
39
|
+
for (const [alias, target] of Object.entries(options.akaMethods)) {
|
|
40
|
+
if (alias.includes(' ') || alias.includes('`')) {
|
|
41
|
+
throw new Error(`Invalid alias '${alias}': aliases cannot contain space or backtick characters`);
|
|
42
|
+
}
|
|
43
|
+
aliasMap.set(alias, target);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const withMethods = options?.withMethods
|
|
48
|
+
? options.withMethods instanceof Set
|
|
49
|
+
? new Set(options.withMethods)
|
|
50
|
+
: new Set(options.withMethods)
|
|
51
|
+
: options?.akaMethods
|
|
52
|
+
? new Set<string>()
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
if (options?.akaMethods) {
|
|
56
|
+
for (const target of Object.values(options.akaMethods)) {
|
|
57
|
+
withMethods?.add(target);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { aliasMap, withMethods };
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
/**
|
|
23
65
|
* Apply alias substitutions to a path string.
|
|
24
66
|
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
@@ -30,6 +72,16 @@ function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
|
30
72
|
return substituted.join('?.');
|
|
31
73
|
}
|
|
32
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a special root-reference token at the start of a string.
|
|
77
|
+
* '$0' refers to the first argument passed to assignFrom / resolveValues.
|
|
78
|
+
*/
|
|
79
|
+
function resolveRootReference(path: string, source: any, root: any): { source: any; path: string } | null {
|
|
80
|
+
if (path === '$0') return { source: root ?? source, path: '' };
|
|
81
|
+
if (path.startsWith('$0?.')) return { source: root ?? source, path: path.substring(4) };
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
33
85
|
/**
|
|
34
86
|
* Path cache for parsed path strings.
|
|
35
87
|
* Avoids re-splitting the same path on repeated calls.
|
|
@@ -142,6 +194,16 @@ function getArray(
|
|
|
142
194
|
const aliased = applyAliases(item, aliasMap);
|
|
143
195
|
const parts = parseCachedPath(aliased);
|
|
144
196
|
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
197
|
+
} else if (typeof item === 'string' && item.startsWith('$0')) {
|
|
198
|
+
const rootRef = resolveRootReference(item, source, options?.root);
|
|
199
|
+
if (rootRef === null) {
|
|
200
|
+
result.push(source);
|
|
201
|
+
} else {
|
|
202
|
+
const aliased = applyAliases(rootRef.path, aliasMap);
|
|
203
|
+
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
204
|
+
const parts = parseCachedPath(normalizedPath);
|
|
205
|
+
result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods));
|
|
206
|
+
}
|
|
145
207
|
} else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
146
208
|
result.push(getProtocolValue(item, protocols, options));
|
|
147
209
|
} else if (Array.isArray(item)) {
|
|
@@ -177,20 +239,7 @@ export function getValues(
|
|
|
177
239
|
source: any,
|
|
178
240
|
options?: GetValuesOptions
|
|
179
241
|
): Record<string, any> {
|
|
180
|
-
|
|
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;
|
|
242
|
+
const { aliasMap, withMethods } = normalizeAliasOptions(options);
|
|
194
243
|
|
|
195
244
|
const protocols = options?.protocols;
|
|
196
245
|
|
|
@@ -200,6 +249,16 @@ export function getValues(
|
|
|
200
249
|
const aliased = applyAliases(value, aliasMap);
|
|
201
250
|
const parts = parseCachedPath(aliased);
|
|
202
251
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
252
|
+
} else if (typeof value === 'string' && value.startsWith('$0')) {
|
|
253
|
+
const rootRef = resolveRootReference(value, source, options?.root);
|
|
254
|
+
if (rootRef === null) {
|
|
255
|
+
result[key] = source;
|
|
256
|
+
} else {
|
|
257
|
+
const aliased = applyAliases(rootRef.path, aliasMap);
|
|
258
|
+
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
259
|
+
const parts = parseCachedPath(normalizedPath);
|
|
260
|
+
result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods);
|
|
261
|
+
}
|
|
203
262
|
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
204
263
|
result[key] = getProtocolValue(value, protocols, options);
|
|
205
264
|
} else if (Array.isArray(value)) {
|
|
@@ -231,25 +290,25 @@ export function getValue(
|
|
|
231
290
|
source: any,
|
|
232
291
|
options?: GetValuesOptions
|
|
233
292
|
): any {
|
|
234
|
-
|
|
293
|
+
const rootRef = resolveRootReference(path, source, options?.root);
|
|
294
|
+
if (rootRef) {
|
|
295
|
+
path = rootRef.path;
|
|
296
|
+
source = rootRef.source;
|
|
297
|
+
} else if (!path.startsWith('?.')) {
|
|
298
|
+
return path;
|
|
299
|
+
}
|
|
235
300
|
|
|
236
301
|
let aliased = path;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
for (const [alias, target] of Object.entries(options.aka)) {
|
|
240
|
-
aliasMap.set(alias, target);
|
|
241
|
-
}
|
|
302
|
+
const { aliasMap } = normalizeAliasOptions(options);
|
|
303
|
+
if (aliasMap.size > 0) {
|
|
242
304
|
aliased = applyAliases(path, aliasMap);
|
|
243
305
|
}
|
|
244
306
|
|
|
245
|
-
const
|
|
307
|
+
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
308
|
+
const parts = parseCachedPath(normalizedPath);
|
|
246
309
|
if (parts.length === 0) return source;
|
|
247
310
|
|
|
248
|
-
const withMethods = options
|
|
249
|
-
? options.withMethods instanceof Set
|
|
250
|
-
? options.withMethods
|
|
251
|
-
: new Set(options.withMethods)
|
|
252
|
-
: undefined;
|
|
311
|
+
const { withMethods } = normalizeAliasOptions(options);
|
|
253
312
|
|
|
254
313
|
return navigatePath(source, parts, withMethods);
|
|
255
314
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handlers/addEventListener.ts — Event binding handler for += operator.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported when assignGingerly/assignFrom detects an += with
|
|
5
|
+
* an object RHS containing an `on` property on a DOM Element LHS.
|
|
6
|
+
*
|
|
7
|
+
* Attaches an event listener that executes assign vectors on event fire.
|
|
8
|
+
*/
|
|
9
|
+
import assignGingerly from '../assignGingerly.js';
|
|
10
|
+
/**
|
|
11
|
+
* WeakMap for dedup: Element → Map<key, AbortController>
|
|
12
|
+
*/
|
|
13
|
+
const keyMap = new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Reserved keys that are not shorthand assign patterns.
|
|
16
|
+
*/
|
|
17
|
+
const RESERVED_KEYS = new Set([
|
|
18
|
+
'on', 'get', 'fromLHS', 'fromHost', 'fromEvent', 'fromTarget',
|
|
19
|
+
'toTarget', 'toHost', 'toLHS', 'withOptions', 'toTargetOptions',
|
|
20
|
+
'toHostOptions', 'toLHSOptions', 'dispatch', 'nudge'
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Extract shorthand keys (implicit toHost) from a vector config.
|
|
24
|
+
* Any key not in RESERVED_KEYS that starts with '?.' or contains an operator is shorthand.
|
|
25
|
+
*/
|
|
26
|
+
function extractShorthand(config) {
|
|
27
|
+
const shorthand = {};
|
|
28
|
+
let hasAny = false;
|
|
29
|
+
for (const key of Object.keys(config)) {
|
|
30
|
+
if (!RESERVED_KEYS.has(key)) {
|
|
31
|
+
shorthand[key] = config[key];
|
|
32
|
+
hasAny = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return hasAny ? shorthand : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Process a single AssignDispatchVector — execute all toTarget/toHost/toLHS assignments.
|
|
39
|
+
*/
|
|
40
|
+
function processVector(vector, source, target, host, lhs, inheritedOptions, useAssignFrom) {
|
|
41
|
+
const destinations = [
|
|
42
|
+
{ key: 'toTarget', dest: target },
|
|
43
|
+
{ key: 'toHost', dest: host },
|
|
44
|
+
{ key: 'toLHS', dest: lhs },
|
|
45
|
+
];
|
|
46
|
+
for (const { key, dest } of destinations) {
|
|
47
|
+
const pattern = vector[key];
|
|
48
|
+
if (!pattern || Object.keys(pattern).length === 0)
|
|
49
|
+
continue;
|
|
50
|
+
if (useAssignFrom && source != null) {
|
|
51
|
+
// Dynamic import assignFrom on demand (fire-and-forget context, already async)
|
|
52
|
+
import('../assignFrom.js').then(({ assignFrom }) => {
|
|
53
|
+
assignFrom(dest, pattern, { from: source, ...inheritedOptions, ...vector.withOptions });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
assignGingerly(dest, pattern, inheritedOptions);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Handle shorthand (implicit toHost)
|
|
61
|
+
const shorthand = extractShorthand(vector);
|
|
62
|
+
if (shorthand) {
|
|
63
|
+
if (useAssignFrom && source != null) {
|
|
64
|
+
import('../assignFrom.js').then(({ assignFrom }) => {
|
|
65
|
+
assignFrom(host, shorthand, { from: source, ...inheritedOptions, ...vector.withOptions });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
assignGingerly(host, shorthand, inheritedOptions);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Attach an event listener based on AddEventListenerConfig.
|
|
75
|
+
*
|
|
76
|
+
* @param lhs - The DOM element to attach the listener to
|
|
77
|
+
* @param config - The event handler configuration
|
|
78
|
+
* @param target - The assignFrom target (first arg)
|
|
79
|
+
* @param host - The options.from (source/view model)
|
|
80
|
+
* @param inheritedOptions - Parent assignFrom options (withMethods, aka, etc.)
|
|
81
|
+
*/
|
|
82
|
+
export function attachEventListener(lhs, config, target, host, inheritedOptions) {
|
|
83
|
+
const { on: eventName, get: getConfig, fromLHS, fromHost, fromTarget, fromEvent, dispatch } = config;
|
|
84
|
+
// Resolve get config values
|
|
85
|
+
const { abortController, key, nudge, options: listenerOptions, stopPropagation, preventDefault, dispatch: getDispatch } = getConfig ?? {};
|
|
86
|
+
// Handle dedup via key
|
|
87
|
+
let controller;
|
|
88
|
+
if (key) {
|
|
89
|
+
let elMap = keyMap.get(lhs);
|
|
90
|
+
if (!elMap) {
|
|
91
|
+
elMap = new Map();
|
|
92
|
+
keyMap.set(lhs, elMap);
|
|
93
|
+
}
|
|
94
|
+
// Abort previous listener with same key
|
|
95
|
+
const prev = elMap.get(key);
|
|
96
|
+
if (prev)
|
|
97
|
+
prev.abort();
|
|
98
|
+
// Create new controller for this key
|
|
99
|
+
controller = new AbortController();
|
|
100
|
+
elMap.set(key, controller);
|
|
101
|
+
}
|
|
102
|
+
else if (abortController instanceof AbortController) {
|
|
103
|
+
controller = abortController;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
controller = new AbortController();
|
|
107
|
+
}
|
|
108
|
+
// Attach the listener
|
|
109
|
+
lhs.addEventListener(eventName, (event) => {
|
|
110
|
+
if (stopPropagation)
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
if (preventDefault)
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
// Static assignments (no from) — top-level toTarget/toHost/toLHS + shorthand
|
|
115
|
+
processVector(config, null, target, host, lhs, inheritedOptions, false);
|
|
116
|
+
// fromLHS assignments
|
|
117
|
+
if (fromLHS) {
|
|
118
|
+
processVector(fromLHS, lhs, target, host, lhs, inheritedOptions, true);
|
|
119
|
+
}
|
|
120
|
+
// fromHost assignments
|
|
121
|
+
if (fromHost) {
|
|
122
|
+
processVector(fromHost, host, target, host, lhs, inheritedOptions, true);
|
|
123
|
+
}
|
|
124
|
+
// fromTarget assignments
|
|
125
|
+
if (fromTarget) {
|
|
126
|
+
processVector(fromTarget, target, target, host, lhs, inheritedOptions, true);
|
|
127
|
+
}
|
|
128
|
+
// fromEvent assignments
|
|
129
|
+
if (fromEvent) {
|
|
130
|
+
processVector(fromEvent, event, target, host, lhs, inheritedOptions, true);
|
|
131
|
+
}
|
|
132
|
+
// Dispatch custom event if configured
|
|
133
|
+
const dispatchConfig = dispatch || getDispatch;
|
|
134
|
+
if (dispatchConfig && dispatchConfig.type) {
|
|
135
|
+
const EventCtr = (typeof dispatchConfig.eventCtr === 'function'
|
|
136
|
+
? dispatchConfig.eventCtr
|
|
137
|
+
: CustomEvent);
|
|
138
|
+
const evt = new EventCtr(dispatchConfig.type, {
|
|
139
|
+
detail: dispatchConfig.detail,
|
|
140
|
+
bubbles: dispatchConfig.bubbles ?? true,
|
|
141
|
+
cancelable: dispatchConfig.cancelable ?? false,
|
|
142
|
+
composed: dispatchConfig.composed ?? true,
|
|
143
|
+
});
|
|
144
|
+
lhs.dispatchEvent(evt);
|
|
145
|
+
}
|
|
146
|
+
}, { signal: controller.signal, ...listenerOptions });
|
|
147
|
+
// Nudge: remove disabled, add interaction hints
|
|
148
|
+
if (nudge) {
|
|
149
|
+
import('./nudge.js').then(({ nudge: nudgeFn }) => {
|
|
150
|
+
nudgeFn(lhs);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handlers/addEventListener.ts — Event binding handler for += operator.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported when assignGingerly/assignFrom detects an += with
|
|
5
|
+
* an object RHS containing an `on` property on a DOM Element LHS.
|
|
6
|
+
*
|
|
7
|
+
* Attaches an event listener that executes assign vectors on event fire.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assignGingerly from '../assignGingerly.js';
|
|
11
|
+
import type { AddEventListenerConfig, AssignDispatchVector } from '../types/assign-gingerly/types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* WeakMap for dedup: Element → Map<key, AbortController>
|
|
15
|
+
*/
|
|
16
|
+
const keyMap = new WeakMap<Element, Map<string, AbortController>>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reserved keys that are not shorthand assign patterns.
|
|
20
|
+
*/
|
|
21
|
+
const RESERVED_KEYS = new Set([
|
|
22
|
+
'on', 'get', 'fromLHS', 'fromHost', 'fromEvent', 'fromTarget',
|
|
23
|
+
'toTarget', 'toHost', 'toLHS', 'withOptions', 'toTargetOptions',
|
|
24
|
+
'toHostOptions', 'toLHSOptions', 'dispatch', 'nudge'
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract shorthand keys (implicit toHost) from a vector config.
|
|
29
|
+
* Any key not in RESERVED_KEYS that starts with '?.' or contains an operator is shorthand.
|
|
30
|
+
*/
|
|
31
|
+
function extractShorthand(config: Record<string, any>): Record<string, any> | null {
|
|
32
|
+
const shorthand: Record<string, any> = {};
|
|
33
|
+
let hasAny = false;
|
|
34
|
+
for (const key of Object.keys(config)) {
|
|
35
|
+
if (!RESERVED_KEYS.has(key)) {
|
|
36
|
+
shorthand[key] = config[key];
|
|
37
|
+
hasAny = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return hasAny ? shorthand : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Process a single AssignDispatchVector — execute all toTarget/toHost/toLHS assignments.
|
|
45
|
+
*/
|
|
46
|
+
function processVector(
|
|
47
|
+
vector: AssignDispatchVector,
|
|
48
|
+
source: any,
|
|
49
|
+
target: any,
|
|
50
|
+
host: any,
|
|
51
|
+
lhs: Element,
|
|
52
|
+
inheritedOptions: any,
|
|
53
|
+
useAssignFrom: boolean
|
|
54
|
+
): void {
|
|
55
|
+
const destinations = [
|
|
56
|
+
{ key: 'toTarget' as const, dest: target },
|
|
57
|
+
{ key: 'toHost' as const, dest: host },
|
|
58
|
+
{ key: 'toLHS' as const, dest: lhs },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const { key, dest } of destinations) {
|
|
62
|
+
const pattern = vector[key];
|
|
63
|
+
if (!pattern || Object.keys(pattern).length === 0) continue;
|
|
64
|
+
if (useAssignFrom && source != null) {
|
|
65
|
+
// Dynamic import assignFrom on demand (fire-and-forget context, already async)
|
|
66
|
+
import('../assignFrom.js').then(({ assignFrom }) => {
|
|
67
|
+
assignFrom(dest, pattern, { from: source, ...inheritedOptions, ...vector.withOptions });
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
assignGingerly(dest, pattern, inheritedOptions);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle shorthand (implicit toHost)
|
|
75
|
+
const shorthand = extractShorthand(vector as Record<string, any>);
|
|
76
|
+
if (shorthand) {
|
|
77
|
+
if (useAssignFrom && source != null) {
|
|
78
|
+
import('../assignFrom.js').then(({ assignFrom }) => {
|
|
79
|
+
assignFrom(host, shorthand, { from: source, ...inheritedOptions, ...vector.withOptions });
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
assignGingerly(host, shorthand, inheritedOptions);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Attach an event listener based on AddEventListenerConfig.
|
|
89
|
+
*
|
|
90
|
+
* @param lhs - The DOM element to attach the listener to
|
|
91
|
+
* @param config - The event handler configuration
|
|
92
|
+
* @param target - The assignFrom target (first arg)
|
|
93
|
+
* @param host - The options.from (source/view model)
|
|
94
|
+
* @param inheritedOptions - Parent assignFrom options (withMethods, aka, etc.)
|
|
95
|
+
*/
|
|
96
|
+
export function attachEventListener(
|
|
97
|
+
lhs: Element,
|
|
98
|
+
config: AddEventListenerConfig,
|
|
99
|
+
target: any,
|
|
100
|
+
host: any,
|
|
101
|
+
inheritedOptions: any
|
|
102
|
+
): void {
|
|
103
|
+
const { on: eventName, get: getConfig, fromLHS, fromHost, fromTarget, fromEvent, dispatch } = config;
|
|
104
|
+
|
|
105
|
+
// Resolve get config values
|
|
106
|
+
const { abortController, key, nudge, options: listenerOptions, stopPropagation, preventDefault, dispatch: getDispatch } = getConfig ?? {} as any;
|
|
107
|
+
|
|
108
|
+
// Handle dedup via key
|
|
109
|
+
let controller: AbortController;
|
|
110
|
+
if (key) {
|
|
111
|
+
let elMap = keyMap.get(lhs);
|
|
112
|
+
if (!elMap) {
|
|
113
|
+
elMap = new Map();
|
|
114
|
+
keyMap.set(lhs, elMap);
|
|
115
|
+
}
|
|
116
|
+
// Abort previous listener with same key
|
|
117
|
+
const prev = elMap.get(key);
|
|
118
|
+
if (prev) prev.abort();
|
|
119
|
+
// Create new controller for this key
|
|
120
|
+
controller = new AbortController();
|
|
121
|
+
elMap.set(key, controller);
|
|
122
|
+
} else if (abortController instanceof AbortController) {
|
|
123
|
+
controller = abortController;
|
|
124
|
+
} else {
|
|
125
|
+
controller = new AbortController();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Attach the listener
|
|
129
|
+
lhs.addEventListener(eventName, (event: Event) => {
|
|
130
|
+
if (stopPropagation) event.stopPropagation();
|
|
131
|
+
if (preventDefault) event.preventDefault();
|
|
132
|
+
|
|
133
|
+
// Static assignments (no from) — top-level toTarget/toHost/toLHS + shorthand
|
|
134
|
+
processVector(config, null, target, host, lhs, inheritedOptions, false);
|
|
135
|
+
|
|
136
|
+
// fromLHS assignments
|
|
137
|
+
if (fromLHS) {
|
|
138
|
+
processVector(fromLHS, lhs, target, host, lhs, inheritedOptions, true);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// fromHost assignments
|
|
142
|
+
if (fromHost) {
|
|
143
|
+
processVector(fromHost, host, target, host, lhs, inheritedOptions, true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// fromTarget assignments
|
|
147
|
+
if (fromTarget) {
|
|
148
|
+
processVector(fromTarget, target, target, host, lhs, inheritedOptions, true);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// fromEvent assignments
|
|
152
|
+
if (fromEvent) {
|
|
153
|
+
processVector(fromEvent, event, target, host, lhs, inheritedOptions, true);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Dispatch custom event if configured
|
|
157
|
+
const dispatchConfig = dispatch || getDispatch;
|
|
158
|
+
if (dispatchConfig && dispatchConfig.type) {
|
|
159
|
+
const EventCtr = (typeof dispatchConfig.eventCtr === 'function'
|
|
160
|
+
? dispatchConfig.eventCtr
|
|
161
|
+
: CustomEvent) as typeof CustomEvent;
|
|
162
|
+
const evt = new EventCtr(dispatchConfig.type, {
|
|
163
|
+
detail: dispatchConfig.detail,
|
|
164
|
+
bubbles: dispatchConfig.bubbles ?? true,
|
|
165
|
+
cancelable: dispatchConfig.cancelable ?? false,
|
|
166
|
+
composed: dispatchConfig.composed ?? true,
|
|
167
|
+
});
|
|
168
|
+
lhs.dispatchEvent(evt);
|
|
169
|
+
}
|
|
170
|
+
}, { signal: controller.signal, ...listenerOptions });
|
|
171
|
+
|
|
172
|
+
// Nudge: remove disabled, add interaction hints
|
|
173
|
+
if (nudge) {
|
|
174
|
+
import('./nudge.js').then(({ nudge: nudgeFn }) => {
|
|
175
|
+
nudgeFn(lhs);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
package/handlers/arr.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to normalize a value to an array.
|
|
3
|
+
* - If undefined, returns empty array
|
|
4
|
+
* - If already an array, returns as-is
|
|
5
|
+
* - Otherwise, wraps the value in an array
|
|
6
|
+
*
|
|
7
|
+
* @param inp - Value to normalize to array
|
|
8
|
+
* @returns Array containing the value(s)
|
|
9
|
+
*/
|
|
10
|
+
export function arr(inp) {
|
|
11
|
+
return inp === undefined ? []
|
|
12
|
+
: Array.isArray(inp) ? inp : [inp];
|
|
13
|
+
}
|
package/handlers/arr.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to normalize a value to an array.
|
|
3
|
+
* - If undefined, returns empty array
|
|
4
|
+
* - If already an array, returns as-is
|
|
5
|
+
* - Otherwise, wraps the value in an array
|
|
6
|
+
*
|
|
7
|
+
* @param inp - Value to normalize to array
|
|
8
|
+
* @returns Array containing the value(s)
|
|
9
|
+
*/
|
|
10
|
+
export function arr<T = any>(inp: T | T[] | undefined): T[] {
|
|
11
|
+
return inp === undefined ? []
|
|
12
|
+
: Array.isArray(inp) ? inp : [inp];
|
|
13
|
+
}
|
package/handlers/lazyLoad.js
CHANGED
|
@@ -217,10 +217,10 @@ export class LazyLoadHandler {
|
|
|
217
217
|
const len = Math.min(elements.length, assign.configs.length);
|
|
218
218
|
for (let j = 0; j < len; j++) {
|
|
219
219
|
const cfg = assign.configs[j];
|
|
220
|
-
assignFrom(elements[j], cfg.
|
|
220
|
+
assignFrom(elements[j], cfg.toClone ?? {}, { from, ...cfg.withOptions });
|
|
221
221
|
}
|
|
222
|
-
} else if (assign.
|
|
223
|
-
assignFrom(elements[0], assign.
|
|
222
|
+
} else if (assign.toClone) {
|
|
223
|
+
assignFrom(elements[0], assign.toClone, { from, ...assign.withOptions });
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
async onCloneInserted(nodes, lhsTarget, resolvedParams) {
|