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/assignGingerly.js CHANGED
@@ -16,7 +16,22 @@ export function getInstanceMap() {
16
16
  /**
17
17
  * Base registry class for managing enhancement configurations
18
18
  */
19
- export class EnhancementRegistry {
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
- // Recursively apply assignGingerly for nested objects
284
- if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
285
- parent[lastKey] = {};
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
- // Recursively apply assignGingerly for nested objects
296
- if (!(key in target) || typeof target[key] !== 'object') {
297
- target[key] = {};
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
- export class EnhancementRegistry {
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
- // Recursively apply assignGingerly for nested objects
342
- if (!(lastKey in parent) || typeof parent[lastKey] !== 'object') {
343
- parent[lastKey] = {};
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
- // Recursively apply assignGingerly for nested objects
352
- if (!(key in target) || typeof target[key] !== 'object') {
353
- target[key] = {};
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
+ }