assign-gingerly 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +713 -40
- package/assignGingerly.js +193 -9
- package/assignGingerly.ts +237 -9
- package/handleIshProperty.js +92 -0
- package/handleIshProperty.ts +115 -0
- package/index.js +1 -1
- package/index.ts +1 -1
- package/object-extension.js +20 -1
- package/object-extension.ts +23 -2
- package/package.json +3 -3
- package/parseWithAttrs.js +29 -23
- package/parseWithAttrs.ts +41 -24
- package/playwright.config.ts +9 -9
- package/types/assign-gingerly/types.d.ts +80 -23
- package/types/global.d.ts +4 -0
- package/waitForEvent.js +15 -1
- package/waitForEvent.ts +19 -1
package/assignGingerly.js
CHANGED
|
@@ -16,7 +16,22 @@ export function getInstanceMap() {
|
|
|
16
16
|
/**
|
|
17
17
|
* Base registry class for managing enhancement configurations
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Event dispatched when enhancement configs are registered
|
|
21
|
+
*/
|
|
22
|
+
export class EnhancementRegisteredEvent extends Event {
|
|
23
|
+
config;
|
|
24
|
+
static eventName = 'register';
|
|
25
|
+
constructor(config) {
|
|
26
|
+
super(EnhancementRegisteredEvent.eventName);
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Registry for enhancement configurations
|
|
32
|
+
* Extends EventTarget to dispatch events when configs are registered
|
|
33
|
+
*/
|
|
34
|
+
export class EnhancementRegistry extends EventTarget {
|
|
20
35
|
#items = new Set();
|
|
21
36
|
push(items) {
|
|
22
37
|
if (Array.isArray(items)) {
|
|
@@ -25,6 +40,8 @@ export class EnhancementRegistry {
|
|
|
25
40
|
else {
|
|
26
41
|
this.#items.add(items);
|
|
27
42
|
}
|
|
43
|
+
// Dispatch event after adding items
|
|
44
|
+
this.dispatchEvent(new EnhancementRegisteredEvent(items));
|
|
28
45
|
}
|
|
29
46
|
getItems() {
|
|
30
47
|
return Array.from(this.#items);
|
|
@@ -53,6 +70,74 @@ export class EnhancementRegistry {
|
|
|
53
70
|
return undefined;
|
|
54
71
|
}
|
|
55
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Registry for ItemScope Manager configurations
|
|
75
|
+
* Extends EventTarget to support lazy registration via events
|
|
76
|
+
*/
|
|
77
|
+
export class ItemscopeRegistry extends EventTarget {
|
|
78
|
+
#configs = new Map();
|
|
79
|
+
#pendingSetups = new Map();
|
|
80
|
+
/**
|
|
81
|
+
* Define a new manager configuration
|
|
82
|
+
* @param name - Manager name (matches itemscope attribute value)
|
|
83
|
+
* @param config - Manager configuration object
|
|
84
|
+
* @throws Error if name is already registered
|
|
85
|
+
*/
|
|
86
|
+
define(name, config) {
|
|
87
|
+
if (this.#configs.has(name)) {
|
|
88
|
+
throw new Error('Already registered');
|
|
89
|
+
}
|
|
90
|
+
this.#configs.set(name, config);
|
|
91
|
+
this.dispatchEvent(new Event(name));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get a manager configuration by name
|
|
95
|
+
* @param name - Manager name
|
|
96
|
+
* @returns Manager configuration or undefined
|
|
97
|
+
*/
|
|
98
|
+
get(name) {
|
|
99
|
+
return this.#configs.get(name);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Wait for a manager to be defined and all pending setups to complete
|
|
103
|
+
* @param name - Manager name to wait for
|
|
104
|
+
* @returns Promise that resolves when manager is defined and all setups are complete
|
|
105
|
+
*/
|
|
106
|
+
async whenDefined(name) {
|
|
107
|
+
// If not yet defined, wait for definition
|
|
108
|
+
if (!this.#configs.has(name)) {
|
|
109
|
+
await new Promise((resolve) => {
|
|
110
|
+
this.addEventListener(name, () => resolve(), { once: true });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Wait for all pending setups for this manager
|
|
114
|
+
const pending = this.#pendingSetups.get(name);
|
|
115
|
+
if (pending && pending.length > 0) {
|
|
116
|
+
await Promise.all(pending);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Internal method to track a pending setup
|
|
121
|
+
* @param name - Manager name
|
|
122
|
+
* @param promise - Promise representing the setup operation
|
|
123
|
+
*/
|
|
124
|
+
_trackSetup(name, promise) {
|
|
125
|
+
if (!this.#pendingSetups.has(name)) {
|
|
126
|
+
this.#pendingSetups.set(name, []);
|
|
127
|
+
}
|
|
128
|
+
this.#pendingSetups.get(name).push(promise);
|
|
129
|
+
// Clean up after completion
|
|
130
|
+
promise.finally(() => {
|
|
131
|
+
const pending = this.#pendingSetups.get(name);
|
|
132
|
+
if (pending) {
|
|
133
|
+
const index = pending.indexOf(promise);
|
|
134
|
+
if (index > -1) {
|
|
135
|
+
pending.splice(index, 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
56
141
|
/**
|
|
57
142
|
* Helper function to check if a string key represents a Symbol.for expression
|
|
58
143
|
*/
|
|
@@ -142,6 +227,49 @@ function ensureNestedPath(obj, pathParts) {
|
|
|
142
227
|
}
|
|
143
228
|
return current;
|
|
144
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Helper function to check if a property is readonly
|
|
232
|
+
* A property is readonly if:
|
|
233
|
+
* - It's a data property with writable: false, OR
|
|
234
|
+
* - It's an accessor property with a getter but no setter
|
|
235
|
+
*/
|
|
236
|
+
function isReadonlyProperty(obj, propName) {
|
|
237
|
+
let descriptor = Object.getOwnPropertyDescriptor(obj, propName);
|
|
238
|
+
if (!descriptor) {
|
|
239
|
+
// Check prototype chain
|
|
240
|
+
let proto = Object.getPrototypeOf(obj);
|
|
241
|
+
while (proto) {
|
|
242
|
+
descriptor = Object.getOwnPropertyDescriptor(proto, propName);
|
|
243
|
+
if (descriptor)
|
|
244
|
+
break;
|
|
245
|
+
proto = Object.getPrototypeOf(proto);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!descriptor)
|
|
249
|
+
return false;
|
|
250
|
+
// If it's a data property, check writable flag
|
|
251
|
+
if ('value' in descriptor) {
|
|
252
|
+
return descriptor.writable === false;
|
|
253
|
+
}
|
|
254
|
+
// If it's an accessor property, check if it has only a getter (no setter)
|
|
255
|
+
if ('get' in descriptor) {
|
|
256
|
+
return descriptor.set === undefined;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Helper function to check if a value is a class instance (not a plain object)
|
|
262
|
+
* Returns true for instances of classes, false for plain objects, arrays, and primitives
|
|
263
|
+
*/
|
|
264
|
+
function isClassInstance(value) {
|
|
265
|
+
if (!value || typeof value !== 'object')
|
|
266
|
+
return false;
|
|
267
|
+
if (Array.isArray(value))
|
|
268
|
+
return false;
|
|
269
|
+
const proto = Object.getPrototypeOf(value);
|
|
270
|
+
// Plain objects have Object.prototype or null as prototype
|
|
271
|
+
return proto !== Object.prototype && proto !== null;
|
|
272
|
+
}
|
|
145
273
|
/**
|
|
146
274
|
* Main assignGingerly function
|
|
147
275
|
*/
|
|
@@ -175,6 +303,38 @@ export function assignGingerly(target, source, options) {
|
|
|
175
303
|
for (const sym of Object.getOwnPropertySymbols(source)) {
|
|
176
304
|
processedSource[sym] = source[sym];
|
|
177
305
|
}
|
|
306
|
+
// Process 'ish' property for HTMLElements with itemscope (async, non-blocking)
|
|
307
|
+
if ('ish' in processedSource) {
|
|
308
|
+
if (typeof HTMLElement !== 'undefined' && target instanceof HTMLElement) {
|
|
309
|
+
// Capture the value before deleting
|
|
310
|
+
const ishValue = processedSource['ish'];
|
|
311
|
+
// Remove 'ish' from processedSource to prevent normal assignment
|
|
312
|
+
delete processedSource['ish'];
|
|
313
|
+
// Get the itemscope attribute to track the setup
|
|
314
|
+
const itemscopeValue = target.getAttribute('itemscope');
|
|
315
|
+
// Load handler on demand and process asynchronously
|
|
316
|
+
const setupPromise = (async () => {
|
|
317
|
+
try {
|
|
318
|
+
const { handleIshProperty } = await import('./handleIshProperty.js');
|
|
319
|
+
await handleIshProperty(target, ishValue, options, assignGingerly);
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
console.error('Error in handleIshProperty:', err);
|
|
323
|
+
// Re-throw errors asynchronously so they're visible
|
|
324
|
+
setTimeout(() => { throw err; }, 0);
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
// Track the setup promise with the registry if we have an itemscope value
|
|
328
|
+
if (itemscopeValue && typeof itemscopeValue === 'string' && itemscopeValue.length > 0) {
|
|
329
|
+
const registry = target.customElementRegistry?.itemscopeRegistry
|
|
330
|
+
?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
|
|
331
|
+
if (registry && typeof registry._trackSetup === 'function') {
|
|
332
|
+
registry._trackSetup(itemscopeValue, setupPromise);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// For non-HTMLElement targets, 'ish' is processed as a normal property
|
|
337
|
+
}
|
|
178
338
|
// First pass: handle all non-symbol keys and sync operations
|
|
179
339
|
for (const key of Object.keys(processedSource)) {
|
|
180
340
|
const value = processedSource[key];
|
|
@@ -280,11 +440,23 @@ export function assignGingerly(target, source, options) {
|
|
|
280
440
|
const lastKey = pathParts[pathParts.length - 1];
|
|
281
441
|
const parent = ensureNestedPath(target, pathParts);
|
|
282
442
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
283
|
-
//
|
|
284
|
-
if (
|
|
285
|
-
|
|
443
|
+
// Check if property exists and is readonly OR is a class instance
|
|
444
|
+
if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
|
|
445
|
+
// Property is readonly or a class instance - check if current value is an object
|
|
446
|
+
const currentValue = parent[lastKey];
|
|
447
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
448
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
|
|
449
|
+
}
|
|
450
|
+
// Recursively apply assignGingerly to the readonly object or class instance
|
|
451
|
+
assignGingerly(currentValue, value, options);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
// Property is writable and not a class instance - normal recursive merge
|
|
455
|
+
if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
|
|
456
|
+
parent[lastKey] = {};
|
|
457
|
+
}
|
|
458
|
+
assignGingerly(parent[lastKey], value, options);
|
|
286
459
|
}
|
|
287
|
-
assignGingerly(parent[lastKey], value, options);
|
|
288
460
|
}
|
|
289
461
|
else {
|
|
290
462
|
parent[lastKey] = value;
|
|
@@ -292,11 +464,23 @@ export function assignGingerly(target, source, options) {
|
|
|
292
464
|
}
|
|
293
465
|
else {
|
|
294
466
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
295
|
-
//
|
|
296
|
-
if (
|
|
297
|
-
|
|
467
|
+
// Check if property exists and is readonly OR is a class instance
|
|
468
|
+
if (key in target && (isReadonlyProperty(target, key) || isClassInstance(target[key]))) {
|
|
469
|
+
// Property is readonly or a class instance - check if current value is an object
|
|
470
|
+
const currentValue = target[key];
|
|
471
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
472
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(target, key) ? 'readonly ' : ''}primitive property '${String(key)}'`);
|
|
473
|
+
}
|
|
474
|
+
// Recursively apply assignGingerly to the readonly object or class instance
|
|
475
|
+
assignGingerly(currentValue, value, options);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
// Property is writable and not a class instance - normal recursive merge
|
|
479
|
+
if (!(key in target) || typeof target[key] !== 'object') {
|
|
480
|
+
target[key] = {};
|
|
481
|
+
}
|
|
482
|
+
assignGingerly(target[key], value, options);
|
|
298
483
|
}
|
|
299
|
-
assignGingerly(target[key], value, options);
|
|
300
484
|
}
|
|
301
485
|
else {
|
|
302
486
|
target[key] = value;
|
package/assignGingerly.ts
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
import { EnhancementConfig } from "./types/assign-gingerly/types";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Constructor signature for ItemScope Manager classes
|
|
7
|
+
*/
|
|
8
|
+
export type ItemscopeManager<T = any> = {
|
|
9
|
+
new (element: HTMLElement, initVals?: Partial<T>): T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for ItemScope Manager registration
|
|
14
|
+
*/
|
|
15
|
+
export interface ItemscopeManagerConfig<T = any> {
|
|
16
|
+
/**
|
|
17
|
+
* Manager class constructor
|
|
18
|
+
*/
|
|
19
|
+
manager: ItemscopeManager<T>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional lifecycle method keys
|
|
23
|
+
* - dispose: Method name to call when manager is disposed
|
|
24
|
+
* - resolved: Property/event name indicating manager is ready
|
|
25
|
+
*/
|
|
26
|
+
lifecycleKeys?: {
|
|
27
|
+
dispose?: string | symbol;
|
|
28
|
+
resolved?: string | symbol;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
// Polyfill for WeakMap.prototype.getOrInsert
|
|
6
33
|
|
|
7
34
|
// if (typeof WeakMap.prototype.getOrInsertComputed !== 'function') {
|
|
@@ -23,6 +50,7 @@ import { EnhancementConfig } from "./types/assign-gingerly/types";
|
|
|
23
50
|
*/
|
|
24
51
|
export interface IAssignGingerlyOptions {
|
|
25
52
|
registry?: typeof EnhancementRegistry | EnhancementRegistry;
|
|
53
|
+
bypassChecks?: boolean;
|
|
26
54
|
}
|
|
27
55
|
|
|
28
56
|
/**
|
|
@@ -45,7 +73,24 @@ export function getInstanceMap(): WeakMap<object, Map<EnhancementConfig, any>> {
|
|
|
45
73
|
/**
|
|
46
74
|
* Base registry class for managing enhancement configurations
|
|
47
75
|
*/
|
|
48
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Event dispatched when enhancement configs are registered
|
|
78
|
+
*/
|
|
79
|
+
export class EnhancementRegisteredEvent extends Event {
|
|
80
|
+
static eventName = 'register';
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
public config: EnhancementConfig | EnhancementConfig[]
|
|
84
|
+
) {
|
|
85
|
+
super(EnhancementRegisteredEvent.eventName);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registry for enhancement configurations
|
|
91
|
+
* Extends EventTarget to dispatch events when configs are registered
|
|
92
|
+
*/
|
|
93
|
+
export class EnhancementRegistry extends EventTarget {
|
|
49
94
|
#items: Set<EnhancementConfig> = new Set();
|
|
50
95
|
|
|
51
96
|
push(items: EnhancementConfig | EnhancementConfig[]): void {
|
|
@@ -54,6 +99,9 @@ export class EnhancementRegistry {
|
|
|
54
99
|
} else {
|
|
55
100
|
this.#items.add(items);
|
|
56
101
|
}
|
|
102
|
+
|
|
103
|
+
// Dispatch event after adding items
|
|
104
|
+
this.dispatchEvent(new EnhancementRegisteredEvent(items));
|
|
57
105
|
}
|
|
58
106
|
|
|
59
107
|
getItems(): EnhancementConfig[] {
|
|
@@ -85,6 +133,81 @@ export class EnhancementRegistry {
|
|
|
85
133
|
}
|
|
86
134
|
}
|
|
87
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Registry for ItemScope Manager configurations
|
|
138
|
+
* Extends EventTarget to support lazy registration via events
|
|
139
|
+
*/
|
|
140
|
+
export class ItemscopeRegistry extends EventTarget {
|
|
141
|
+
#configs: Map<string, ItemscopeManagerConfig> = new Map();
|
|
142
|
+
#pendingSetups: Map<string, Promise<void>[]> = new Map();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Define a new manager configuration
|
|
146
|
+
* @param name - Manager name (matches itemscope attribute value)
|
|
147
|
+
* @param config - Manager configuration object
|
|
148
|
+
* @throws Error if name is already registered
|
|
149
|
+
*/
|
|
150
|
+
define(name: string, config: ItemscopeManagerConfig): void {
|
|
151
|
+
if (this.#configs.has(name)) {
|
|
152
|
+
throw new Error('Already registered');
|
|
153
|
+
}
|
|
154
|
+
this.#configs.set(name, config);
|
|
155
|
+
this.dispatchEvent(new Event(name));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a manager configuration by name
|
|
160
|
+
* @param name - Manager name
|
|
161
|
+
* @returns Manager configuration or undefined
|
|
162
|
+
*/
|
|
163
|
+
get(name: string): ItemscopeManagerConfig | undefined {
|
|
164
|
+
return this.#configs.get(name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Wait for a manager to be defined and all pending setups to complete
|
|
169
|
+
* @param name - Manager name to wait for
|
|
170
|
+
* @returns Promise that resolves when manager is defined and all setups are complete
|
|
171
|
+
*/
|
|
172
|
+
async whenDefined(name: string): Promise<void> {
|
|
173
|
+
// If not yet defined, wait for definition
|
|
174
|
+
if (!this.#configs.has(name)) {
|
|
175
|
+
await new Promise<void>((resolve) => {
|
|
176
|
+
this.addEventListener(name, () => resolve(), { once: true });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Wait for all pending setups for this manager
|
|
181
|
+
const pending = this.#pendingSetups.get(name);
|
|
182
|
+
if (pending && pending.length > 0) {
|
|
183
|
+
await Promise.all(pending);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Internal method to track a pending setup
|
|
189
|
+
* @param name - Manager name
|
|
190
|
+
* @param promise - Promise representing the setup operation
|
|
191
|
+
*/
|
|
192
|
+
_trackSetup(name: string, promise: Promise<void>): void {
|
|
193
|
+
if (!this.#pendingSetups.has(name)) {
|
|
194
|
+
this.#pendingSetups.set(name, []);
|
|
195
|
+
}
|
|
196
|
+
this.#pendingSetups.get(name)!.push(promise);
|
|
197
|
+
|
|
198
|
+
// Clean up after completion
|
|
199
|
+
promise.finally(() => {
|
|
200
|
+
const pending = this.#pendingSetups.get(name);
|
|
201
|
+
if (pending) {
|
|
202
|
+
const index = pending.indexOf(promise);
|
|
203
|
+
if (index > -1) {
|
|
204
|
+
pending.splice(index, 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
88
211
|
|
|
89
212
|
/**
|
|
90
213
|
* Helper function to check if a string key represents a Symbol.for expression
|
|
@@ -186,6 +309,53 @@ function ensureNestedPath(obj: any, pathParts: string[]): any {
|
|
|
186
309
|
return current;
|
|
187
310
|
}
|
|
188
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Helper function to check if a property is readonly
|
|
314
|
+
* A property is readonly if:
|
|
315
|
+
* - It's a data property with writable: false, OR
|
|
316
|
+
* - It's an accessor property with a getter but no setter
|
|
317
|
+
*/
|
|
318
|
+
function isReadonlyProperty(obj: any, propName: string | symbol): boolean {
|
|
319
|
+
let descriptor = Object.getOwnPropertyDescriptor(obj, propName);
|
|
320
|
+
|
|
321
|
+
if (!descriptor) {
|
|
322
|
+
// Check prototype chain
|
|
323
|
+
let proto = Object.getPrototypeOf(obj);
|
|
324
|
+
while (proto) {
|
|
325
|
+
descriptor = Object.getOwnPropertyDescriptor(proto, propName);
|
|
326
|
+
if (descriptor) break;
|
|
327
|
+
proto = Object.getPrototypeOf(proto);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!descriptor) return false;
|
|
332
|
+
|
|
333
|
+
// If it's a data property, check writable flag
|
|
334
|
+
if ('value' in descriptor) {
|
|
335
|
+
return descriptor.writable === false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// If it's an accessor property, check if it has only a getter (no setter)
|
|
339
|
+
if ('get' in descriptor) {
|
|
340
|
+
return descriptor.set === undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Helper function to check if a value is a class instance (not a plain object)
|
|
348
|
+
* Returns true for instances of classes, false for plain objects, arrays, and primitives
|
|
349
|
+
*/
|
|
350
|
+
function isClassInstance(value: any): boolean {
|
|
351
|
+
if (!value || typeof value !== 'object') return false;
|
|
352
|
+
if (Array.isArray(value)) return false;
|
|
353
|
+
|
|
354
|
+
const proto = Object.getPrototypeOf(value);
|
|
355
|
+
// Plain objects have Object.prototype or null as prototype
|
|
356
|
+
return proto !== Object.prototype && proto !== null;
|
|
357
|
+
}
|
|
358
|
+
|
|
189
359
|
/**
|
|
190
360
|
* Main assignGingerly function
|
|
191
361
|
*/
|
|
@@ -224,6 +394,42 @@ export function assignGingerly(
|
|
|
224
394
|
processedSource[sym] = source[sym];
|
|
225
395
|
}
|
|
226
396
|
|
|
397
|
+
// Process 'ish' property for HTMLElements with itemscope (async, non-blocking)
|
|
398
|
+
if ('ish' in processedSource) {
|
|
399
|
+
if (typeof HTMLElement !== 'undefined' && target instanceof HTMLElement) {
|
|
400
|
+
// Capture the value before deleting
|
|
401
|
+
const ishValue = processedSource['ish'];
|
|
402
|
+
// Remove 'ish' from processedSource to prevent normal assignment
|
|
403
|
+
delete processedSource['ish'];
|
|
404
|
+
|
|
405
|
+
// Get the itemscope attribute to track the setup
|
|
406
|
+
const itemscopeValue = target.getAttribute('itemscope');
|
|
407
|
+
|
|
408
|
+
// Load handler on demand and process asynchronously
|
|
409
|
+
const setupPromise = (async () => {
|
|
410
|
+
try {
|
|
411
|
+
const { handleIshProperty } = await import('./handleIshProperty.js');
|
|
412
|
+
await handleIshProperty(target, ishValue, options, assignGingerly);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
console.error('Error in handleIshProperty:', err);
|
|
415
|
+
// Re-throw errors asynchronously so they're visible
|
|
416
|
+
setTimeout(() => { throw err; }, 0);
|
|
417
|
+
}
|
|
418
|
+
})();
|
|
419
|
+
|
|
420
|
+
// Track the setup promise with the registry if we have an itemscope value
|
|
421
|
+
if (itemscopeValue && typeof itemscopeValue === 'string' && itemscopeValue.length > 0) {
|
|
422
|
+
const registry = (target as any).customElementRegistry?.itemscopeRegistry
|
|
423
|
+
?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
|
|
424
|
+
|
|
425
|
+
if (registry && typeof registry._trackSetup === 'function') {
|
|
426
|
+
registry._trackSetup(itemscopeValue, setupPromise);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// For non-HTMLElement targets, 'ish' is processed as a normal property
|
|
431
|
+
}
|
|
432
|
+
|
|
227
433
|
// First pass: handle all non-symbol keys and sync operations
|
|
228
434
|
for (const key of Object.keys(processedSource)) {
|
|
229
435
|
const value = processedSource[key];
|
|
@@ -338,21 +544,43 @@ export function assignGingerly(
|
|
|
338
544
|
const parent = ensureNestedPath(target, pathParts);
|
|
339
545
|
|
|
340
546
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
341
|
-
//
|
|
342
|
-
if (
|
|
343
|
-
|
|
547
|
+
// Check if property exists and is readonly OR is a class instance
|
|
548
|
+
if (lastKey in parent && (isReadonlyProperty(parent, lastKey) || isClassInstance(parent[lastKey]))) {
|
|
549
|
+
// Property is readonly or a class instance - check if current value is an object
|
|
550
|
+
const currentValue = parent[lastKey];
|
|
551
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
552
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(parent, lastKey) ? 'readonly ' : ''}primitive property '${String(lastKey)}'`);
|
|
553
|
+
}
|
|
554
|
+
// Recursively apply assignGingerly to the readonly object or class instance
|
|
555
|
+
assignGingerly(currentValue, value, options);
|
|
556
|
+
} else {
|
|
557
|
+
// Property is writable and not a class instance - normal recursive merge
|
|
558
|
+
if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
|
|
559
|
+
parent[lastKey] = {};
|
|
560
|
+
}
|
|
561
|
+
assignGingerly(parent[lastKey], value, options);
|
|
344
562
|
}
|
|
345
|
-
assignGingerly(parent[lastKey], value, options);
|
|
346
563
|
} else {
|
|
347
564
|
parent[lastKey] = value;
|
|
348
565
|
}
|
|
349
566
|
} else {
|
|
350
567
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
351
|
-
//
|
|
352
|
-
if (
|
|
353
|
-
|
|
568
|
+
// Check if property exists and is readonly OR is a class instance
|
|
569
|
+
if (key in target && (isReadonlyProperty(target, key) || isClassInstance(target[key]))) {
|
|
570
|
+
// Property is readonly or a class instance - check if current value is an object
|
|
571
|
+
const currentValue = target[key];
|
|
572
|
+
if (typeof currentValue !== 'object' || currentValue === null) {
|
|
573
|
+
throw new Error(`Cannot merge object into ${isReadonlyProperty(target, key) ? 'readonly ' : ''}primitive property '${String(key)}'`);
|
|
574
|
+
}
|
|
575
|
+
// Recursively apply assignGingerly to the readonly object or class instance
|
|
576
|
+
assignGingerly(currentValue, value, options);
|
|
577
|
+
} else {
|
|
578
|
+
// Property is writable and not a class instance - normal recursive merge
|
|
579
|
+
if (!(key in target) || typeof target[key] !== 'object') {
|
|
580
|
+
target[key] = {};
|
|
581
|
+
}
|
|
582
|
+
assignGingerly(target[key], value, options);
|
|
354
583
|
}
|
|
355
|
-
assignGingerly(target[key], value, options);
|
|
356
584
|
} else {
|
|
357
585
|
target[key] = value;
|
|
358
586
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle the 'ish' property assignment for HTMLElements with itemscope attributes.
|
|
3
|
+
* This function validates the element and value, then defines or updates the 'ish' property.
|
|
4
|
+
*
|
|
5
|
+
* @param element - The HTMLElement to assign the 'ish' property to
|
|
6
|
+
* @param value - The value to assign (must be an object)
|
|
7
|
+
* @param options - Optional assignGingerly options
|
|
8
|
+
* @param assignGingerlyFn - Reference to the assignGingerly function for recursive calls
|
|
9
|
+
*/
|
|
10
|
+
export async function handleIshProperty(element, value, options, assignGingerlyFn) {
|
|
11
|
+
// Validate itemscope attribute
|
|
12
|
+
const itemscopeValue = element.getAttribute('itemscope');
|
|
13
|
+
if (typeof itemscopeValue !== 'string' || itemscopeValue.length === 0) {
|
|
14
|
+
throw new Error('Element must have itemscope attribute set to a non-empty string value');
|
|
15
|
+
}
|
|
16
|
+
// Validate value is an object
|
|
17
|
+
if (typeof value !== 'object' || value === null) {
|
|
18
|
+
throw new Error('ish property value must be an object');
|
|
19
|
+
}
|
|
20
|
+
// Get or create the 'ish' property on the element
|
|
21
|
+
if (!('ish' in element)) {
|
|
22
|
+
await defineIshProperty(element, itemscopeValue, options, assignGingerlyFn);
|
|
23
|
+
}
|
|
24
|
+
// Queue the value for assignment
|
|
25
|
+
const ishDescriptor = Object.getOwnPropertyDescriptor(element, 'ish');
|
|
26
|
+
if (ishDescriptor && ishDescriptor.set) {
|
|
27
|
+
ishDescriptor.set.call(element, value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Define the 'ish' property on an HTMLElement with itemscope attribute.
|
|
32
|
+
* This function handles both immediate and lazy manager instantiation.
|
|
33
|
+
*
|
|
34
|
+
* @param element - The HTMLElement to define the 'ish' property on
|
|
35
|
+
* @param managerName - The name of the manager (from itemscope attribute)
|
|
36
|
+
* @param options - Optional assignGingerly options
|
|
37
|
+
* @param assignGingerlyFn - Reference to the assignGingerly function for recursive calls
|
|
38
|
+
*/
|
|
39
|
+
async function defineIshProperty(element, managerName, options, assignGingerlyFn) {
|
|
40
|
+
// Determine which registry to use
|
|
41
|
+
const registry = element.customElementRegistry?.itemscopeRegistry
|
|
42
|
+
?? (typeof customElements !== 'undefined' ? customElements.itemscopeRegistry : undefined);
|
|
43
|
+
if (!registry) {
|
|
44
|
+
throw new Error('ItemscopeRegistry not available');
|
|
45
|
+
}
|
|
46
|
+
// Check if manager is registered
|
|
47
|
+
let config = registry.get(managerName);
|
|
48
|
+
// If not registered, wait for registration
|
|
49
|
+
if (!config) {
|
|
50
|
+
const { waitForEvent } = await import('./waitForEvent.js');
|
|
51
|
+
await waitForEvent(registry, managerName);
|
|
52
|
+
config = registry.get(managerName);
|
|
53
|
+
if (!config) {
|
|
54
|
+
throw new Error(`Manager "${managerName}" not found after registration event`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Create manager instance
|
|
58
|
+
let managerInstance = null;
|
|
59
|
+
const valueQueue = [];
|
|
60
|
+
// Define the 'ish' property
|
|
61
|
+
Object.defineProperty(element, 'ish', {
|
|
62
|
+
get() {
|
|
63
|
+
return managerInstance;
|
|
64
|
+
},
|
|
65
|
+
set(newValue) {
|
|
66
|
+
// If setting the same instance, do nothing
|
|
67
|
+
if (newValue === managerInstance) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Queue the value
|
|
71
|
+
valueQueue.push(newValue);
|
|
72
|
+
// If manager not yet instantiated, create it
|
|
73
|
+
if (!managerInstance) {
|
|
74
|
+
// Merge all queued values for initVals
|
|
75
|
+
const initVals = Object.assign({}, ...valueQueue);
|
|
76
|
+
managerInstance = new config.manager(element, initVals);
|
|
77
|
+
valueQueue.length = 0; // Clear queue
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Process queue asynchronously
|
|
81
|
+
(async () => {
|
|
82
|
+
while (valueQueue.length > 0) {
|
|
83
|
+
const queuedValue = valueQueue.shift();
|
|
84
|
+
await assignGingerlyFn(managerInstance, queuedValue, options);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
});
|
|
92
|
+
}
|