assign-gingerly 0.0.28 → 0.0.30
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/Infer.js +154 -0
- package/Infer.ts +190 -0
- package/README.md +453 -0
- package/getHost.js +51 -0
- package/getHost.ts +58 -0
- package/index.js +1 -0
- package/index.ts +1 -0
- package/object-extension.js +68 -0
- package/object-extension.ts +78 -0
- package/package.json +79 -71
- package/parseWithAttrs.js +3 -3
- package/parseWithAttrs.ts +3 -3
- package/types/assign-gingerly/types.d.ts +7 -1
package/Infer.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol for smart value assignment
|
|
3
|
+
* When used with element.set[value], it infers and sets the appropriate value property
|
|
4
|
+
*/
|
|
5
|
+
export const value = Symbol.for('assign-gingerly:value');
|
|
6
|
+
/**
|
|
7
|
+
* Symbol for smart display assignment
|
|
8
|
+
* When used with element.set[display], it infers and sets the appropriate display property
|
|
9
|
+
*/
|
|
10
|
+
export const display = Symbol.for('assign-gingerly:display');
|
|
11
|
+
/**
|
|
12
|
+
* Enhancement class that provides smart value and display property inference
|
|
13
|
+
* Automatically determines the correct property to set based on element type
|
|
14
|
+
*/
|
|
15
|
+
export class Infer {
|
|
16
|
+
#weakRef;
|
|
17
|
+
get enhancedElement() {
|
|
18
|
+
return this.#weakRef.deref();
|
|
19
|
+
}
|
|
20
|
+
constructor(enhancedElement) {
|
|
21
|
+
this.#weakRef = new WeakRef(enhancedElement);
|
|
22
|
+
}
|
|
23
|
+
#value;
|
|
24
|
+
get value() {
|
|
25
|
+
return this.#value;
|
|
26
|
+
}
|
|
27
|
+
set value(nv) {
|
|
28
|
+
this.#value = nv;
|
|
29
|
+
const { enhancedElement } = this;
|
|
30
|
+
enhancedElement[inferValueProperty(enhancedElement)] = nv;
|
|
31
|
+
}
|
|
32
|
+
#display;
|
|
33
|
+
get display() {
|
|
34
|
+
return this.#display;
|
|
35
|
+
}
|
|
36
|
+
set display(nv) {
|
|
37
|
+
this.#display = nv;
|
|
38
|
+
const { enhancedElement } = this;
|
|
39
|
+
enhancedElement[inferDisplayProperty(enhancedElement)] = nv;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the inferred event type for the element
|
|
43
|
+
* @returns The most appropriate event type for this element
|
|
44
|
+
*/
|
|
45
|
+
get eventType() {
|
|
46
|
+
return inferEventType(this.enhancedElement);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Registry item for the Infer enhancement
|
|
51
|
+
* Register this with customElements.enhancementRegistry to enable smart value/display assignment
|
|
52
|
+
*/
|
|
53
|
+
export const registryItem = {
|
|
54
|
+
spawn: Infer,
|
|
55
|
+
enhKey: 'infer',
|
|
56
|
+
symlinks: {
|
|
57
|
+
[value]: 'value',
|
|
58
|
+
[display]: 'display'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Infer the most appropriate value property for an element
|
|
63
|
+
* @param element - The element to infer the property for
|
|
64
|
+
* @returns The property name to use for value assignment
|
|
65
|
+
*/
|
|
66
|
+
export function inferValueProperty(element) {
|
|
67
|
+
const tagName = element.localName;
|
|
68
|
+
// Input elements - check type attribute
|
|
69
|
+
if (tagName === 'input') {
|
|
70
|
+
const type = element.getAttribute('type')?.toLowerCase();
|
|
71
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
72
|
+
return 'checked';
|
|
73
|
+
}
|
|
74
|
+
return 'value';
|
|
75
|
+
}
|
|
76
|
+
// Form controls with value property
|
|
77
|
+
if (tagName === 'textarea' || tagName === 'select') {
|
|
78
|
+
return 'value';
|
|
79
|
+
}
|
|
80
|
+
// Semantic HTML elements with specific properties
|
|
81
|
+
if (tagName === 'time') {
|
|
82
|
+
return 'dateTime';
|
|
83
|
+
}
|
|
84
|
+
if (tagName === 'data') {
|
|
85
|
+
return 'value';
|
|
86
|
+
}
|
|
87
|
+
if (tagName === 'meter' || tagName === 'progress') {
|
|
88
|
+
return 'value';
|
|
89
|
+
}
|
|
90
|
+
if (tagName === 'output') {
|
|
91
|
+
return 'value';
|
|
92
|
+
}
|
|
93
|
+
// Check for itemprop attribute as a hint
|
|
94
|
+
const itemprop = element.getAttribute('itemprop');
|
|
95
|
+
if (itemprop) {
|
|
96
|
+
return itemprop;
|
|
97
|
+
}
|
|
98
|
+
// Default fallback
|
|
99
|
+
return 'textContent';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Infer the most appropriate display property for an element
|
|
103
|
+
* @param element - The element to infer the property for
|
|
104
|
+
* @returns The property name to use for display assignment
|
|
105
|
+
*/
|
|
106
|
+
export function inferDisplayProperty(element) {
|
|
107
|
+
const tagName = element.localName;
|
|
108
|
+
// Form controls display their value
|
|
109
|
+
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
|
110
|
+
return 'value';
|
|
111
|
+
}
|
|
112
|
+
// Time elements display formatted time
|
|
113
|
+
if (tagName === 'time') {
|
|
114
|
+
return 'textContent';
|
|
115
|
+
}
|
|
116
|
+
// Data elements display human-readable content
|
|
117
|
+
if (tagName === 'data') {
|
|
118
|
+
return 'textContent';
|
|
119
|
+
}
|
|
120
|
+
// Progress/meter elements use ARIA for display
|
|
121
|
+
if (tagName === 'meter' || tagName === 'progress') {
|
|
122
|
+
return 'ariaValueText';
|
|
123
|
+
}
|
|
124
|
+
// Default fallback
|
|
125
|
+
return 'textContent';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Infer the most appropriate event type for an element
|
|
129
|
+
* Used when no explicit event type is provided
|
|
130
|
+
* @param element - The element to infer the event type for
|
|
131
|
+
* @returns The event type name like 'input', 'change', 'click', 'submit'
|
|
132
|
+
*/
|
|
133
|
+
export function inferEventType(element) {
|
|
134
|
+
const tagName = element.localName;
|
|
135
|
+
// Form controls that support input event
|
|
136
|
+
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
|
137
|
+
return 'input';
|
|
138
|
+
}
|
|
139
|
+
// Form submission
|
|
140
|
+
if (tagName === 'form') {
|
|
141
|
+
return 'submit';
|
|
142
|
+
}
|
|
143
|
+
// Details element
|
|
144
|
+
if (tagName === 'details') {
|
|
145
|
+
return 'toggle';
|
|
146
|
+
}
|
|
147
|
+
// Dialog element
|
|
148
|
+
if (tagName === 'dialog') {
|
|
149
|
+
return 'close';
|
|
150
|
+
}
|
|
151
|
+
// Default fallback for interactive elements
|
|
152
|
+
return 'click';
|
|
153
|
+
}
|
|
154
|
+
export default registryItem;
|
package/Infer.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { EnhancementConfig } from "./types/assign-gingerly/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Symbol for smart value assignment
|
|
5
|
+
* When used with element.set[value], it infers and sets the appropriate value property
|
|
6
|
+
*/
|
|
7
|
+
export const value = Symbol.for('assign-gingerly:value');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Symbol for smart display assignment
|
|
11
|
+
* When used with element.set[display], it infers and sets the appropriate display property
|
|
12
|
+
*/
|
|
13
|
+
export const display = Symbol.for('assign-gingerly:display');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enhancement class that provides smart value and display property inference
|
|
17
|
+
* Automatically determines the correct property to set based on element type
|
|
18
|
+
*/
|
|
19
|
+
export class Infer<TValue = any, TDisplay = any> {
|
|
20
|
+
#weakRef: WeakRef<Element>;
|
|
21
|
+
|
|
22
|
+
get enhancedElement(){
|
|
23
|
+
return this.#weakRef.deref()!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
constructor(enhancedElement?: Element){
|
|
27
|
+
this.#weakRef = new WeakRef(enhancedElement!);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#value: TValue | undefined;
|
|
31
|
+
|
|
32
|
+
get value(): TValue | undefined {
|
|
33
|
+
return this.#value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set value(nv: TValue){
|
|
37
|
+
this.#value = nv;
|
|
38
|
+
const {enhancedElement} = this;
|
|
39
|
+
(enhancedElement as any)[inferValueProperty(enhancedElement)] = nv;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#display: TDisplay | undefined;
|
|
43
|
+
|
|
44
|
+
get display(): TDisplay | undefined {
|
|
45
|
+
return this.#display;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set display(nv: TDisplay){
|
|
49
|
+
this.#display = nv;
|
|
50
|
+
const {enhancedElement} = this;
|
|
51
|
+
(enhancedElement as any)[inferDisplayProperty(enhancedElement)] = nv;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the inferred event type for the element
|
|
56
|
+
* @returns The most appropriate event type for this element
|
|
57
|
+
*/
|
|
58
|
+
get eventType(): string {
|
|
59
|
+
return inferEventType(this.enhancedElement);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Registry item for the Infer enhancement
|
|
65
|
+
* Register this with customElements.enhancementRegistry to enable smart value/display assignment
|
|
66
|
+
*/
|
|
67
|
+
export const registryItem: EnhancementConfig = {
|
|
68
|
+
spawn: Infer,
|
|
69
|
+
enhKey: 'infer',
|
|
70
|
+
symlinks: {
|
|
71
|
+
[value]: 'value',
|
|
72
|
+
[display]: 'display'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Infer the most appropriate value property for an element
|
|
78
|
+
* @param element - The element to infer the property for
|
|
79
|
+
* @returns The property name to use for value assignment
|
|
80
|
+
*/
|
|
81
|
+
export function inferValueProperty(element: Element): string {
|
|
82
|
+
const tagName = element.localName;
|
|
83
|
+
|
|
84
|
+
// Input elements - check type attribute
|
|
85
|
+
if (tagName === 'input') {
|
|
86
|
+
const type = element.getAttribute('type')?.toLowerCase();
|
|
87
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
88
|
+
return 'checked';
|
|
89
|
+
}
|
|
90
|
+
return 'value';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Form controls with value property
|
|
94
|
+
if (tagName === 'textarea' || tagName === 'select') {
|
|
95
|
+
return 'value';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Semantic HTML elements with specific properties
|
|
99
|
+
if (tagName === 'time') {
|
|
100
|
+
return 'dateTime';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (tagName === 'data') {
|
|
104
|
+
return 'value';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (tagName === 'meter' || tagName === 'progress') {
|
|
108
|
+
return 'value';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (tagName === 'output') {
|
|
112
|
+
return 'value';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for itemprop attribute as a hint
|
|
116
|
+
const itemprop = element.getAttribute('itemprop');
|
|
117
|
+
if (itemprop) {
|
|
118
|
+
return itemprop;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Default fallback
|
|
122
|
+
return 'textContent';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Infer the most appropriate display property for an element
|
|
127
|
+
* @param element - The element to infer the property for
|
|
128
|
+
* @returns The property name to use for display assignment
|
|
129
|
+
*/
|
|
130
|
+
export function inferDisplayProperty(element: Element): string {
|
|
131
|
+
const tagName = element.localName;
|
|
132
|
+
|
|
133
|
+
// Form controls display their value
|
|
134
|
+
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
|
135
|
+
return 'value';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Time elements display formatted time
|
|
139
|
+
if (tagName === 'time') {
|
|
140
|
+
return 'textContent';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Data elements display human-readable content
|
|
144
|
+
if (tagName === 'data') {
|
|
145
|
+
return 'textContent';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Progress/meter elements use ARIA for display
|
|
149
|
+
if (tagName === 'meter' || tagName === 'progress') {
|
|
150
|
+
return 'ariaValueText';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Default fallback
|
|
154
|
+
return 'textContent';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Infer the most appropriate event type for an element
|
|
159
|
+
* Used when no explicit event type is provided
|
|
160
|
+
* @param element - The element to infer the event type for
|
|
161
|
+
* @returns The event type name like 'input', 'change', 'click', 'submit'
|
|
162
|
+
*/
|
|
163
|
+
export function inferEventType(element: Element): string {
|
|
164
|
+
const tagName = element.localName;
|
|
165
|
+
|
|
166
|
+
// Form controls that support input event
|
|
167
|
+
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
|
168
|
+
return 'input';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Form submission
|
|
172
|
+
if (tagName === 'form') {
|
|
173
|
+
return 'submit';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Details element
|
|
177
|
+
if (tagName === 'details') {
|
|
178
|
+
return 'toggle';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Dialog element
|
|
182
|
+
if (tagName === 'dialog') {
|
|
183
|
+
return 'close';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Default fallback for interactive elements
|
|
187
|
+
return 'click';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default registryItem;
|
package/README.md
CHANGED
|
@@ -1427,6 +1427,150 @@ element.enh.dispose(registryItem); // Stops timer and cleans up
|
|
|
1427
1427
|
- Calling `enh.get()` again will create a new instance
|
|
1428
1428
|
- The enhancement property is removed from the enh container
|
|
1429
1429
|
|
|
1430
|
+
#### Memory Management and When to Call Dispose
|
|
1431
|
+
|
|
1432
|
+
**Important: Understanding automatic vs manual cleanup**
|
|
1433
|
+
|
|
1434
|
+
The enhancement storage system uses a **WeakMap** to prevent memory leaks:
|
|
1435
|
+
|
|
1436
|
+
```TypeScript
|
|
1437
|
+
// Global storage: WeakMap<Element, Map<EnhancementConfig, Instance>>
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
**What this means for memory:**
|
|
1441
|
+
|
|
1442
|
+
✅ **Automatic cleanup when elements are garbage collected:**
|
|
1443
|
+
- When an element is GC'd, the WeakMap entry is automatically removed
|
|
1444
|
+
- Both `enhKey` references (`element.enh[enhKey]`) and WeakMap entries are cleaned up
|
|
1445
|
+
- **No memory leak from the storage mechanism itself**
|
|
1446
|
+
|
|
1447
|
+
⚠️ **Manual cleanup needed for enhancement internals:**
|
|
1448
|
+
- Event listeners on global objects (window, document)
|
|
1449
|
+
- Timers (setInterval, setTimeout)
|
|
1450
|
+
- External registries or caches
|
|
1451
|
+
- Network connections or subscriptions
|
|
1452
|
+
|
|
1453
|
+
**The challenge: Knowing WHEN to dispose**
|
|
1454
|
+
|
|
1455
|
+
JavaScript provides no way to detect when an element is about to be garbage collected. Additionally, DOM disconnection doesn't reliably indicate disposal:
|
|
1456
|
+
|
|
1457
|
+
```TypeScript
|
|
1458
|
+
// Element disconnected - but should we dispose?
|
|
1459
|
+
element.remove();
|
|
1460
|
+
|
|
1461
|
+
// Case 1: Temporarily removed, will be re-added
|
|
1462
|
+
setTimeout(() => document.body.append(element), 1000);
|
|
1463
|
+
// ❌ Don't dispose - enhancement should persist
|
|
1464
|
+
|
|
1465
|
+
// Case 2: Moved to another location
|
|
1466
|
+
otherContainer.append(element);
|
|
1467
|
+
// ❌ Don't dispose - enhancement should persist
|
|
1468
|
+
|
|
1469
|
+
// Case 3: Cached for reuse
|
|
1470
|
+
elementCache.set('myElement', element);
|
|
1471
|
+
// ❌ Don't dispose - enhancement should persist
|
|
1472
|
+
|
|
1473
|
+
// Case 4: Truly done, ready for GC
|
|
1474
|
+
element = null;
|
|
1475
|
+
// ✅ Should dispose, but no way to detect this automatically
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
**Practical disposal strategies:**
|
|
1479
|
+
|
|
1480
|
+
1. **Short-lived elements:** Don't worry about disposal - WeakMap handles cleanup automatically when elements are GC'd
|
|
1481
|
+
|
|
1482
|
+
2. **Long-lived applications:** Implement manual disposal at logical boundaries:
|
|
1483
|
+
```TypeScript
|
|
1484
|
+
// On route change
|
|
1485
|
+
router.beforeLeave(() => {
|
|
1486
|
+
oldRouteElements.forEach(el => el.enh.dispose(registryItem));
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
// On explicit user action
|
|
1490
|
+
closeButton.onclick = () => {
|
|
1491
|
+
dialog.enh.dispose(registryItem);
|
|
1492
|
+
dialog.remove();
|
|
1493
|
+
};
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1496
|
+
3. **Framework integration:** Use framework lifecycle hooks:
|
|
1497
|
+
```TypeScript
|
|
1498
|
+
// React
|
|
1499
|
+
useEffect(() => {
|
|
1500
|
+
return () => elementRef.current?.enh.dispose(registryItem);
|
|
1501
|
+
}, []);
|
|
1502
|
+
|
|
1503
|
+
// Vue
|
|
1504
|
+
onUnmounted(() => {
|
|
1505
|
+
element.value?.enh.dispose(registryItem);
|
|
1506
|
+
});
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
4. **MutationObserver heuristic:** Watch for disconnection + timeout (imperfect but practical):
|
|
1510
|
+
```TypeScript
|
|
1511
|
+
const observer = new MutationObserver(() => {
|
|
1512
|
+
if (!element.isConnected) {
|
|
1513
|
+
setTimeout(() => {
|
|
1514
|
+
if (!element.isConnected) {
|
|
1515
|
+
element.enh.dispose(registryItem);
|
|
1516
|
+
}
|
|
1517
|
+
}, 5000); // If still disconnected after 5s, probably done
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
**Best practices for enhancement authors:**
|
|
1523
|
+
|
|
1524
|
+
Always implement proper cleanup in your dispose method:
|
|
1525
|
+
|
|
1526
|
+
```TypeScript
|
|
1527
|
+
class MyEnhancement {
|
|
1528
|
+
element;
|
|
1529
|
+
timerId = null;
|
|
1530
|
+
boundHandler = null;
|
|
1531
|
+
|
|
1532
|
+
constructor(element, ctx) {
|
|
1533
|
+
this.element = element;
|
|
1534
|
+
this.boundHandler = this.handleClick.bind(this);
|
|
1535
|
+
|
|
1536
|
+
// Local listener - OK, will be GC'd with element
|
|
1537
|
+
element.addEventListener('click', this.boundHandler);
|
|
1538
|
+
|
|
1539
|
+
// Global listener - MUST clean up manually
|
|
1540
|
+
window.addEventListener('resize', this.boundHandler);
|
|
1541
|
+
|
|
1542
|
+
// Timer - MUST clean up manually
|
|
1543
|
+
this.timerId = setInterval(() => this.update(), 1000);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
dispose() {
|
|
1547
|
+
// Clean up global listener
|
|
1548
|
+
if (this.boundHandler) {
|
|
1549
|
+
window.removeEventListener('resize', this.boundHandler);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Clean up timer
|
|
1553
|
+
if (this.timerId) {
|
|
1554
|
+
clearInterval(this.timerId);
|
|
1555
|
+
this.timerId = null;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Clear references
|
|
1559
|
+
this.element = null;
|
|
1560
|
+
this.boundHandler = null;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
handleClick() { /* ... */ }
|
|
1564
|
+
update() { /* ... */ }
|
|
1565
|
+
}
|
|
1566
|
+
```
|
|
1567
|
+
|
|
1568
|
+
**Summary:**
|
|
1569
|
+
- ✅ Storage mechanism prevents memory leaks via WeakMap
|
|
1570
|
+
- ⚠️ Enhancement internals need manual cleanup via dispose()
|
|
1571
|
+
- ❌ No automatic way to detect when disposal should happen
|
|
1572
|
+
- 👍 Choose disposal strategy based on your application's lifecycle
|
|
1573
|
+
|
|
1430
1574
|
### Waiting for Async Initialization with `enh.whenResolved(regItem)`
|
|
1431
1575
|
|
|
1432
1576
|
The `enh.whenResolved(regItem)` method provides a way to wait for asynchronous enhancement initialization:
|
|
@@ -3267,3 +3411,312 @@ This design ensures backward compatibility while providing powerful new capabili
|
|
|
3267
3411
|
|
|
3268
3412
|
|
|
3269
3413
|
|
|
3414
|
+
## Smart Value Assignment with Infer Enhancement
|
|
3415
|
+
|
|
3416
|
+
The Infer enhancement provides a symbol-based API for smart value and display property assignment. Instead of manually determining which property to set on different element types (e.g., `value` for inputs, `checked` for checkboxes, `textContent` for divs), the Infer enhancement automatically infers the correct property based on the element type.
|
|
3417
|
+
|
|
3418
|
+
### Why Infer?
|
|
3419
|
+
|
|
3420
|
+
Different HTML elements use different properties to represent their value:
|
|
3421
|
+
- Input text fields use `value`
|
|
3422
|
+
- Checkboxes and radio buttons use `checked`
|
|
3423
|
+
- Time elements use `dateTime`
|
|
3424
|
+
- Divs and spans use `textContent`
|
|
3425
|
+
- Progress and meter elements use `value` but display with `ariaValueText`
|
|
3426
|
+
|
|
3427
|
+
The Infer enhancement eliminates the need to remember these differences by providing two symbols that automatically map to the correct property:
|
|
3428
|
+
|
|
3429
|
+
- `value` symbol - Sets the element's data value
|
|
3430
|
+
- `display` symbol - Sets the element's display/presentation value
|
|
3431
|
+
|
|
3432
|
+
### Basic Usage
|
|
3433
|
+
|
|
3434
|
+
```TypeScript
|
|
3435
|
+
import { value, display, registryItem } from 'assign-gingerly/Infer.js';
|
|
3436
|
+
import 'assign-gingerly/object-extension.js';
|
|
3437
|
+
|
|
3438
|
+
// Register the Infer enhancement
|
|
3439
|
+
customElements.enhancementRegistry.push(registryItem);
|
|
3440
|
+
|
|
3441
|
+
// Use the value symbol - automatically sets the right property
|
|
3442
|
+
const input = document.createElement('input');
|
|
3443
|
+
input.type = 'text';
|
|
3444
|
+
input.set[value] = 'hello';
|
|
3445
|
+
console.log(input.value); // 'hello'
|
|
3446
|
+
|
|
3447
|
+
const checkbox = document.createElement('input');
|
|
3448
|
+
checkbox.type = 'checkbox';
|
|
3449
|
+
checkbox.set[value] = true;
|
|
3450
|
+
console.log(checkbox.checked); // true
|
|
3451
|
+
|
|
3452
|
+
const div = document.createElement('div');
|
|
3453
|
+
div.set[value] = 'content';
|
|
3454
|
+
console.log(div.textContent); // 'content'
|
|
3455
|
+
|
|
3456
|
+
const time = document.createElement('time');
|
|
3457
|
+
time.set[value] = '2024-01-01T00:00:00Z';
|
|
3458
|
+
console.log(time.dateTime); // '2024-01-01T00:00:00Z'
|
|
3459
|
+
```
|
|
3460
|
+
|
|
3461
|
+
### Value Property Inference
|
|
3462
|
+
|
|
3463
|
+
The `value` symbol automatically maps to the most appropriate property for each element type:
|
|
3464
|
+
|
|
3465
|
+
| Element Type | Property Set | Example |
|
|
3466
|
+
|-------------|-------------|---------|
|
|
3467
|
+
| `<input type="text">` | `value` | Text input value |
|
|
3468
|
+
| `<input type="checkbox">` | `checked` | Checkbox state |
|
|
3469
|
+
| `<input type="radio">` | `checked` | Radio button state |
|
|
3470
|
+
| `<textarea>` | `value` | Textarea content |
|
|
3471
|
+
| `<select>` | `value` | Selected option |
|
|
3472
|
+
| `<time>` | `dateTime` | ISO datetime string |
|
|
3473
|
+
| `<data>` | `value` | Machine-readable value |
|
|
3474
|
+
| `<meter>` | `value` | Numeric value |
|
|
3475
|
+
| `<progress>` | `value` | Progress value |
|
|
3476
|
+
| `<output>` | `value` | Output value |
|
|
3477
|
+
| Elements with `itemprop` | `itemprop` value | Custom property name |
|
|
3478
|
+
| Other elements | `textContent` | Text content |
|
|
3479
|
+
|
|
3480
|
+
### Display Property Inference
|
|
3481
|
+
|
|
3482
|
+
The `display` symbol sets the human-readable display value:
|
|
3483
|
+
|
|
3484
|
+
```TypeScript
|
|
3485
|
+
// Time element - display formatted time
|
|
3486
|
+
const time = document.createElement('time');
|
|
3487
|
+
time.set[value] = '2024-01-01T00:00:00Z'; // Machine-readable
|
|
3488
|
+
time.set[display] = 'January 1, 2024'; // Human-readable
|
|
3489
|
+
console.log(time.dateTime); // '2024-01-01T00:00:00Z'
|
|
3490
|
+
console.log(time.textContent); // 'January 1, 2024'
|
|
3491
|
+
|
|
3492
|
+
// Meter element - display with ARIA
|
|
3493
|
+
const meter = document.createElement('meter');
|
|
3494
|
+
meter.min = 0;
|
|
3495
|
+
meter.max = 100;
|
|
3496
|
+
meter.set[value] = 75; // Numeric value
|
|
3497
|
+
meter.set[display] = '75 percent'; // Screen reader text
|
|
3498
|
+
console.log(meter.value); // 75
|
|
3499
|
+
console.log(meter.ariaValueText); // '75 percent'
|
|
3500
|
+
```
|
|
3501
|
+
|
|
3502
|
+
| Element Type | Property Set | Example |
|
|
3503
|
+
|-------------|-------------|---------|
|
|
3504
|
+
| `<input>`, `<textarea>`, `<select>` | `value` | Form control value |
|
|
3505
|
+
| `<time>` | `textContent` | Formatted time string |
|
|
3506
|
+
| `<data>` | `textContent` | Human-readable content |
|
|
3507
|
+
| `<meter>`, `<progress>` | `ariaValueText` | Screen reader text |
|
|
3508
|
+
| Other elements | `textContent` | Text content |
|
|
3509
|
+
|
|
3510
|
+
### Accessing the Enhancement Instance
|
|
3511
|
+
|
|
3512
|
+
The Infer enhancement is accessible via `element.enh.infer`:
|
|
3513
|
+
|
|
3514
|
+
```TypeScript
|
|
3515
|
+
const input = document.createElement('input');
|
|
3516
|
+
input.set[value] = 'test';
|
|
3517
|
+
|
|
3518
|
+
// Access the enhancement instance
|
|
3519
|
+
console.log(input.enh.infer.value); // 'test' (cached value)
|
|
3520
|
+
|
|
3521
|
+
// The instance maintains references to both value and display
|
|
3522
|
+
input.set[display] = 'Test Display';
|
|
3523
|
+
console.log(input.enh.infer.value); // 'test'
|
|
3524
|
+
console.log(input.enh.infer.display); // 'Test Display'
|
|
3525
|
+
```
|
|
3526
|
+
|
|
3527
|
+
### Using with assignGingerly
|
|
3528
|
+
|
|
3529
|
+
The Infer enhancement integrates seamlessly with `assignGingerly`:
|
|
3530
|
+
|
|
3531
|
+
```TypeScript
|
|
3532
|
+
import { value, display } from 'assign-gingerly/Infer.js';
|
|
3533
|
+
|
|
3534
|
+
const element = document.createElement('input');
|
|
3535
|
+
element.type = 'text';
|
|
3536
|
+
|
|
3537
|
+
// Use symbols in assignGingerly
|
|
3538
|
+
element.assignGingerly({
|
|
3539
|
+
[value]: 'hello world',
|
|
3540
|
+
style: {
|
|
3541
|
+
color: 'blue'
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
|
|
3545
|
+
console.log(element.value); // 'hello world'
|
|
3546
|
+
console.log(element.style.color); // 'blue'
|
|
3547
|
+
```
|
|
3548
|
+
|
|
3549
|
+
### Itemprop Support
|
|
3550
|
+
|
|
3551
|
+
Elements with an `itemprop` attribute use that attribute's value as the property name:
|
|
3552
|
+
|
|
3553
|
+
```html
|
|
3554
|
+
<span itemprop="title"></span>
|
|
3555
|
+
```
|
|
3556
|
+
|
|
3557
|
+
```TypeScript
|
|
3558
|
+
const span = document.querySelector('[itemprop="title"]');
|
|
3559
|
+
span.set[value] = 'My Title';
|
|
3560
|
+
console.log(span.title); // 'My Title'
|
|
3561
|
+
```
|
|
3562
|
+
|
|
3563
|
+
### Implementation Details
|
|
3564
|
+
|
|
3565
|
+
The Infer enhancement is implemented as a standard enhancement class:
|
|
3566
|
+
|
|
3567
|
+
```TypeScript
|
|
3568
|
+
class Infer<TValue = any, TDisplay = any> {
|
|
3569
|
+
#weakRef: WeakRef<Element>;
|
|
3570
|
+
|
|
3571
|
+
constructor(enhancedElement?: Element) {
|
|
3572
|
+
this.#weakRef = new WeakRef(enhancedElement!);
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
get value(): TValue | undefined { /* ... */ }
|
|
3576
|
+
set value(nv: TValue) {
|
|
3577
|
+
const element = this.#weakRef.deref()!;
|
|
3578
|
+
element[inferValueProperty(element)] = nv;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
get display(): TDisplay | undefined { /* ... */ }
|
|
3582
|
+
set display(nv: TDisplay) {
|
|
3583
|
+
const element = this.#weakRef.deref()!;
|
|
3584
|
+
element[inferDisplayProperty(element)] = nv;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
```
|
|
3588
|
+
|
|
3589
|
+
**Registry Configuration:**
|
|
3590
|
+
|
|
3591
|
+
```TypeScript
|
|
3592
|
+
export const registryItem: EnhancementConfig = {
|
|
3593
|
+
spawn: Infer,
|
|
3594
|
+
enhKey: 'infer',
|
|
3595
|
+
symlinks: {
|
|
3596
|
+
[value]: 'value',
|
|
3597
|
+
[display]: 'display'
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
```
|
|
3601
|
+
|
|
3602
|
+
The `symlinks` mapping connects the symbols to the enhancement's properties, enabling the `element.set[symbol]` syntax.
|
|
3603
|
+
|
|
3604
|
+
### Helper Functions
|
|
3605
|
+
|
|
3606
|
+
The Infer module exports helper functions for manual property and event type inference:
|
|
3607
|
+
|
|
3608
|
+
```TypeScript
|
|
3609
|
+
import { inferValueProperty, inferDisplayProperty, inferEventType } from 'assign-gingerly/Infer.js';
|
|
3610
|
+
|
|
3611
|
+
const input = document.createElement('input');
|
|
3612
|
+
input.type = 'checkbox';
|
|
3613
|
+
|
|
3614
|
+
const valueProp = inferValueProperty(input);
|
|
3615
|
+
console.log(valueProp); // 'checked'
|
|
3616
|
+
|
|
3617
|
+
const displayProp = inferDisplayProperty(input);
|
|
3618
|
+
console.log(displayProp); // 'value'
|
|
3619
|
+
|
|
3620
|
+
const eventType = inferEventType(input);
|
|
3621
|
+
console.log(eventType); // 'input'
|
|
3622
|
+
```
|
|
3623
|
+
|
|
3624
|
+
These functions can be useful when you need to determine the property or event type name without actually setting a value or attaching a listener.
|
|
3625
|
+
|
|
3626
|
+
**Event Type Inference:**
|
|
3627
|
+
|
|
3628
|
+
The `inferEventType` function returns the most appropriate event type for different element types:
|
|
3629
|
+
|
|
3630
|
+
| Element Type | Event Type | Use Case |
|
|
3631
|
+
|-------------|-----------|----------|
|
|
3632
|
+
| `<input>`, `<textarea>`, `<select>` | `input` | Form control value changes |
|
|
3633
|
+
| `<form>` | `submit` | Form submission |
|
|
3634
|
+
| `<details>` | `toggle` | Details element open/close |
|
|
3635
|
+
| `<dialog>` | `close` | Dialog dismissal |
|
|
3636
|
+
| Other elements | `click` | Default interactive event |
|
|
3637
|
+
|
|
3638
|
+
**Accessing via Enhancement Instance:**
|
|
3639
|
+
|
|
3640
|
+
The inferred event type is also available as a getter on the enhancement instance:
|
|
3641
|
+
|
|
3642
|
+
```TypeScript
|
|
3643
|
+
const input = document.createElement('input');
|
|
3644
|
+
input.set[value] = 'test';
|
|
3645
|
+
|
|
3646
|
+
console.log(input.enh.infer.eventType); // 'input'
|
|
3647
|
+
|
|
3648
|
+
const form = document.createElement('form');
|
|
3649
|
+
form.set[value] = 'test';
|
|
3650
|
+
|
|
3651
|
+
console.log(form.enh.infer.eventType); // 'submit'
|
|
3652
|
+
```
|
|
3653
|
+
|
|
3654
|
+
This is particularly useful when building enhancements that need to attach event listeners but don't know the element type in advance.
|
|
3655
|
+
|
|
3656
|
+
### Benefits
|
|
3657
|
+
|
|
3658
|
+
1. **Type-agnostic code**: Write code that works with any element type without conditionals
|
|
3659
|
+
2. **Cleaner syntax**: No need to remember which property each element type uses
|
|
3660
|
+
3. **Accessibility**: Separate value and display properties support screen readers
|
|
3661
|
+
4. **Framework-friendly**: Symbols work well with reactive frameworks and data binding
|
|
3662
|
+
5. **Extensible**: Based on the enhancement registry system, can be customized or extended
|
|
3663
|
+
|
|
3664
|
+
### Complete Example
|
|
3665
|
+
|
|
3666
|
+
```html
|
|
3667
|
+
<!DOCTYPE html>
|
|
3668
|
+
<html>
|
|
3669
|
+
<head>
|
|
3670
|
+
<script type="module">
|
|
3671
|
+
import { value, display, registryItem } from './Infer.js';
|
|
3672
|
+
import './object-extension.js';
|
|
3673
|
+
|
|
3674
|
+
// Register the enhancement
|
|
3675
|
+
customElements.enhancementRegistry.push(registryItem);
|
|
3676
|
+
|
|
3677
|
+
// Create various elements
|
|
3678
|
+
const input = document.createElement('input');
|
|
3679
|
+
input.type = 'text';
|
|
3680
|
+
|
|
3681
|
+
const checkbox = document.createElement('input');
|
|
3682
|
+
checkbox.type = 'checkbox';
|
|
3683
|
+
|
|
3684
|
+
const time = document.createElement('time');
|
|
3685
|
+
|
|
3686
|
+
const meter = document.createElement('meter');
|
|
3687
|
+
meter.min = 0;
|
|
3688
|
+
meter.max = 100;
|
|
3689
|
+
|
|
3690
|
+
// Set values using the same symbol - each element handles it correctly
|
|
3691
|
+
input.set[value] = 'Hello World';
|
|
3692
|
+
checkbox.set[value] = true;
|
|
3693
|
+
time.set[value] = '2024-01-01T00:00:00Z';
|
|
3694
|
+
time.set[display] = 'January 1, 2024';
|
|
3695
|
+
meter.set[value] = 75;
|
|
3696
|
+
meter.set[display] = '75 percent';
|
|
3697
|
+
|
|
3698
|
+
// Add to document
|
|
3699
|
+
document.body.append(input, checkbox, time, meter);
|
|
3700
|
+
|
|
3701
|
+
console.log('Input value:', input.value); // 'Hello World'
|
|
3702
|
+
console.log('Checkbox checked:', checkbox.checked); // true
|
|
3703
|
+
console.log('Time dateTime:', time.dateTime); // '2024-01-01T00:00:00Z'
|
|
3704
|
+
console.log('Time display:', time.textContent); // 'January 1, 2024'
|
|
3705
|
+
console.log('Meter value:', meter.value); // 75
|
|
3706
|
+
console.log('Meter display:', meter.ariaValueText); // '75 percent'
|
|
3707
|
+
</script>
|
|
3708
|
+
</head>
|
|
3709
|
+
<body>
|
|
3710
|
+
<h1>Infer Enhancement Demo</h1>
|
|
3711
|
+
</body>
|
|
3712
|
+
</html>
|
|
3713
|
+
```
|
|
3714
|
+
|
|
3715
|
+
### Browser Support
|
|
3716
|
+
|
|
3717
|
+
The Infer enhancement requires:
|
|
3718
|
+
- Chrome 146+ (for scoped custom element registries)
|
|
3719
|
+
- Modern browsers with Symbol support
|
|
3720
|
+
- WeakRef support (all modern browsers)
|
|
3721
|
+
|
|
3722
|
+
For browsers without scoped registry support, the enhancement falls back to the global `customElements.enhancementRegistry`.
|
package/getHost.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the itemscope host element for a given element.
|
|
3
|
+
* This function finds the closest element with an itemscope attribute and waits for it to be ready.
|
|
4
|
+
*
|
|
5
|
+
* @param el - The element to start searching from
|
|
6
|
+
* @returns The itemscope host element, or null if none found
|
|
7
|
+
*/
|
|
8
|
+
export async function getHost(el) {
|
|
9
|
+
const itemScopeHost = el.closest('[itemscope]');
|
|
10
|
+
if (itemScopeHost) {
|
|
11
|
+
const { localName } = itemScopeHost;
|
|
12
|
+
// If it's a custom element, wait for it to be defined
|
|
13
|
+
if (localName.includes('-')) {
|
|
14
|
+
const registry = itemScopeHost.customElementRegistry ?? customElements;
|
|
15
|
+
await registry.whenDefined(localName);
|
|
16
|
+
return itemScopeHost;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Check if itemscope specifies a value (manager name)
|
|
20
|
+
const itemscopeValue = itemScopeHost.getAttribute('itemscope');
|
|
21
|
+
if (itemscopeValue && itemscopeValue.length > 0) {
|
|
22
|
+
// Wait for the manager to be defined in the itemscope registry
|
|
23
|
+
const registry = itemScopeHost.customElementRegistry?.itemscopeRegistry
|
|
24
|
+
?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
|
|
25
|
+
if (registry) {
|
|
26
|
+
await registry.whenDefined(itemscopeValue);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return itemScopeHost;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// No itemscope host found in the light DOM
|
|
34
|
+
// Check if we're inside a shadow root and get the shadow host
|
|
35
|
+
const rootNode = el.getRootNode();
|
|
36
|
+
// Check if it's a shadow root (has a host property)
|
|
37
|
+
if (rootNode && 'host' in rootNode && rootNode.host) {
|
|
38
|
+
const host = rootNode.host;
|
|
39
|
+
const { localName } = host;
|
|
40
|
+
// If the host is a custom element, wait for it to be defined
|
|
41
|
+
if (localName.includes('-')) {
|
|
42
|
+
const registry = host.customElementRegistry ?? customElements;
|
|
43
|
+
await registry.whenDefined(localName);
|
|
44
|
+
}
|
|
45
|
+
return host;
|
|
46
|
+
}
|
|
47
|
+
// Not in a shadow root either
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export default getHost;
|
package/getHost.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the itemscope host element for a given element.
|
|
3
|
+
* This function finds the closest element with an itemscope attribute and waits for it to be ready.
|
|
4
|
+
*
|
|
5
|
+
* @param el - The element to start searching from
|
|
6
|
+
* @returns The itemscope host element, or null if none found
|
|
7
|
+
*/
|
|
8
|
+
export async function getHost(el: Element): Promise<Element | null> {
|
|
9
|
+
const itemScopeHost = el.closest('[itemscope]');
|
|
10
|
+
if (itemScopeHost) {
|
|
11
|
+
const { localName } = itemScopeHost;
|
|
12
|
+
|
|
13
|
+
// If it's a custom element, wait for it to be defined
|
|
14
|
+
if (localName.includes('-')) {
|
|
15
|
+
const registry = (itemScopeHost as any).customElementRegistry ?? customElements;
|
|
16
|
+
await registry.whenDefined(localName);
|
|
17
|
+
return itemScopeHost;
|
|
18
|
+
} else {
|
|
19
|
+
// Check if itemscope specifies a value (manager name)
|
|
20
|
+
const itemscopeValue = itemScopeHost.getAttribute('itemscope');
|
|
21
|
+
|
|
22
|
+
if (itemscopeValue && itemscopeValue.length > 0) {
|
|
23
|
+
// Wait for the manager to be defined in the itemscope registry
|
|
24
|
+
const registry = (itemScopeHost as any).customElementRegistry?.itemscopeRegistry
|
|
25
|
+
?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
|
|
26
|
+
|
|
27
|
+
if (registry) {
|
|
28
|
+
await registry.whenDefined(itemscopeValue);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return itemScopeHost;
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// No itemscope host found in the light DOM
|
|
36
|
+
// Check if we're inside a shadow root and get the shadow host
|
|
37
|
+
const rootNode = el.getRootNode();
|
|
38
|
+
|
|
39
|
+
// Check if it's a shadow root (has a host property)
|
|
40
|
+
if (rootNode && 'host' in rootNode && (rootNode as ShadowRoot).host) {
|
|
41
|
+
const host = (rootNode as ShadowRoot).host;
|
|
42
|
+
const { localName } = host;
|
|
43
|
+
|
|
44
|
+
// If the host is a custom element, wait for it to be defined
|
|
45
|
+
if (localName.includes('-')) {
|
|
46
|
+
const registry = (host as any).customElementRegistry ?? customElements;
|
|
47
|
+
await registry.whenDefined(localName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return host;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Not in a shadow root either
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default getHost;
|
package/index.js
CHANGED
|
@@ -6,4 +6,5 @@ export { ParserRegistry, globalParserRegistry } from './parserRegistry.js';
|
|
|
6
6
|
export { parseWithAttrs } from './parseWithAttrs.js';
|
|
7
7
|
export { buildCSSQuery } from './buildCSSQuery.js';
|
|
8
8
|
export { resolveTemplate } from './resolveTemplate.js';
|
|
9
|
+
export { getHost } from './getHost.js';
|
|
9
10
|
import './object-extension.js';
|
package/index.ts
CHANGED
|
@@ -6,4 +6,5 @@ export {ParserRegistry, globalParserRegistry} from './parserRegistry.js';
|
|
|
6
6
|
export {parseWithAttrs} from './parseWithAttrs.js';
|
|
7
7
|
export {buildCSSQuery} from './buildCSSQuery.js';
|
|
8
8
|
export {resolveTemplate} from './resolveTemplate.js';
|
|
9
|
+
export {getHost} from './getHost.js';
|
|
9
10
|
import './object-extension.js';
|
package/object-extension.js
CHANGED
|
@@ -284,6 +284,74 @@ if (typeof Element !== 'undefined') {
|
|
|
284
284
|
enumerable: true,
|
|
285
285
|
configurable: true,
|
|
286
286
|
});
|
|
287
|
+
/**
|
|
288
|
+
* Adds 'set' property to Element prototype for symbol-based dependency injection
|
|
289
|
+
* Returns a proxy that intercepts symbol property assignments
|
|
290
|
+
*/
|
|
291
|
+
Object.defineProperty(Element.prototype, 'set', {
|
|
292
|
+
get: function () {
|
|
293
|
+
const element = this;
|
|
294
|
+
return new Proxy({}, {
|
|
295
|
+
set: (_, prop, value) => {
|
|
296
|
+
if (typeof prop === 'symbol') {
|
|
297
|
+
// Get the registry from customElementRegistry (scoped or global)
|
|
298
|
+
const registry = element.customElementRegistry?.enhancementRegistry
|
|
299
|
+
?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
|
|
300
|
+
if (registry) {
|
|
301
|
+
const registryItem = registry.findBySymbol(prop);
|
|
302
|
+
if (registryItem) {
|
|
303
|
+
const instanceMap = getInstanceMap();
|
|
304
|
+
const instances = instanceMap.getOrInsertComputed(element, () => new Map());
|
|
305
|
+
let instance = instances.get(registryItem);
|
|
306
|
+
if (!instance) {
|
|
307
|
+
const SpawnClass = registryItem.spawn;
|
|
308
|
+
// Check canSpawn if it exists
|
|
309
|
+
if (typeof SpawnClass.canSpawn === 'function') {
|
|
310
|
+
const ctx = { config: registryItem };
|
|
311
|
+
if (!SpawnClass.canSpawn(element, ctx)) {
|
|
312
|
+
// canSpawn returned false, skip spawning
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// If target is an Element and registryItem has enhKey, pass element to constructor
|
|
317
|
+
if (registryItem.enhKey) {
|
|
318
|
+
const ctx = { config: registryItem };
|
|
319
|
+
const initVals = element.enh?.[registryItem.enhKey] &&
|
|
320
|
+
!(element.enh[registryItem.enhKey] instanceof SpawnClass)
|
|
321
|
+
? element.enh[registryItem.enhKey]
|
|
322
|
+
: undefined;
|
|
323
|
+
instance = new SpawnClass(element, ctx, initVals);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const ctx = { config: registryItem };
|
|
327
|
+
instance = new SpawnClass(element, ctx);
|
|
328
|
+
}
|
|
329
|
+
instances.set(registryItem, instance);
|
|
330
|
+
// If registryItem has enhKey, store on enh
|
|
331
|
+
if (registryItem.enhKey) {
|
|
332
|
+
if (!element.enh) {
|
|
333
|
+
// This shouldn't happen since enh is a getter, but be safe
|
|
334
|
+
element.enh = {};
|
|
335
|
+
}
|
|
336
|
+
element.enh[registryItem.enhKey] = instance;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (registryItem.symlinks) {
|
|
340
|
+
const mappedKey = registryItem.symlinks[prop];
|
|
341
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
342
|
+
instance[mappedKey] = value;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
enumerable: false,
|
|
353
|
+
configurable: true,
|
|
354
|
+
});
|
|
287
355
|
}
|
|
288
356
|
/**
|
|
289
357
|
* Adds assignGingerly method to all objects via the Object prototype
|
package/object-extension.ts
CHANGED
|
@@ -404,6 +404,84 @@ if (typeof Element !== 'undefined') {
|
|
|
404
404
|
enumerable: true,
|
|
405
405
|
configurable: true,
|
|
406
406
|
});
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Adds 'set' property to Element prototype for symbol-based dependency injection
|
|
410
|
+
* Returns a proxy that intercepts symbol property assignments
|
|
411
|
+
*/
|
|
412
|
+
Object.defineProperty(Element.prototype, 'set', {
|
|
413
|
+
get: function (this: Element) {
|
|
414
|
+
const element = this;
|
|
415
|
+
return new Proxy(
|
|
416
|
+
{},
|
|
417
|
+
{
|
|
418
|
+
set: (_: any, prop: string | symbol, value: any) => {
|
|
419
|
+
if (typeof prop === 'symbol') {
|
|
420
|
+
// Get the registry from customElementRegistry (scoped or global)
|
|
421
|
+
const registry = (element as any).customElementRegistry?.enhancementRegistry
|
|
422
|
+
?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
|
|
423
|
+
|
|
424
|
+
if (registry) {
|
|
425
|
+
const registryItem = registry.findBySymbol(prop);
|
|
426
|
+
if (registryItem) {
|
|
427
|
+
const instanceMap = getInstanceMap();
|
|
428
|
+
const instances = instanceMap.getOrInsertComputed(element, () => new Map());
|
|
429
|
+
let instance = instances.get(registryItem);
|
|
430
|
+
|
|
431
|
+
if (!instance) {
|
|
432
|
+
const SpawnClass = registryItem.spawn;
|
|
433
|
+
|
|
434
|
+
// Check canSpawn if it exists
|
|
435
|
+
if (typeof SpawnClass.canSpawn === 'function') {
|
|
436
|
+
const ctx = { config: registryItem };
|
|
437
|
+
if (!SpawnClass.canSpawn(element, ctx)) {
|
|
438
|
+
// canSpawn returned false, skip spawning
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// If target is an Element and registryItem has enhKey, pass element to constructor
|
|
444
|
+
if (registryItem.enhKey) {
|
|
445
|
+
const ctx = { config: registryItem };
|
|
446
|
+
const initVals = (element as any).enh?.[registryItem.enhKey] &&
|
|
447
|
+
!((element as any).enh[registryItem.enhKey] instanceof SpawnClass)
|
|
448
|
+
? (element as any).enh[registryItem.enhKey]
|
|
449
|
+
: undefined;
|
|
450
|
+
instance = new SpawnClass(element, ctx, initVals);
|
|
451
|
+
} else {
|
|
452
|
+
const ctx = { config: registryItem };
|
|
453
|
+
instance = new SpawnClass(element, ctx);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
instances.set(registryItem, instance);
|
|
457
|
+
|
|
458
|
+
// If registryItem has enhKey, store on enh
|
|
459
|
+
if (registryItem.enhKey) {
|
|
460
|
+
if (!(element as any).enh) {
|
|
461
|
+
// This shouldn't happen since enh is a getter, but be safe
|
|
462
|
+
(element as any).enh = {};
|
|
463
|
+
}
|
|
464
|
+
(element as any).enh[registryItem.enhKey] = instance;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if(registryItem.symlinks){
|
|
469
|
+
const mappedKey = registryItem.symlinks[prop];
|
|
470
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
471
|
+
(instance as any)[mappedKey] = value;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return true;
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
},
|
|
482
|
+
enumerable: false,
|
|
483
|
+
configurable: true,
|
|
484
|
+
});
|
|
407
485
|
}
|
|
408
486
|
|
|
409
487
|
/**
|
package/package.json
CHANGED
|
@@ -1,71 +1,79 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "This package provides a utility function for carefully merging one object into another.",
|
|
5
|
-
"homepage": "https://github.com/bahrus/assign-gingerly#readme",
|
|
6
|
-
"bugs": {
|
|
7
|
-
"url": "https://github.com/bahrus/assign-gingerly/issues"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/bahrus/assign-gingerly.git"
|
|
12
|
-
},
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
|
|
15
|
-
"type": "module",
|
|
16
|
-
"types": "types/assign-gingerly/types.d.ts",
|
|
17
|
-
"files": [
|
|
18
|
-
"*.js",
|
|
19
|
-
"*.ts",
|
|
20
|
-
"README.md",
|
|
21
|
-
"LICENSE",
|
|
22
|
-
"types/assign-gingerly/types.d.ts"
|
|
23
|
-
],
|
|
24
|
-
"exports": {
|
|
25
|
-
".": {
|
|
26
|
-
"default": "./index.js",
|
|
27
|
-
"types": "./index.ts"
|
|
28
|
-
},
|
|
29
|
-
"./assignGingerly.js": {
|
|
30
|
-
"default": "./assignGingerly.js",
|
|
31
|
-
"types": "./assignGingerly.ts"
|
|
32
|
-
},
|
|
33
|
-
"./assignTentatively.js": {
|
|
34
|
-
"default": "./assignTentatively.js",
|
|
35
|
-
"types": "./assignTentatively.ts"
|
|
36
|
-
},
|
|
37
|
-
"./waitForEvent.js": {
|
|
38
|
-
"default": "./waitForEvent.js"
|
|
39
|
-
},
|
|
40
|
-
"./parserRegistry.js": {
|
|
41
|
-
"default": "./parserRegistry.js"
|
|
42
|
-
},
|
|
43
|
-
"./parseWithAttrs.js": {
|
|
44
|
-
"default": "./parseWithAttrs.js",
|
|
45
|
-
"types": "./parseWithAttrs.ts"
|
|
46
|
-
},
|
|
47
|
-
"./buildCSSQuery.js": {
|
|
48
|
-
"default": "./buildCSSQuery.js",
|
|
49
|
-
"types": "./buildCSSQuery.ts"
|
|
50
|
-
},
|
|
51
|
-
"./resolveTemplate.js": {
|
|
52
|
-
"default": "./resolveTemplate.js",
|
|
53
|
-
"types": "./resolveTemplate.ts"
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "assign-gingerly",
|
|
3
|
+
"version": "0.0.30",
|
|
4
|
+
"description": "This package provides a utility function for carefully merging one object into another.",
|
|
5
|
+
"homepage": "https://github.com/bahrus/assign-gingerly#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/bahrus/assign-gingerly/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/bahrus/assign-gingerly.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"types": "types/assign-gingerly/types.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"*.js",
|
|
19
|
+
"*.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"types/assign-gingerly/types.d.ts"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"default": "./index.js",
|
|
27
|
+
"types": "./index.ts"
|
|
28
|
+
},
|
|
29
|
+
"./assignGingerly.js": {
|
|
30
|
+
"default": "./assignGingerly.js",
|
|
31
|
+
"types": "./assignGingerly.ts"
|
|
32
|
+
},
|
|
33
|
+
"./assignTentatively.js": {
|
|
34
|
+
"default": "./assignTentatively.js",
|
|
35
|
+
"types": "./assignTentatively.ts"
|
|
36
|
+
},
|
|
37
|
+
"./waitForEvent.js": {
|
|
38
|
+
"default": "./waitForEvent.js"
|
|
39
|
+
},
|
|
40
|
+
"./parserRegistry.js": {
|
|
41
|
+
"default": "./parserRegistry.js"
|
|
42
|
+
},
|
|
43
|
+
"./parseWithAttrs.js": {
|
|
44
|
+
"default": "./parseWithAttrs.js",
|
|
45
|
+
"types": "./parseWithAttrs.ts"
|
|
46
|
+
},
|
|
47
|
+
"./buildCSSQuery.js": {
|
|
48
|
+
"default": "./buildCSSQuery.js",
|
|
49
|
+
"types": "./buildCSSQuery.ts"
|
|
50
|
+
},
|
|
51
|
+
"./resolveTemplate.js": {
|
|
52
|
+
"default": "./resolveTemplate.js",
|
|
53
|
+
"types": "./resolveTemplate.ts"
|
|
54
|
+
},
|
|
55
|
+
"./getHost.js": {
|
|
56
|
+
"default": "./getHost.js",
|
|
57
|
+
"types": "./getHost.ts"
|
|
58
|
+
},
|
|
59
|
+
"./Infer.js": {
|
|
60
|
+
"default": "./Infer.js",
|
|
61
|
+
"types": "./Infer.ts"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"main": "index.js",
|
|
65
|
+
"module": "index.js",
|
|
66
|
+
"scripts": {
|
|
67
|
+
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
68
|
+
"test": "playwright test",
|
|
69
|
+
"update": "ncu -u && npm install",
|
|
70
|
+
"safari": "npx playwright wk http://localhost:8000",
|
|
71
|
+
"chrome": "npx playwright cr http://localhost:8000"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@playwright/test": "1.59.1",
|
|
75
|
+
"spa-ssi": "0.0.27",
|
|
76
|
+
"@types/node": "25.5.2",
|
|
77
|
+
"typescript": "6.0.2"
|
|
78
|
+
}
|
|
79
|
+
}
|
package/parseWithAttrs.js
CHANGED
|
@@ -147,7 +147,8 @@ function hasDashOrNonASCII(str) {
|
|
|
147
147
|
* @returns The attribute value or null
|
|
148
148
|
*/
|
|
149
149
|
function getAttributeValue(element, attrName, allowUnprefixed) {
|
|
150
|
-
const
|
|
150
|
+
const { localName } = element;
|
|
151
|
+
const isCustomElement = localName.includes('-');
|
|
151
152
|
const isSVGElement = typeof SVGElement !== 'undefined' && element instanceof SVGElement;
|
|
152
153
|
// For custom elements and SVG - strict enh- requirement
|
|
153
154
|
if (isCustomElement || isSVGElement) {
|
|
@@ -157,8 +158,7 @@ function getAttributeValue(element, attrName, allowUnprefixed) {
|
|
|
157
158
|
// Only fallback if tag name matches the allowUnprefixed pattern
|
|
158
159
|
if (allowUnprefixed) {
|
|
159
160
|
const pattern = typeof allowUnprefixed === 'string' ? new RegExp(allowUnprefixed) : allowUnprefixed;
|
|
160
|
-
|
|
161
|
-
if (pattern.test(tagName)) {
|
|
161
|
+
if (pattern.test(localName)) {
|
|
162
162
|
return element.getAttribute(attrName);
|
|
163
163
|
}
|
|
164
164
|
}
|
package/parseWithAttrs.ts
CHANGED
|
@@ -186,7 +186,8 @@ function getAttributeValue(
|
|
|
186
186
|
attrName: string,
|
|
187
187
|
allowUnprefixed?: string | RegExp
|
|
188
188
|
): string | null {
|
|
189
|
-
const
|
|
189
|
+
const { localName } = element;
|
|
190
|
+
const isCustomElement = localName.includes('-');
|
|
190
191
|
const isSVGElement = typeof SVGElement !== 'undefined' && element instanceof SVGElement;
|
|
191
192
|
|
|
192
193
|
// For custom elements and SVG - strict enh- requirement
|
|
@@ -197,8 +198,7 @@ function getAttributeValue(
|
|
|
197
198
|
// Only fallback if tag name matches the allowUnprefixed pattern
|
|
198
199
|
if (allowUnprefixed) {
|
|
199
200
|
const pattern = typeof allowUnprefixed === 'string' ? new RegExp(allowUnprefixed) : allowUnprefixed;
|
|
200
|
-
|
|
201
|
-
if (pattern.test(tagName)) {
|
|
201
|
+
if (pattern.test(localName)) {
|
|
202
202
|
return element.getAttribute(attrName);
|
|
203
203
|
}
|
|
204
204
|
}
|
|
@@ -112,7 +112,7 @@ export type ParserFunction<T = any> =
|
|
|
112
112
|
| ((attrValue: string | null) => any)
|
|
113
113
|
| ((attrValue: string | null, context?: ParserContext<T>) => any);
|
|
114
114
|
|
|
115
|
-
export interface AttrConfig<T =
|
|
115
|
+
export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
116
116
|
/**
|
|
117
117
|
* Type of the property value (JSON-serializable string format)
|
|
118
118
|
*/
|
|
@@ -152,6 +152,12 @@ export interface AttrConfig<T = any> {
|
|
|
152
152
|
| ParserFunction<T>
|
|
153
153
|
| string
|
|
154
154
|
;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* configuration information needed by a custom parser to properly
|
|
158
|
+
* parse the attribute.
|
|
159
|
+
*/
|
|
160
|
+
parserConfig?: TParserConfig;
|
|
155
161
|
|
|
156
162
|
/**
|
|
157
163
|
* Default value to use when attribute is missing
|