assign-gingerly 0.0.65 → 0.0.67

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/assignFrom.js CHANGED
@@ -307,7 +307,7 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
307
307
  const ids = getEffectiveIds(options);
308
308
  if (!ids)
309
309
  return;
310
- //TODO
310
+ const { withMethods, aka, akaMethods, protocols, from } = options;
311
311
  for (const key of idRefNormalKeys) {
312
312
  const parsed = parseIdRef(key);
313
313
  if (!parsed)
@@ -317,11 +317,11 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
317
317
  continue;
318
318
  const value = expandedPattern[key];
319
319
  if (parsed.remainingPath) {
320
- const resolvedValue = getValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: target });
320
+ const resolvedValue = getValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target });
321
321
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
322
322
  }
323
323
  else {
324
- const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: target });
324
+ const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target });
325
325
  if (!('__v' in resolvedValue)) {
326
326
  assignGingerly(el, resolvedValue, options);
327
327
  }
package/assignFrom.ts CHANGED
@@ -321,7 +321,8 @@ function processIdRefNormalKeys(
321
321
  ): void {
322
322
  const ids = getEffectiveIds(options);
323
323
  if (!ids) return;
324
- //TODO
324
+
325
+ const { withMethods, aka, akaMethods, protocols, from } = options;
325
326
  for (const key of idRefNormalKeys) {
326
327
  const parsed = parseIdRef(key);
327
328
  if (!parsed) continue;
@@ -332,15 +333,15 @@ function processIdRefNormalKeys(
332
333
  const value = expandedPattern[key];
333
334
  if (parsed.remainingPath) {
334
335
  const resolvedValue = getValues(
335
- { __v: value }, options.from,
336
- { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: target }
336
+ { __v: value }, from,
337
+ { withMethods, aka, akaMethods, protocols, root: target }
337
338
  );
338
339
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
339
340
  } else {
340
341
  const resolvedValue = getValues(
341
342
  typeof value === 'object' && value !== null ? value : { __v: value },
342
- options.from,
343
- { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: target }
343
+ from,
344
+ { withMethods, aka, akaMethods, protocols, root: target }
344
345
  );
345
346
  if (!('__v' in resolvedValue)) {
346
347
  assignGingerly(el, resolvedValue, options);
@@ -46,7 +46,7 @@ export async function assignFromAsync(target, pattern, options, permissions) {
46
46
  if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
47
47
  const ids = { ...options.pin, ...options.at };
48
48
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
49
- //TODO
49
+ const { withMethods, aka, akaMethods, protocols, from } = options;
50
50
  for (const key of idRefNormalKeys) {
51
51
  const parsed = parseIdRef(key);
52
52
  if (!parsed)
@@ -57,13 +57,13 @@ export async function assignFromAsync(target, pattern, options, permissions) {
57
57
  const value = expandedPattern[key];
58
58
  if (parsed.remainingPath) {
59
59
  // Resolve the RHS value
60
- const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: el });
60
+ const resolvedValue = await resolveValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el });
61
61
  // Apply remaining path on the resolved element
62
62
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
63
63
  }
64
64
  else {
65
65
  // No remaining path — resolve and assign directly to the element
66
- const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: el });
66
+ const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el });
67
67
  if ('__v' in resolvedValue) {
68
68
  // Single value — can't assign to element root without a path
69
69
  }
@@ -83,7 +83,7 @@ export async function assignFromAsync(
83
83
  if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
84
84
  const ids = { ...options.pin, ...options.at };
85
85
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
86
- //TODO
86
+ const { withMethods, aka, akaMethods, protocols, from } = options;
87
87
  for (const key of idRefNormalKeys) {
88
88
  const parsed = parseIdRef(key);
89
89
  if (!parsed) continue;
@@ -95,8 +95,8 @@ export async function assignFromAsync(
95
95
  if (parsed.remainingPath) {
96
96
  // Resolve the RHS value
97
97
  const resolvedValue = await resolveValues(
98
- { __v: value }, options.from,
99
- { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: el }
98
+ { __v: value }, from,
99
+ { withMethods, aka, akaMethods, protocols, root: el }
100
100
  );
101
101
  // Apply remaining path on the resolved element
102
102
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
@@ -104,8 +104,8 @@ export async function assignFromAsync(
104
104
  // No remaining path — resolve and assign directly to the element
105
105
  const resolvedValue = await resolveValues(
106
106
  typeof value === 'object' && value !== null ? value : { __v: value },
107
- options.from,
108
- { withMethods: options.withMethods, aka: options.aka, akaMethods: options.akaMethods, protocols: options.protocols, root: el }
107
+ from,
108
+ { withMethods, aka, akaMethods, protocols, root: el }
109
109
  );
110
110
  if ('__v' in resolvedValue) {
111
111
  // Single value — can't assign to element root without a path
package/assignGingerly.js CHANGED
@@ -562,7 +562,6 @@ export function assignGingerly(target, source, options, permissions) {
562
562
  if (!target || typeof target !== 'object') {
563
563
  return target;
564
564
  }
565
- //TODO
566
565
  const { aliasMap, withMethods: withMethodsSet } = normalizeAliasOptions(options);
567
566
  // Convert withAsyncMethods array to Set for O(1) lookup
568
567
  const withAsyncMethodsSet = options?.withAsyncMethods
@@ -639,22 +638,61 @@ export function assignGingerly(target, source, options, permissions) {
639
638
  if (path) {
640
639
  if (isNestedPath(path)) {
641
640
  const pathParts = parsePath(path);
642
- const lastKey = pathParts[pathParts.length - 1];
643
- const parent = ensureNestedPath(target, pathParts);
644
- if (!(lastKey in parent)) {
645
- parent[lastKey] = value;
641
+ // Check for withMethods path evaluation
642
+ let lhsValue;
643
+ let lhsParent;
644
+ let lhsKey;
645
+ if (withMethodsSet) {
646
+ const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet);
647
+ lhsParent = result.target;
648
+ lhsKey = result.lastKey;
649
+ lhsValue = lhsParent[lhsKey];
650
+ }
651
+ else {
652
+ lhsKey = pathParts[pathParts.length - 1];
653
+ lhsParent = ensureNestedPath(target, pathParts);
654
+ lhsValue = lhsParent[lhsKey];
655
+ }
656
+ // Event handler: Element LHS + object RHS with 'on' property
657
+ if (lhsValue instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
658
+ const capturedLhs = lhsValue;
659
+ const capturedValue = value;
660
+ const capturedTarget = target;
661
+ const capturedOptions = options;
662
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
663
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
664
+ });
665
+ continue;
646
666
  }
647
- else if (Array.isArray(parent[lastKey])) {
648
- parent[lastKey] = Array.isArray(value)
649
- ? [...parent[lastKey], ...value]
650
- : [...parent[lastKey], value];
667
+ if (!(lhsKey in lhsParent)) {
668
+ lhsParent[lhsKey] = value;
669
+ }
670
+ else if (Array.isArray(lhsValue)) {
671
+ lhsParent[lhsKey] = Array.isArray(value)
672
+ ? [...lhsValue, ...value]
673
+ : [...lhsValue, value];
674
+ }
675
+ else if (typeof lhsValue === 'number' && typeof value === 'string') {
676
+ const parsed = Number(value);
677
+ lhsParent[lhsKey] = isNaN(parsed) ? lhsValue + value : lhsValue + parsed;
651
678
  }
652
679
  else {
653
- parent[lastKey] += value;
680
+ lhsParent[lhsKey] += value;
654
681
  }
655
682
  }
656
683
  else {
657
684
  // Plain key - direct operation on target
685
+ // Event handler: Element LHS + object RHS with 'on' property
686
+ if (target[path] instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
687
+ const capturedLhs = target[path];
688
+ const capturedValue = value;
689
+ const capturedTarget = target;
690
+ const capturedOptions = options;
691
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
692
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
693
+ });
694
+ continue;
695
+ }
658
696
  if (!(path in target)) {
659
697
  target[path] = value;
660
698
  }
@@ -663,6 +701,10 @@ export function assignGingerly(target, source, options, permissions) {
663
701
  ? [...target[path], ...value]
664
702
  : [...target[path], value];
665
703
  }
704
+ else if (typeof target[path] === 'number' && typeof value === 'string') {
705
+ const parsed = Number(value);
706
+ target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
707
+ }
666
708
  else {
667
709
  target[path] += value;
668
710
  }
package/assignGingerly.ts CHANGED
@@ -767,7 +767,6 @@ export function assignGingerly(
767
767
  if (!target || typeof target !== 'object') {
768
768
  return target;
769
769
  }
770
- //TODO
771
770
 
772
771
  const { aliasMap, withMethods: withMethodsSet } = normalizeAliasOptions(options);
773
772
 
@@ -852,25 +851,69 @@ export function assignGingerly(
852
851
  if (path) {
853
852
  if (isNestedPath(path)) {
854
853
  const pathParts = parsePath(path);
855
- const lastKey = pathParts[pathParts.length - 1];
856
- const parent = ensureNestedPath(target, pathParts);
857
- if (!(lastKey in parent)) {
858
- parent[lastKey] = value;
859
- } else if (Array.isArray(parent[lastKey])) {
860
- parent[lastKey] = Array.isArray(value)
861
- ? [...parent[lastKey], ...value]
862
- : [...parent[lastKey], value];
854
+
855
+ // Check for withMethods path evaluation
856
+ let lhsValue: any;
857
+ let lhsParent: any;
858
+ let lhsKey: string;
859
+ if (withMethodsSet) {
860
+ const result = evaluatePathWithMethods(target, pathParts, value, withMethodsSet);
861
+ lhsParent = result.target;
862
+ lhsKey = result.lastKey;
863
+ lhsValue = lhsParent[lhsKey];
864
+ } else {
865
+ lhsKey = pathParts[pathParts.length - 1];
866
+ lhsParent = ensureNestedPath(target, pathParts);
867
+ lhsValue = lhsParent[lhsKey];
868
+ }
869
+
870
+ // Event handler: Element LHS + object RHS with 'on' property
871
+ if (lhsValue instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
872
+ const capturedLhs = lhsValue;
873
+ const capturedValue = value;
874
+ const capturedTarget = target;
875
+ const capturedOptions = options;
876
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
877
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
878
+ });
879
+ continue;
880
+ }
881
+
882
+ if (!(lhsKey in lhsParent)) {
883
+ lhsParent[lhsKey] = value;
884
+ } else if (Array.isArray(lhsValue)) {
885
+ lhsParent[lhsKey] = Array.isArray(value)
886
+ ? [...lhsValue, ...value]
887
+ : [...lhsValue, value];
888
+ } else if (typeof lhsValue === 'number' && typeof value === 'string') {
889
+ const parsed = Number(value);
890
+ lhsParent[lhsKey] = isNaN(parsed) ? lhsValue + value : lhsValue + parsed;
863
891
  } else {
864
- parent[lastKey] += value;
892
+ lhsParent[lhsKey] += value;
865
893
  }
866
894
  } else {
867
895
  // Plain key - direct operation on target
896
+ // Event handler: Element LHS + object RHS with 'on' property
897
+ if (target[path] instanceof Element && value && typeof value === 'object' && !Array.isArray(value) && 'on' in value) {
898
+ const capturedLhs = target[path];
899
+ const capturedValue = value;
900
+ const capturedTarget = target;
901
+ const capturedOptions = options;
902
+ import('./handlers/addEventListener.js').then(({ attachEventListener }) => {
903
+ attachEventListener(capturedLhs, capturedValue, capturedTarget, capturedOptions?.from ?? capturedTarget, capturedOptions ?? {});
904
+ });
905
+ continue;
906
+ }
907
+
868
908
  if (!(path in target)) {
869
909
  target[path] = value;
870
910
  } else if (Array.isArray(target[path])) {
871
911
  target[path] = Array.isArray(value)
872
912
  ? [...target[path], ...value]
873
913
  : [...target[path], value];
914
+ } else if (typeof target[path] === 'number' && typeof value === 'string') {
915
+ const parsed = Number(value);
916
+ target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
874
917
  } else {
875
918
  target[path] += value;
876
919
  }
@@ -129,6 +129,10 @@ export function assignTentatively(target, source, options) {
129
129
  ? [...parent[lastKey], ...value]
130
130
  : [...parent[lastKey], value];
131
131
  }
132
+ else if (typeof parent[lastKey] === 'number' && typeof value === 'string') {
133
+ const parsed = Number(value);
134
+ parent[lastKey] = isNaN(parsed) ? parent[lastKey] + value : parent[lastKey] + parsed;
135
+ }
132
136
  else {
133
137
  parent[lastKey] += value;
134
138
  }
@@ -149,6 +153,10 @@ export function assignTentatively(target, source, options) {
149
153
  ? [...target[path], ...value]
150
154
  : [...target[path], value];
151
155
  }
156
+ else if (typeof target[path] === 'number' && typeof value === 'string') {
157
+ const parsed = Number(value);
158
+ target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
159
+ }
152
160
  else {
153
161
  target[path] += value;
154
162
  }
@@ -155,6 +155,9 @@ export function assignTentatively(
155
155
  parent[lastKey] = Array.isArray(value)
156
156
  ? [...parent[lastKey], ...value]
157
157
  : [...parent[lastKey], value];
158
+ } else if (typeof parent[lastKey] === 'number' && typeof value === 'string') {
159
+ const parsed = Number(value);
160
+ parent[lastKey] = isNaN(parsed) ? parent[lastKey] + value : parent[lastKey] + parsed;
158
161
  } else {
159
162
  parent[lastKey] += value;
160
163
  }
@@ -172,6 +175,9 @@ export function assignTentatively(
172
175
  target[path] = Array.isArray(value)
173
176
  ? [...target[path], ...value]
174
177
  : [...target[path], value];
178
+ } else if (typeof target[path] === 'number' && typeof value === 'string') {
179
+ const parsed = Number(value);
180
+ target[path] = isNaN(parsed) ? target[path] + value : target[path] + parsed;
175
181
  } else {
176
182
  target[path] += value;
177
183
  }
@@ -1,34 +1,46 @@
1
- /**
2
- * builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
3
- *
4
- * Import and spread into the `handlers` option for concise handler configs:
5
- *
6
- * @example
7
- * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
- *
9
- * assignFrom(target, {
10
- * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
- * }, { from: vm, handlers: builtInEmoji });
12
- */
13
-
14
- /**
15
- * Emoji → built-in handler name mapping.
16
- *
17
- * | Emoji | Handler |
18
- * |-------|---------|
19
- * | 📦 | builtIns.lazyLoad |
20
- * | 🎚️ | builtIns.lazyLoadSwitch |
21
- * | 🔗 | builtIns.join |
22
- * | 🏷️ | builtIns.microDataJoin |
23
- * | 📋 | builtIns.manageTemplateList |
24
- */
25
- export const builtInEmoji: Record<string, string> = {
26
- '📦': 'builtIns.lazyLoad',
27
- '🎚️': 'builtIns.lazyLoadSwitch',
28
- '🔗': 'builtIns.join',
29
- '🏷️': 'builtIns.microDataJoin',
30
- '📋': 'builtIns.manageTemplateList',
31
- '📊': 'builtIns.rangeSelector',
32
- };
33
-
34
- export default builtInEmoji;
1
+ /**
2
+ * builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
3
+ *
4
+ * Import and spread into the `handlers` option for concise handler configs:
5
+ *
6
+ * @example
7
+ * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
+ *
9
+ * assignFrom(target, {
10
+ * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
+ * }, { from: vm, handlers: builtInEmoji });
12
+ */
13
+ /**
14
+ * Emoji → built-in handler name mapping.
15
+ *
16
+ * | Emoji | Handler |
17
+ * |-------|---------|
18
+ * | 📦 | builtIns.lazyLoad |
19
+ * | 🎚️ | builtIns.lazyLoadSwitch |
20
+ * | 🔗 | builtIns.join |
21
+ * | 🏷️ | builtIns.microDataJoin |
22
+ * | 📋 | builtIns.manageTemplateList |
23
+ */
24
+ export const builtInEmoji = {
25
+ '📦': 'builtIns.lazyLoad',
26
+ '🎚️': 'builtIns.lazyLoadSwitch',
27
+ '🔗': 'builtIns.join',
28
+ '🏷️': 'builtIns.microDataJoin',
29
+ '📋': 'builtIns.manageTemplateList',
30
+ '📊': 'builtIns.rangeSelector',
31
+ };
32
+ export const akaMethods = {
33
+ '🔍': 'querySelector',
34
+ '🧺': 'querySelectorAll',
35
+ '+': 'add',
36
+ '🧬': 'cloneNode'
37
+ };
38
+ export const aka = {
39
+ '©️': 'content?.cloneNode?.true',
40
+ //'🔎': 'clone?.querySelector'
41
+ };
42
+ export const emojis = {
43
+ builtInEmoji,
44
+ akaMethods,
45
+ };
46
+ export default emojis;
package/emojis.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
3
+ *
4
+ * Import and spread into the `handlers` option for concise handler configs:
5
+ *
6
+ * @example
7
+ * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
+ *
9
+ * assignFrom(target, {
10
+ * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
+ * }, { from: vm, handlers: builtInEmoji });
12
+ */
13
+
14
+ /**
15
+ * Emoji → built-in handler name mapping.
16
+ *
17
+ * | Emoji | Handler |
18
+ * |-------|---------|
19
+ * | 📦 | builtIns.lazyLoad |
20
+ * | 🎚️ | builtIns.lazyLoadSwitch |
21
+ * | 🔗 | builtIns.join |
22
+ * | 🏷️ | builtIns.microDataJoin |
23
+ * | 📋 | builtIns.manageTemplateList |
24
+ */
25
+ export const builtInEmoji: Record<string, string> = {
26
+ '📦': 'builtIns.lazyLoad',
27
+ '🎚️': 'builtIns.lazyLoadSwitch',
28
+ '🔗': 'builtIns.join',
29
+ '🏷️': 'builtIns.microDataJoin',
30
+ '📋': 'builtIns.manageTemplateList',
31
+ '📊': 'builtIns.rangeSelector',
32
+ };
33
+
34
+ export const akaMethods: Record<string, string> = {
35
+ '🔍': 'querySelector',
36
+ '🧺': 'querySelectorAll',
37
+ '+': 'add',
38
+ '🧬': 'cloneNode'
39
+ };
40
+
41
+ export const aka: Record<string, string> = {
42
+ '©️': 'content?.cloneNode?.true',
43
+ //'🔎': 'clone?.querySelector'
44
+ };
45
+
46
+
47
+ export const emojis = {
48
+ builtInEmoji,
49
+ akaMethods,
50
+ }
51
+
52
+ export default emojis;
package/getValues.js CHANGED
@@ -16,7 +16,6 @@
16
16
  * count: 42
17
17
  * }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
18
18
  */
19
- //TODO
20
19
  export function normalizeAliasOptions(options) {
21
20
  const aliasMap = new Map();
22
21
  if (options?.aka) {
package/getValues.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import type { GetValuesOptions } from './types/assign-gingerly/types.js';
21
- //TODO
21
+
22
22
  export function normalizeAliasOptions(options?: {
23
23
  aka?: Record<string, string>;
24
24
  akaMethods?: Record<string, string>;
@@ -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
+ }