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.
Files changed (42) hide show
  1. package/README.md +85 -26
  2. package/assignFrom.js +21 -19
  3. package/assignFrom.ts +28 -25
  4. package/assignFromAsync.js +10 -7
  5. package/assignFromAsync.ts +15 -98
  6. package/assignGingerly.js +54 -26
  7. package/assignGingerly.ts +62 -27
  8. package/assignTentatively.js +8 -0
  9. package/assignTentatively.ts +6 -0
  10. package/{builtInEmoji.ts → emojis.js} +46 -34
  11. package/emojis.ts +52 -0
  12. package/getValues.js +80 -25
  13. package/getValues.ts +85 -26
  14. package/handlers/addEventListener.js +153 -0
  15. package/handlers/addEventListener.ts +178 -0
  16. package/handlers/arr.js +13 -0
  17. package/handlers/arr.ts +13 -0
  18. package/handlers/lazyLoad.js +3 -3
  19. package/handlers/lazyLoad.ts +3 -3
  20. package/handlers/manageTemplateList.js +48 -21
  21. package/handlers/manageTemplateList.ts +51 -21
  22. package/handlers/nudge.js +23 -0
  23. package/handlers/nudge.ts +23 -0
  24. package/index.js +2 -0
  25. package/index.ts +2 -0
  26. package/inferencer/types/assign-gingerly/types.d.ts +327 -1
  27. package/inferencer/types/mount-observer/types.d.ts +10 -2
  28. package/inferencer/types/roundabout/types.d.ts +1 -1
  29. package/inferencer/types/three-peat/types.d.ts +18 -0
  30. package/object-extension.js +1 -1
  31. package/package.json +12 -4
  32. package/paths.ts +1 -1
  33. package/{withIdsCorrector.js → pinCorrector.js} +5 -5
  34. package/{withIdsCorrector.ts → pinCorrector.ts} +5 -5
  35. package/processHandlerCommands.js +4 -2
  36. package/processHandlerCommands.ts +4 -2
  37. package/resolveIdRef.js +8 -8
  38. package/resolveIdRef.ts +9 -9
  39. package/resolveValues.js +20 -88
  40. package/resolveValues.ts +17 -91
  41. package/types/assign-gingerly/types.d.ts +159 -3
  42. 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
- // Build alias map
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
- if (!path.startsWith('?.'))
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
- if (options?.aka) {
208
- const aliasMap = new Map();
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 parts = parseCachedPath(aliased);
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?.withMethods
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
- // 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;
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
- if (!path.startsWith('?.')) return path;
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
- 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
- }
302
+ const { aliasMap } = normalizeAliasOptions(options);
303
+ if (aliasMap.size > 0) {
242
304
  aliased = applyAliases(path, aliasMap);
243
305
  }
244
306
 
245
- const parts = parseCachedPath(aliased);
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?.withMethods
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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.assignToFragment ?? {}, { from, ...cfg.withOptions });
220
+ assignFrom(elements[j], cfg.toClone ?? {}, { from, ...cfg.withOptions });
221
221
  }
222
- } else if (assign.assignToFragment) {
223
- assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
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) {