assign-gingerly 0.0.53 → 0.0.55

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 (114) hide show
  1. package/README.md +847 -5
  2. package/assignFrom.js +229 -9
  3. package/assignFrom.ts +338 -9
  4. package/assignGingerly.js +34 -1
  5. package/assignGingerly.ts +54 -1
  6. package/beVigilant.js +73 -0
  7. package/beVigilant.ts +85 -0
  8. package/enhanceAll.js +106 -0
  9. package/enhanceAll.ts +138 -0
  10. package/handlers/join.js +74 -0
  11. package/handlers/join.ts +80 -0
  12. package/handlers/lazyLoad.js +212 -0
  13. package/handlers/lazyLoad.ts +307 -0
  14. package/handlers/lazyLoadSwitch.js +58 -0
  15. package/handlers/lazyLoadSwitch.ts +63 -0
  16. package/handlers/microDataJoin.js +184 -0
  17. package/handlers/microDataJoin.ts +270 -0
  18. package/inferencer/.gitmodules +3 -0
  19. package/inferencer/.vscode/settings.json +2 -0
  20. package/inferencer/InferencedPropagator.js +230 -0
  21. package/inferencer/InferencedPropagator.ts +269 -0
  22. package/inferencer/LICENSE +21 -0
  23. package/inferencer/README.md +524 -0
  24. package/inferencer/Requirements/SupportForPropagator.md +368 -0
  25. package/inferencer/imports.html +7 -0
  26. package/inferencer/inferencer.js +254 -0
  27. package/inferencer/inferencer.ts +292 -0
  28. package/inferencer/package-lock.json +129 -0
  29. package/inferencer/package.json +60 -0
  30. package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
  31. package/inferencer/playwright-report/index.html +90 -0
  32. package/inferencer/playwright.config.ts +54 -0
  33. package/inferencer/test-results/.last-run.json +6 -0
  34. package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
  35. package/inferencer/tests/inferencedPropagator.html +428 -0
  36. package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
  37. package/inferencer/tests/inferencer.html +355 -0
  38. package/inferencer/tests/inferencer.spec.ts +19 -0
  39. package/inferencer/tsconfig.json +19 -0
  40. package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
  41. package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
  42. package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
  43. package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
  44. package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
  45. package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
  46. package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
  47. package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
  48. package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
  49. package/inferencer/types/LICENSE +21 -0
  50. package/inferencer/types/NewCustomElement.md +388 -0
  51. package/inferencer/types/NewCustomElementFeature.md +683 -0
  52. package/inferencer/types/NewEnhancementInstructions.md +705 -0
  53. package/inferencer/types/README.md +2 -0
  54. package/inferencer/types/agrace/types.d.ts +11 -0
  55. package/inferencer/types/assign-gingerly/types.d.ts +572 -0
  56. package/inferencer/types/be-a-beacon/types.d.ts +17 -0
  57. package/inferencer/types/be-bound/types.d.ts +66 -0
  58. package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
  59. package/inferencer/types/be-calculating/types.d.ts +54 -0
  60. package/inferencer/types/be-clonable/types.d.ts +38 -0
  61. package/inferencer/types/be-committed/types.d.ts +22 -0
  62. package/inferencer/types/be-consoling/types.d.ts +24 -0
  63. package/inferencer/types/be-decked-with/types.d.ts +26 -0
  64. package/inferencer/types/be-delible/types.d.ts +27 -0
  65. package/inferencer/types/be-dispatching/types.d.ts +34 -0
  66. package/inferencer/types/be-evanescent/types.d.ts +20 -0
  67. package/inferencer/types/be-flashy/types.d.ts +21 -0
  68. package/inferencer/types/be-gone/types.d.ts +25 -0
  69. package/inferencer/types/be-observing/types.d.ts +55 -0
  70. package/inferencer/types/be-reflective/types.d.ts +78 -0
  71. package/inferencer/types/be-reformable/types.d.ts +49 -0
  72. package/inferencer/types/be-render-neutral/types.d.ts +32 -0
  73. package/inferencer/types/be-switched/types.d.ts +146 -0
  74. package/inferencer/types/be-typed/types.d.ts +32 -0
  75. package/inferencer/types/be-valued/types.d.ts +22 -0
  76. package/inferencer/types/data-props/types.d.ts +34 -0
  77. package/inferencer/types/do-inc/types.d.ts +56 -0
  78. package/inferencer/types/do-invoke/types.d.ts +38 -0
  79. package/inferencer/types/do-merge/types.d.ts +28 -0
  80. package/inferencer/types/do-toggle/types.d.ts +31 -0
  81. package/inferencer/types/face-up/types.d.ts +100 -0
  82. package/inferencer/types/fetch-for/types.d.ts +36 -0
  83. package/inferencer/types/folder-picker/types.d.ts +21 -0
  84. package/inferencer/types/global.d.ts +29 -0
  85. package/inferencer/types/id-generation/types.d.ts +26 -0
  86. package/inferencer/types/inferencer/types.d.ts +46 -0
  87. package/inferencer/types/mount-observer/types.d.ts +363 -0
  88. package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
  89. package/inferencer/types/pipe-in/types.d.ts +52 -0
  90. package/inferencer/types/roundabout/types.d.ts +268 -0
  91. package/inferencer/types/soak-up/types.d.ts +40 -0
  92. package/inferencer/types/templ-maker/types.d.ts +43 -0
  93. package/inferencer/types/time-ticker/types.d.ts +62 -0
  94. package/inferencer/types/truth-sourcer/types.d.ts +44 -0
  95. package/inferencer/upSearch.js +27 -0
  96. package/inferencer/upSearch.ts +26 -0
  97. package/inferencer/withScopePerimeter.js +27 -0
  98. package/inferencer/withScopePerimeter.ts +33 -0
  99. package/inferredAssignments.js +38 -0
  100. package/inferredAssignments.ts +65 -0
  101. package/isAllowedImportPath.js +42 -0
  102. package/isAllowedImportPath.ts +53 -0
  103. package/package.json +57 -3
  104. package/paths.js +231 -0
  105. package/paths.ts +413 -0
  106. package/processHandlerCommands.js +188 -0
  107. package/processHandlerCommands.ts +217 -0
  108. package/resolveIdRef.js +144 -0
  109. package/resolveIdRef.ts +170 -0
  110. package/resolveValues.js +41 -2
  111. package/resolveValues.ts +41 -1
  112. package/transitionHelper.js +109 -0
  113. package/transitionHelper.ts +132 -0
  114. package/types/assign-gingerly/types.d.ts +89 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * builtIns.microDataJoin handler for assignFrom.
3
+ *
4
+ * Renders a template array as semantic microdata-annotated DOM elements.
5
+ * Each dynamic value gets an HTML element based on its resolved type,
6
+ * with itemprop set to the property name.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when
9
+ * `do: 'builtIns.microDataJoin'` is encountered.
10
+ */
11
+ const MARKER_START_PREFIX = '?start name="';
12
+ const MARKER_END = '?end';
13
+ const MARKER_NAME = 'microDataJoin';
14
+
15
+ function findMarkers(target) {
16
+ const startText = `${MARKER_START_PREFIX}${MARKER_NAME}"`;
17
+ let startMarker = null;
18
+ let endMarker = null;
19
+ const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
20
+ let node;
21
+ while ((node = walker.nextNode())) {
22
+ if (!startMarker && node.data === startText) {
23
+ startMarker = node;
24
+ } else if (startMarker && !endMarker && node.data === MARKER_END) {
25
+ endMarker = node;
26
+ break;
27
+ }
28
+ }
29
+ return [startMarker, endMarker];
30
+ }
31
+
32
+ function createMarkers(target) {
33
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${MARKER_NAME}"`);
34
+ const endMarker = document.createComment(MARKER_END);
35
+ target.appendChild(startMarker);
36
+ target.appendChild(endMarker);
37
+ return [startMarker, endMarker];
38
+ }
39
+
40
+ function getNodesBetweenMarkers(start, end) {
41
+ const nodes = [];
42
+ let current = start.nextSibling;
43
+ while (current && current !== end) {
44
+ nodes.push(current);
45
+ current = current.nextSibling;
46
+ }
47
+ return nodes;
48
+ }
49
+
50
+ function formatForDisplay(value) {
51
+ if (value instanceof Date) {
52
+ return value.toLocaleDateString();
53
+ }
54
+ if (typeof value === 'number') {
55
+ return value.toLocaleString();
56
+ }
57
+ if (typeof value === 'boolean') {
58
+ return '';
59
+ }
60
+ return String(value ?? '');
61
+ }
62
+
63
+ function formatForAttribute(value) {
64
+ if (value instanceof Date) {
65
+ return value.toISOString();
66
+ }
67
+ return String(value);
68
+ }
69
+
70
+ function createElementForValue(prop, value) {
71
+ if (value instanceof Date) {
72
+ const el = document.createElement('time');
73
+ el.setAttribute('itemprop', prop);
74
+ el.setAttribute('datetime', formatForAttribute(value));
75
+ el.textContent = formatForDisplay(value);
76
+ return el;
77
+ }
78
+ if (typeof value === 'number') {
79
+ const el = document.createElement('data');
80
+ el.setAttribute('itemprop', prop);
81
+ el.setAttribute('value', formatForAttribute(value));
82
+ el.textContent = formatForDisplay(value);
83
+ return el;
84
+ }
85
+ if (typeof value === 'boolean') {
86
+ const el = document.createElement('data');
87
+ el.setAttribute('itemprop', prop);
88
+ el.setAttribute('value', String(value));
89
+ return el;
90
+ }
91
+ const el = document.createElement('span');
92
+ el.setAttribute('itemprop', prop);
93
+ el.textContent = String(value ?? '');
94
+ return el;
95
+ }
96
+
97
+ function updateElement(el, prop, value) {
98
+ if (value instanceof Date) {
99
+ el.setAttribute('datetime', formatForAttribute(value));
100
+ el.textContent = formatForDisplay(value);
101
+ } else if (typeof value === 'number') {
102
+ el.setAttribute('value', formatForAttribute(value));
103
+ el.textContent = formatForDisplay(value);
104
+ } else if (typeof value === 'boolean') {
105
+ el.setAttribute('value', String(value));
106
+ } else {
107
+ el.textContent = String(value ?? '');
108
+ }
109
+ }
110
+
111
+ function processTemplate(template) {
112
+ const result = [];
113
+ for (const item of template) {
114
+ if (Array.isArray(item)) {
115
+ const hasNull = item.some(el =>
116
+ el && typeof el === 'object' && 'prop' in el && el.val == null
117
+ );
118
+ if (hasNull) continue;
119
+ result.push(...processTemplate(item));
120
+ } else if (item == null) {
121
+ continue;
122
+ } else {
123
+ result.push(item);
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+
129
+ export class MicroDataJoinHandler {
130
+ config;
131
+ constructor(config) {
132
+ this.config = config;
133
+ }
134
+ async assign(lhsTarget, resolvedParams) {
135
+ const { template } = resolvedParams;
136
+ if (!template || !Array.isArray(template)) {
137
+ throw new Error('builtIns.microDataJoin: resolve.template must be an array');
138
+ }
139
+ if (!(lhsTarget instanceof Element)) {
140
+ throw new Error('builtIns.microDataJoin: lhsTarget must be a DOM Element');
141
+ }
142
+ if (!lhsTarget.hasAttribute('itemscope')) {
143
+ lhsTarget.setAttribute('itemscope', '');
144
+ }
145
+ const processed = processTemplate(template);
146
+ let [startMarker, endMarker] = findMarkers(lhsTarget);
147
+ const isUpdate = startMarker !== null && endMarker !== null;
148
+ if (!isUpdate) {
149
+ [startMarker, endMarker] = createMarkers(lhsTarget);
150
+ }
151
+ if (isUpdate) {
152
+ const existingNodes = getNodesBetweenMarkers(startMarker, endMarker);
153
+ let nodeIdx = 0;
154
+ for (const segment of processed) {
155
+ if (typeof segment === 'string') {
156
+ const node = existingNodes[nodeIdx];
157
+ if (node && node.nodeType === Node.TEXT_NODE) {
158
+ if (node.textContent !== segment) {
159
+ node.textContent = segment;
160
+ }
161
+ }
162
+ nodeIdx++;
163
+ } else {
164
+ const node = existingNodes[nodeIdx];
165
+ if (node && node instanceof Element && node.getAttribute('itemprop') === segment.prop) {
166
+ updateElement(node, segment.prop, segment.val);
167
+ }
168
+ nodeIdx++;
169
+ }
170
+ }
171
+ } else {
172
+ const fragment = document.createDocumentFragment();
173
+ for (const segment of processed) {
174
+ if (typeof segment === 'string') {
175
+ fragment.appendChild(document.createTextNode(segment));
176
+ } else {
177
+ const el = createElementForValue(segment.prop, segment.val);
178
+ fragment.appendChild(el);
179
+ }
180
+ }
181
+ endMarker.parentNode.insertBefore(fragment, endMarker);
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * builtIns.microDataJoin handler for assignFrom.
3
+ *
4
+ * Renders a template array as semantic microdata-annotated DOM elements.
5
+ * Each dynamic value gets an HTML element based on its resolved type,
6
+ * with itemprop set to the property name.
7
+ *
8
+ * Type → Element mapping:
9
+ * - string → <span itemprop=name>value</span>
10
+ * - number → <data itemprop=name value=raw>formatted</data>
11
+ * - boolean → <data itemprop=name value=true/false></data>
12
+ * - Date → <time itemprop=name datetime=iso>formatted</time>
13
+ * - null/undefined → omitted (or sub-array dropped if in optional segment)
14
+ *
15
+ * Uses comment markers for idempotent updates — first call creates elements,
16
+ * subsequent calls update existing elements in place.
17
+ *
18
+ * This handler is auto-loaded by processHandlerCommands when
19
+ * `do: 'builtIns.microDataJoin'` is encountered.
20
+ *
21
+ * @example
22
+ * assignFrom(oSection, {
23
+ * '?.querySelector?.div =>': {
24
+ * do: 'builtIns.microDataJoin',
25
+ * resolve: {
26
+ * template: [
27
+ * { prop: 'firstName', val: '?.firstName' },
28
+ * ' ',
29
+ * { prop: 'lastName', val: '?.lastName' }
30
+ * ]
31
+ * }
32
+ * }
33
+ * }, { from: vm, withMethods: ['querySelector'] });
34
+ */
35
+
36
+ import type { AssignFromHandler } from '../assignFrom.js';
37
+
38
+ const MARKER_START_PREFIX = '?start name="';
39
+ const MARKER_END = '?end';
40
+ const MARKER_NAME = 'microDataJoin';
41
+
42
+ interface TemplateSegment {
43
+ prop: string;
44
+ val: any;
45
+ format?: string;
46
+ }
47
+
48
+ /**
49
+ * Find existing start/end comment markers in a target element.
50
+ */
51
+ function findMarkers(target: Element): [Comment | null, Comment | null] {
52
+ const startText = `${MARKER_START_PREFIX}${MARKER_NAME}"`;
53
+ let startMarker: Comment | null = null;
54
+ let endMarker: Comment | null = null;
55
+
56
+ const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
57
+ let node: Comment | null;
58
+ while ((node = walker.nextNode() as Comment | null)) {
59
+ if (!startMarker && node.data === startText) {
60
+ startMarker = node;
61
+ } else if (startMarker && !endMarker && node.data === MARKER_END) {
62
+ endMarker = node;
63
+ break;
64
+ }
65
+ }
66
+ return [startMarker, endMarker];
67
+ }
68
+
69
+ /**
70
+ * Create start/end markers in the target.
71
+ */
72
+ function createMarkers(target: Element): [Comment, Comment] {
73
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${MARKER_NAME}"`);
74
+ const endMarker = document.createComment(MARKER_END);
75
+ target.appendChild(startMarker);
76
+ target.appendChild(endMarker);
77
+ return [startMarker, endMarker];
78
+ }
79
+
80
+ /**
81
+ * Get all nodes between start and end markers.
82
+ */
83
+ function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
84
+ const nodes: Node[] = [];
85
+ let current: Node | null = start.nextSibling;
86
+ while (current && current !== end) {
87
+ nodes.push(current);
88
+ current = current.nextSibling;
89
+ }
90
+ return nodes;
91
+ }
92
+
93
+ /**
94
+ * Format a value for display (textContent) using locale.
95
+ */
96
+ function formatForDisplay(value: any): string {
97
+ if (value instanceof Date) {
98
+ return value.toLocaleDateString();
99
+ }
100
+ if (typeof value === 'number') {
101
+ return value.toLocaleString();
102
+ }
103
+ if (typeof value === 'boolean') {
104
+ return ''; // booleans show nothing by default
105
+ }
106
+ return String(value ?? '');
107
+ }
108
+
109
+ /**
110
+ * Format a value for the machine-readable attribute (value or datetime).
111
+ */
112
+ function formatForAttribute(value: any): string {
113
+ if (value instanceof Date) {
114
+ return value.toISOString();
115
+ }
116
+ return String(value);
117
+ }
118
+
119
+ /**
120
+ * Create the appropriate DOM element for a resolved value.
121
+ */
122
+ function createElementForValue(prop: string, value: any): Element {
123
+ if (value instanceof Date) {
124
+ const el = document.createElement('time');
125
+ el.setAttribute('itemprop', prop);
126
+ el.setAttribute('datetime', formatForAttribute(value));
127
+ el.textContent = formatForDisplay(value);
128
+ return el;
129
+ }
130
+ if (typeof value === 'number') {
131
+ const el = document.createElement('data');
132
+ el.setAttribute('itemprop', prop);
133
+ el.setAttribute('value', formatForAttribute(value));
134
+ el.textContent = formatForDisplay(value);
135
+ return el;
136
+ }
137
+ if (typeof value === 'boolean') {
138
+ const el = document.createElement('data');
139
+ el.setAttribute('itemprop', prop);
140
+ el.setAttribute('value', String(value));
141
+ // empty textContent for booleans
142
+ return el;
143
+ }
144
+ // Default: string → span
145
+ const el = document.createElement('span');
146
+ el.setAttribute('itemprop', prop);
147
+ el.textContent = String(value ?? '');
148
+ return el;
149
+ }
150
+
151
+ /**
152
+ * Update an existing element with a new value (in-place update).
153
+ */
154
+ function updateElement(el: Element, prop: string, value: any): void {
155
+ if (value instanceof Date) {
156
+ el.setAttribute('datetime', formatForAttribute(value));
157
+ el.textContent = formatForDisplay(value);
158
+ } else if (typeof value === 'number') {
159
+ el.setAttribute('value', formatForAttribute(value));
160
+ el.textContent = formatForDisplay(value);
161
+ } else if (typeof value === 'boolean') {
162
+ el.setAttribute('value', String(value));
163
+ } else {
164
+ el.textContent = String(value ?? '');
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Process the template array, handling optional (nested) segments.
170
+ * Returns a flat array of items to render, with null sub-arrays dropped.
171
+ */
172
+ function processTemplate(template: any[]): (string | TemplateSegment)[] {
173
+ const result: (string | TemplateSegment)[] = [];
174
+ for (const item of template) {
175
+ if (Array.isArray(item)) {
176
+ // All-or-nothing: if any {prop,val} segment has val === null/undefined, drop entire sub-array
177
+ const hasNull = item.some(el =>
178
+ el && typeof el === 'object' && 'prop' in el && el.val == null
179
+ );
180
+ if (hasNull) continue;
181
+ // Sub-array passes — flatten
182
+ result.push(...processTemplate(item));
183
+ } else if (item == null) {
184
+ continue;
185
+ } else {
186
+ result.push(item);
187
+ }
188
+ }
189
+ return result;
190
+ }
191
+
192
+ /**
193
+ * MicroDataJoinHandler — renders template arrays as semantic microdata DOM.
194
+ */
195
+ export class MicroDataJoinHandler implements AssignFromHandler {
196
+ config: any;
197
+
198
+ constructor(config: any) {
199
+ this.config = config;
200
+ }
201
+
202
+ async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
203
+ const { template } = resolvedParams;
204
+
205
+ if (!template || !Array.isArray(template)) {
206
+ throw new Error('builtIns.microDataJoin: resolve.template must be an array');
207
+ }
208
+
209
+ if (!(lhsTarget instanceof Element)) {
210
+ throw new Error('builtIns.microDataJoin: lhsTarget must be a DOM Element');
211
+ }
212
+
213
+ // Add itemscope to target if not present
214
+ if (!lhsTarget.hasAttribute('itemscope')) {
215
+ lhsTarget.setAttribute('itemscope', '');
216
+ }
217
+
218
+ // Process template (handle optional segments)
219
+ const processed = processTemplate(template);
220
+
221
+ // Find or create markers
222
+ let [startMarker, endMarker] = findMarkers(lhsTarget);
223
+ const isUpdate = startMarker !== null && endMarker !== null;
224
+
225
+ if (!isUpdate) {
226
+ [startMarker, endMarker] = createMarkers(lhsTarget);
227
+ }
228
+
229
+ if (isUpdate) {
230
+ // Update existing nodes in place
231
+ const existingNodes = getNodesBetweenMarkers(startMarker!, endMarker!);
232
+ let nodeIdx = 0;
233
+
234
+ for (const segment of processed) {
235
+ if (typeof segment === 'string') {
236
+ // Literal text — update or skip text node
237
+ const node = existingNodes[nodeIdx];
238
+ if (node && node.nodeType === Node.TEXT_NODE) {
239
+ if (node.textContent !== segment) {
240
+ node.textContent = segment;
241
+ }
242
+ }
243
+ nodeIdx++;
244
+ } else {
245
+ // {prop, val} — update element
246
+ const node = existingNodes[nodeIdx];
247
+ if (node && node instanceof Element && node.getAttribute('itemprop') === segment.prop) {
248
+ updateElement(node, segment.prop, segment.val);
249
+ }
250
+ nodeIdx++;
251
+ }
252
+ }
253
+ } else {
254
+ // First render — create all nodes
255
+ const fragment = document.createDocumentFragment();
256
+
257
+ for (const segment of processed) {
258
+ if (typeof segment === 'string') {
259
+ fragment.appendChild(document.createTextNode(segment));
260
+ } else {
261
+ // {prop, val} object
262
+ const el = createElementForValue(segment.prop, segment.val);
263
+ fragment.appendChild(el);
264
+ }
265
+ }
266
+
267
+ endMarker!.parentNode!.insertBefore(fragment, endMarker!);
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,3 @@
1
+ [submodule "types"]
2
+ path = types
3
+ url = https://github.com/bahrus/types.git
@@ -0,0 +1,2 @@
1
+ {
2
+ }
@@ -0,0 +1,230 @@
1
+ import { inferEventType } from './inferencer.js';
2
+ /**
3
+ * An inferred propagator that extends EventTarget.
4
+ * Emits property change events by intelligently hooking into
5
+ * whatever change detection mechanism is available for the element.
6
+ *
7
+ * Property observation is lazy — wiring only happens when
8
+ * addEventListener is called for a given property name.
9
+ */
10
+ export class InferencedPropagator extends EventTarget {
11
+ #infer;
12
+ #watchedProperties = new Map();
13
+ constructor(infer) {
14
+ super();
15
+ this.#infer = infer;
16
+ }
17
+ addEventListener(type, callback, options) {
18
+ super.addEventListener(type, callback, options);
19
+ if (!this.#watchedProperties.has(type)) {
20
+ // Placeholder cleanup until async wiring completes
21
+ this.#watchedProperties.set(type, () => { });
22
+ this.#wireProperty(type);
23
+ }
24
+ }
25
+ removeEventListener(type, callback, options) {
26
+ super.removeEventListener(type, callback, options);
27
+ }
28
+ async #wireProperty(propName) {
29
+ const element = this.#infer.enhancedElement;
30
+ // Snapshot current value before async setup
31
+ const initialValue = element[propName];
32
+ const cleanup = await this.#setupStrategy(element, propName);
33
+ this.#watchedProperties.set(propName, cleanup);
34
+ // If value changed during async wiring, dispatch immediately
35
+ const currentValue = element[propName];
36
+ if (currentValue !== initialValue) {
37
+ this.dispatchEvent(new Event(propName));
38
+ }
39
+ }
40
+ /**
41
+ * Determine and set up the best observation strategy for a property.
42
+ * Returns a cleanup function.
43
+ */
44
+ async #setupStrategy(element, propName) {
45
+ // Strategy 1: Attribute-reflected properties
46
+ const attrName = this.#toAttributeName(propName);
47
+ if (this.#isAttributeReflected(element, propName, attrName)) {
48
+ return this.#observeAttribute(element, propName, attrName);
49
+ }
50
+ // Strategy 2: Native event (user-driven) + setter interception (programmatic)
51
+ const eventType = this.#getNativeEventType(element, propName);
52
+ if (eventType) {
53
+ return this.#observeHybrid(element, propName, eventType);
54
+ }
55
+ // Strategy 3: Setter interception (custom elements or known prototypes)
56
+ const descriptor = this.#getPropertyDescriptor(element, propName);
57
+ if (descriptor?.set) {
58
+ return this.#interceptSetter(element, propName, descriptor);
59
+ }
60
+ // Strategy 4: Polling fallback
61
+ return this.#observePolling(element, propName);
62
+ }
63
+ // ─── Strategy Implementations ────────────────────────────────────────
64
+ /**
65
+ * Strategy 1: Use MutationObserver on the corresponding attribute.
66
+ */
67
+ #observeAttribute(element, propName, attrName) {
68
+ const observer = new MutationObserver(() => {
69
+ this.dispatchEvent(new Event(propName));
70
+ });
71
+ observer.observe(element, {
72
+ attributes: true,
73
+ attributeFilter: [attrName],
74
+ });
75
+ return () => observer.disconnect();
76
+ }
77
+ /**
78
+ * Strategy 2: Listen for native events AND intercept setter for programmatic changes.
79
+ */
80
+ #observeHybrid(element, propName, eventType) {
81
+ const ac = new AbortController();
82
+ const { signal } = ac;
83
+ // Listen for user-driven events
84
+ element.addEventListener(eventType, () => {
85
+ this.dispatchEvent(new Event(propName));
86
+ }, { signal });
87
+ // Also intercept setter for programmatic changes
88
+ const descriptor = this.#getPropertyDescriptor(element, propName);
89
+ let restoreSetter;
90
+ if (descriptor?.set) {
91
+ restoreSetter = this.#interceptSetter(element, propName, descriptor);
92
+ }
93
+ return () => {
94
+ ac.abort();
95
+ restoreSetter?.();
96
+ };
97
+ }
98
+ /**
99
+ * Strategy 3: Override the property setter on the instance to detect programmatic writes.
100
+ */
101
+ #interceptSetter(element, propName, descriptor) {
102
+ const originalSet = descriptor.set;
103
+ const originalGet = descriptor.get;
104
+ const self = this;
105
+ Object.defineProperty(element, propName, {
106
+ get: originalGet ? function () { return originalGet.call(this); } : undefined,
107
+ set(newValue) {
108
+ const oldValue = originalGet ? originalGet.call(this) : undefined;
109
+ originalSet.call(this, newValue);
110
+ if (newValue !== oldValue) {
111
+ self.dispatchEvent(new Event(propName));
112
+ }
113
+ },
114
+ configurable: true,
115
+ enumerable: descriptor.enumerable,
116
+ });
117
+ return () => {
118
+ // Restore by deleting instance property — prototype descriptor takes over again
119
+ delete element[propName];
120
+ };
121
+ }
122
+ /**
123
+ * Strategy 4: Polling via requestAnimationFrame.
124
+ */
125
+ #observePolling(element, propName) {
126
+ let active = true;
127
+ let lastValue = element[propName];
128
+ const poll = () => {
129
+ if (!active)
130
+ return;
131
+ const currentValue = element[propName];
132
+ if (currentValue !== lastValue) {
133
+ lastValue = currentValue;
134
+ this.dispatchEvent(new Event(propName));
135
+ }
136
+ requestAnimationFrame(poll);
137
+ };
138
+ requestAnimationFrame(poll);
139
+ return () => { active = false; };
140
+ }
141
+ // ─── Helpers ─────────────────────────────────────────────────────────
142
+ /**
143
+ * Convert a camelCase property name to its kebab-case attribute equivalent.
144
+ * e.g. ariaValueNow -> aria-valuenow
145
+ */
146
+ #toAttributeName(propName) {
147
+ // aria properties have a direct mapping
148
+ if (propName.startsWith('aria')) {
149
+ return propName.replace(/([A-Z])/g, '-$1').toLowerCase();
150
+ }
151
+ // General camelCase to kebab-case
152
+ return propName.replace(/([A-Z])/g, '-$1').toLowerCase();
153
+ }
154
+ /**
155
+ * Check if setting a property updates the corresponding attribute.
156
+ */
157
+ #isAttributeReflected(element, propName, attrName) {
158
+ // For form elements, 'value' attribute is only the default — property changes
159
+ // from user input do NOT update the attribute, so it's not truly reflected.
160
+ const { localName } = element;
161
+ if (propName === 'value' && ['input', 'textarea', 'select', 'button'].includes(localName)) {
162
+ return false;
163
+ }
164
+ // For <data>, <meter>, <output>, <progress> the value property reflects to the value attribute
165
+ if (propName === 'value' && ['data', 'meter', 'output', 'progress'].includes(localName)) {
166
+ return true;
167
+ }
168
+ // Quick check: does the attribute currently exist or is the property known to reflect?
169
+ if (element.hasAttribute(attrName))
170
+ return true;
171
+ // For aria-* properties, they are spec-defined as reflecting
172
+ if (propName.startsWith('aria'))
173
+ return true;
174
+ // For href, src on appropriate elements
175
+ const reflectedProps = {
176
+ 'href': ['a', 'area', 'link', 'base'],
177
+ 'src': ['img', 'script', 'iframe', 'audio', 'video', 'source', 'embed'],
178
+ 'action': ['form'],
179
+ 'value': ['option', 'param', 'li'],
180
+ };
181
+ if (reflectedProps[propName]?.includes(localName))
182
+ return true;
183
+ return false;
184
+ }
185
+ /**
186
+ * Determine the native event type that fires when a property changes (user-driven).
187
+ * Leverages Infer's inferEventType for elements known to have input-like behavior.
188
+ */
189
+ #getNativeEventType(element, propName) {
190
+ const { localName } = element;
191
+ // Only applicable for form-like elements with value/checked properties
192
+ if (['input', 'textarea', 'select'].includes(localName)) {
193
+ if (propName === 'value' || propName === 'valueAsNumber' || propName === 'valueAsDate' || propName === 'checked' || propName === 'selectedIndex') {
194
+ return inferEventType(element);
195
+ }
196
+ }
197
+ if (localName === 'details' && propName === 'open') {
198
+ return 'toggle';
199
+ }
200
+ // contentEditable elements fire 'input' events on user edits
201
+ if (element instanceof HTMLElement && element.isContentEditable) {
202
+ if (propName === 'textContent' || propName === 'innerHTML' || propName === 'innerText') {
203
+ return 'input';
204
+ }
205
+ }
206
+ return undefined;
207
+ }
208
+ /**
209
+ * Walk the prototype chain to find a property descriptor.
210
+ */
211
+ #getPropertyDescriptor(element, propName) {
212
+ let proto = Object.getPrototypeOf(element);
213
+ while (proto && proto !== Object.prototype) {
214
+ const desc = Object.getOwnPropertyDescriptor(proto, propName);
215
+ if (desc)
216
+ return desc;
217
+ proto = Object.getPrototypeOf(proto);
218
+ }
219
+ return undefined;
220
+ }
221
+ /**
222
+ * Tear down all watchers and clean up resources.
223
+ */
224
+ destroy() {
225
+ for (const cleanup of this.#watchedProperties.values()) {
226
+ cleanup();
227
+ }
228
+ this.#watchedProperties.clear();
229
+ }
230
+ }