assign-gingerly 0.0.73 → 0.0.74
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 +29 -92
- package/assignFeatures.js +145 -254
- package/assignFeatures.ts +713 -837
- package/defineWithFeatures.js +12 -72
- package/defineWithFeatures.ts +92 -167
- package/enhanceAll.js +8 -4
- package/enhanceAll.ts +8 -7
- package/inferencer/types/assign-gingerly/types.d.ts +2 -1
- package/inferencer/types/font-face-feature/types.d.ts +9 -0
- package/package.json +9 -1
- package/processHandlerCommands.js +11 -34
- package/processHandlerCommands.ts +13 -40
- package/resolveAndAssignFeatures.js +9 -40
- package/resolveAndAssignFeatures.ts +43 -79
- package/types/assign-gingerly/types.d.ts +2 -16
package/assignFeatures.ts
CHANGED
|
@@ -1,837 +1,713 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* assignFeatures - Dependency injection for custom element features
|
|
3
|
-
*
|
|
4
|
-
* Allows custom element authors to declare supported feature slots via
|
|
5
|
-
* `static supportedFeatures`, then have implementations injected via
|
|
6
|
-
* `customElementRegistry.assignFeatures(Ctor, features)`.
|
|
7
|
-
*
|
|
8
|
-
* Features are lazily instantiated on first property access via getter-only
|
|
9
|
-
* properties installed on the class prototype.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
*
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
*
|
|
715
|
-
* Call this in a feature's `static onAssigned` to read config fragments
|
|
716
|
-
* suggested by other features that were processed earlier.
|
|
717
|
-
*
|
|
718
|
-
* @param toFeatureSymbol - Symbol identifying this feature (the target)
|
|
719
|
-
* @param targetClass - The custom element class being configured
|
|
720
|
-
* @returns Array of suggestions (empty if none)
|
|
721
|
-
*
|
|
722
|
-
* @example
|
|
723
|
-
* import { getFeatureInfoSuggestions } from 'assign-gingerly/assignFeatures.js';
|
|
724
|
-
* import { ROUNDABOUT_FEATURE } from './symbols.js';
|
|
725
|
-
*
|
|
726
|
-
* class RoundaboutFeature {
|
|
727
|
-
* static onAssigned(ctr, featureConfig) {
|
|
728
|
-
* const suggestions = getFeatureInfoSuggestions(ROUNDABOUT_FEATURE, ctr);
|
|
729
|
-
* for (const suggestion of suggestions) {
|
|
730
|
-
* if (suggestion.customData) {
|
|
731
|
-
* featureConfig.customData = { ...featureConfig.customData, ...suggestion.customData };
|
|
732
|
-
* }
|
|
733
|
-
* }
|
|
734
|
-
* }
|
|
735
|
-
* }
|
|
736
|
-
*/
|
|
737
|
-
export function getFeatureInfoSuggestions(
|
|
738
|
-
toFeatureSymbol: symbol,
|
|
739
|
-
targetClass: Function
|
|
740
|
-
): FeatureInfoSuggestion[] {
|
|
741
|
-
const symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
|
|
742
|
-
if (!symbolMap) return [];
|
|
743
|
-
return symbolMap.get(targetClass) || [];
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// =============================================================================
|
|
747
|
-
// PropertyBag — base class for nested feature containers
|
|
748
|
-
// =============================================================================
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* PropertyBag is a base class for creating nested feature containers.
|
|
752
|
-
*
|
|
753
|
-
* Subclass it to group related features under a single namespace property.
|
|
754
|
-
* PropertyBag carries the `customElementRegistry` reference from the host element
|
|
755
|
-
* so that nested features can resolve their registries correctly.
|
|
756
|
-
*
|
|
757
|
-
* PropertyBag must be subclassed — direct instantiation throws an error.
|
|
758
|
-
* Subclasses must define `static supportedFeatures` to declare their feature slots.
|
|
759
|
-
*
|
|
760
|
-
* @example
|
|
761
|
-
* class ClubMemberBehaviors extends PropertyBag {
|
|
762
|
-
* static supportedFeatures = {
|
|
763
|
-
* commandBehavior: { fallbackSpawn: CommandFeatureImpl },
|
|
764
|
-
* ariaBehavior: { fallbackSpawn: AriaFeatureImpl }
|
|
765
|
-
* }
|
|
766
|
-
* }
|
|
767
|
-
*
|
|
768
|
-
* class ClubMember extends HTMLElement {
|
|
769
|
-
* static supportedFeatures = {
|
|
770
|
-
* behaviors: { fallbackSpawn: ClubMemberBehaviors }
|
|
771
|
-
* }
|
|
772
|
-
* }
|
|
773
|
-
*
|
|
774
|
-
* customElements.assignFeatures(ClubMember, { behaviors: { spawn: ClubMemberBehaviors } });
|
|
775
|
-
* customElements.assignFeatures(ClubMemberBehaviors, {
|
|
776
|
-
* commandBehavior: { spawn: CommandFeatureImpl }
|
|
777
|
-
* });
|
|
778
|
-
*/
|
|
779
|
-
export class PropertyBag {
|
|
780
|
-
/** Registry reference carried from the host element */
|
|
781
|
-
customElementRegistry: any;
|
|
782
|
-
|
|
783
|
-
constructor(hostElement: any, ctx?: FeatureSpawnContext, initVals?: any) {
|
|
784
|
-
if (this.constructor === PropertyBag) {
|
|
785
|
-
throw new Error(
|
|
786
|
-
'PropertyBag must be subclassed. Define static supportedFeatures on your subclass.'
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Carry the registry reference from the host element
|
|
791
|
-
this.customElementRegistry = hostElement.customElementRegistry ||
|
|
792
|
-
(typeof customElements !== 'undefined' ? customElements : undefined);
|
|
793
|
-
|
|
794
|
-
// Apply any initVals
|
|
795
|
-
if (initVals && typeof initVals === 'object') {
|
|
796
|
-
Object.assign(this, initVals);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// =============================================================================
|
|
802
|
-
// Self-installing: adds featuresRegistry and assignFeatures to CustomElementRegistry
|
|
803
|
-
// This code runs as a side effect when this module is imported.
|
|
804
|
-
// =============================================================================
|
|
805
|
-
|
|
806
|
-
declare global {
|
|
807
|
-
interface CustomElementRegistry {
|
|
808
|
-
featuresRegistry: FeaturesRegistry;
|
|
809
|
-
assignFeatures(ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (typeof CustomElementRegistry !== 'undefined') {
|
|
814
|
-
Object.defineProperty(CustomElementRegistry.prototype, 'featuresRegistry', {
|
|
815
|
-
get: function () {
|
|
816
|
-
const registry = new FeaturesRegistry();
|
|
817
|
-
Object.defineProperty(this, 'featuresRegistry', {
|
|
818
|
-
value: registry,
|
|
819
|
-
writable: true,
|
|
820
|
-
enumerable: false,
|
|
821
|
-
configurable: true,
|
|
822
|
-
});
|
|
823
|
-
return registry;
|
|
824
|
-
},
|
|
825
|
-
enumerable: false,
|
|
826
|
-
configurable: true,
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
|
|
830
|
-
value: function (ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined {
|
|
831
|
-
return assignFeatures(ctr, features, this.featuresRegistry);
|
|
832
|
-
},
|
|
833
|
-
writable: true,
|
|
834
|
-
enumerable: false,
|
|
835
|
-
configurable: true,
|
|
836
|
-
});
|
|
837
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* assignFeatures - Dependency injection for custom element features
|
|
3
|
+
*
|
|
4
|
+
* Allows custom element authors to declare supported feature slots via
|
|
5
|
+
* `static supportedFeatures`, then have implementations injected via
|
|
6
|
+
* `customElementRegistry.assignFeatures(Ctor, features)`.
|
|
7
|
+
*
|
|
8
|
+
* Features are lazily instantiated on first property access via getter-only
|
|
9
|
+
* properties installed on the class prototype.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
13
|
+
import { isAsyncSpawn } from './utils/isAsyncSpawn.js';
|
|
14
|
+
import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
|
|
15
|
+
import { FeatureSpawnContext, SupportedFeatureConfig, FeatureConfig, SupportedFeaturesMap, FeatureConfigsMap } from './types/assign-gingerly/types.js';
|
|
16
|
+
export type { FeatureSpawnContext, SupportedFeatureConfig, FeatureConfig, SupportedFeaturesMap, FeatureConfigsMap };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* WeakMap storing per-instance feature caches.
|
|
20
|
+
* Outer key: the instance (element or other object).
|
|
21
|
+
* Inner map: feature key -> spawned instance.
|
|
22
|
+
*/
|
|
23
|
+
const featureStorage = new WeakMap<object, Map<string, any>>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The features registry: maps a constructor to its accumulated feature injections.
|
|
27
|
+
*/
|
|
28
|
+
export class FeaturesRegistry {
|
|
29
|
+
#registry = new Map<Function, Map<string, FeatureConfig>>();
|
|
30
|
+
|
|
31
|
+
has(ctr: Function): boolean {
|
|
32
|
+
return this.#registry.has(ctr);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get(ctr: Function): Map<string, FeatureConfig> | undefined {
|
|
36
|
+
return this.#registry.get(ctr);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set(ctr: Function, key: string, injection: FeatureConfig): void {
|
|
40
|
+
let features = this.#registry.get(ctr);
|
|
41
|
+
if (!features) {
|
|
42
|
+
features = new Map();
|
|
43
|
+
this.#registry.set(ctr, features);
|
|
44
|
+
}
|
|
45
|
+
features.set(key, injection);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hasKey(ctr: Function, key: string): boolean {
|
|
49
|
+
const features = this.#registry.get(ctr);
|
|
50
|
+
return features ? features.has(key) : false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sentinel symbol to mark stored values as raw initVals (not yet spawned).
|
|
56
|
+
*/
|
|
57
|
+
const RAW_INIT_VALS = Symbol('rawInitVals');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Cache for resolved feature spawns.
|
|
61
|
+
* Key: target constructor, Value: Map<featureKey, resolvedConstructor>
|
|
62
|
+
*/
|
|
63
|
+
export const resolvedSpawnCache = new WeakMap<Function, Map<string, any>>();
|
|
64
|
+
|
|
65
|
+
function getOrCreateClassCache(ctr: Function): Map<string, any> {
|
|
66
|
+
let classCache = resolvedSpawnCache.get(ctr);
|
|
67
|
+
if (!classCache) {
|
|
68
|
+
classCache = new Map();
|
|
69
|
+
resolvedSpawnCache.set(ctr, classCache);
|
|
70
|
+
}
|
|
71
|
+
return classCache;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolves a spawn reference (constructor, async function, or import-path string).
|
|
76
|
+
*/
|
|
77
|
+
async function resolveSpawn(spawnish: any): Promise<Function | undefined> {
|
|
78
|
+
if (!spawnish) return undefined;
|
|
79
|
+
if (typeof spawnish === 'string') {
|
|
80
|
+
return await findClassPrototypeInPath(spawnish);
|
|
81
|
+
}
|
|
82
|
+
if (isAsyncSpawn(spawnish)) {
|
|
83
|
+
return await spawnish();
|
|
84
|
+
}
|
|
85
|
+
return spawnish;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if resolving this spawn reference requires awaiting.
|
|
90
|
+
*/
|
|
91
|
+
function isAsyncResolution(spawnish: any): boolean {
|
|
92
|
+
if (typeof spawnish === 'string') return true;
|
|
93
|
+
if (isAsyncSpawn(spawnish)) return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves all configured spawns for the target constructor and caches them.
|
|
99
|
+
* Returns a Promise if any resolution is async, otherwise undefined.
|
|
100
|
+
*/
|
|
101
|
+
function resolveAndCacheSpawns(
|
|
102
|
+
ctr: Function,
|
|
103
|
+
features: FeatureConfigsMap,
|
|
104
|
+
supportedFeatures: SupportedFeaturesMap
|
|
105
|
+
): Promise<void> | undefined {
|
|
106
|
+
let hasAsync = false;
|
|
107
|
+
const asyncResolutions: Promise<void>[] = [];
|
|
108
|
+
|
|
109
|
+
for (const key of Object.keys(features)) {
|
|
110
|
+
const classCache = getOrCreateClassCache(ctr);
|
|
111
|
+
if (classCache.has(key)) continue;
|
|
112
|
+
|
|
113
|
+
const featureConfig = features[key];
|
|
114
|
+
let spawnish = featureConfig.spawn;
|
|
115
|
+
if (spawnish === undefined) {
|
|
116
|
+
const optIn = supportedFeatures[key];
|
|
117
|
+
spawnish = optIn?.fallbackSpawn;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!spawnish) {
|
|
121
|
+
classCache.set(key, undefined);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isAsyncResolution(spawnish)) {
|
|
126
|
+
hasAsync = true;
|
|
127
|
+
asyncResolutions.push((async () => {
|
|
128
|
+
const resolved = await resolveSpawn(spawnish);
|
|
129
|
+
classCache.set(key, resolved);
|
|
130
|
+
})());
|
|
131
|
+
} else {
|
|
132
|
+
classCache.set(key, spawnish);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (hasAsync) {
|
|
137
|
+
return Promise.all(asyncResolutions).then(() => undefined);
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Installs a getter/setter pair on the constructor's prototype for the given feature key.
|
|
144
|
+
*
|
|
145
|
+
* - The setter stores raw values (pre-upgrade or early assignment) into the WeakMap
|
|
146
|
+
* tagged with a sentinel so the getter knows they are initVals, not spawned instances.
|
|
147
|
+
* - The getter spawns the feature instance on first access, using any stored raw value
|
|
148
|
+
* as initVals, then caches the spawned instance.
|
|
149
|
+
*
|
|
150
|
+
* If an own-property with the same key exists on the instance (e.g., set before
|
|
151
|
+
* the element upgraded), it is captured as initVals and deleted so the prototype
|
|
152
|
+
* getter/setter is no longer shadowed.
|
|
153
|
+
*/
|
|
154
|
+
function installFeatureGetter(
|
|
155
|
+
ctr: Function,
|
|
156
|
+
key: string,
|
|
157
|
+
featuresRegistry: FeaturesRegistry
|
|
158
|
+
): void {
|
|
159
|
+
Object.defineProperty(ctr.prototype, key, {
|
|
160
|
+
get: function (this: any) {
|
|
161
|
+
// Get or create the per-instance storage
|
|
162
|
+
let storage = featureStorage.get(this);
|
|
163
|
+
if (!storage) {
|
|
164
|
+
storage = new Map();
|
|
165
|
+
featureStorage.set(this, storage);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const stored = storage.get(key);
|
|
169
|
+
|
|
170
|
+
// If already spawned (not a raw sentinel, not undefined), return it
|
|
171
|
+
if (stored !== undefined && !(stored && typeof stored === 'object' && RAW_INIT_VALS in stored)) {
|
|
172
|
+
return stored;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Determine initVals: from setter-stored raw value, or from own-property shadow
|
|
176
|
+
let initVals: any = undefined;
|
|
177
|
+
if (stored && typeof stored === 'object' && RAW_INIT_VALS in stored) {
|
|
178
|
+
initVals = stored[RAW_INIT_VALS];
|
|
179
|
+
storage.delete(key);
|
|
180
|
+
} else if (Object.hasOwn(this, key)) {
|
|
181
|
+
initVals = this[key];
|
|
182
|
+
delete this[key]; // Unshadow the prototype accessor
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Resolve the registry — use scoped registry if available, fall back to global
|
|
186
|
+
const registry = (this.customElementRegistry || customElements) as any;
|
|
187
|
+
const fr: FeaturesRegistry = registry.featuresRegistry;
|
|
188
|
+
|
|
189
|
+
if (!fr || !fr.has(ctr)) {
|
|
190
|
+
throw new Error(`assignFeatures: featuresRegistry missing entry for constructor`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const features = fr.get(ctr)!;
|
|
194
|
+
const injection = features.get(key);
|
|
195
|
+
|
|
196
|
+
if (!injection) {
|
|
197
|
+
throw new Error(`assignFeatures: no injection found for feature "${key}"`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Resolve spawn from the cache populated before getters were installed
|
|
201
|
+
const spawns = resolvedSpawnCache.get(ctr);
|
|
202
|
+
const resolvedSpawn = spawns?.get(key);
|
|
203
|
+
if (!resolvedSpawn) {
|
|
204
|
+
throw new Error(`assignFeatures: no spawn implementation found for feature "${key}"`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const supportedFeatures = (ctr as any).supportedFeatures;
|
|
208
|
+
const optIn: SupportedFeatureConfig | undefined = supportedFeatures?.[key];
|
|
209
|
+
|
|
210
|
+
if (!optIn) {
|
|
211
|
+
throw new Error(`assignFeatures: "${key}" not in static supportedFeatures`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// Build the spawn context
|
|
216
|
+
const shared = optIn.getSharedContext?.(this);
|
|
217
|
+
const ctx: FeatureSpawnContext = {
|
|
218
|
+
key,
|
|
219
|
+
optIn,
|
|
220
|
+
injection,
|
|
221
|
+
featuresRegistry: fr,
|
|
222
|
+
shared
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Parse attributes if withAttrs is configured
|
|
226
|
+
let attrInitVals: any = undefined;
|
|
227
|
+
if (injection.withAttrs && this instanceof Element) {
|
|
228
|
+
try {
|
|
229
|
+
attrInitVals = parseWithAttrs(
|
|
230
|
+
this as Element,
|
|
231
|
+
injection.withAttrs,
|
|
232
|
+
true // always unprefixed for features
|
|
233
|
+
);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error('Error parsing feature attributes:', e);
|
|
236
|
+
throw e;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Merge: attributes are base layer, programmatic initVals override
|
|
241
|
+
if (attrInitVals) {
|
|
242
|
+
initVals = initVals
|
|
243
|
+
? { ...attrInitVals, ...initVals }
|
|
244
|
+
: attrInitVals;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Synchronous path: SpawnClass is a constructor
|
|
248
|
+
const instance = new (resolvedSpawn as any)(this, ctx, initVals);
|
|
249
|
+
|
|
250
|
+
// Validate shape if configured
|
|
251
|
+
if (optIn.validateShape) {
|
|
252
|
+
if (!optIn.validateShape(instance)) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`assignFeatures: spawned instance for "${key}" failed shape validation`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
storage.set(key, instance);
|
|
260
|
+
return instance;
|
|
261
|
+
},
|
|
262
|
+
enumerable: true,
|
|
263
|
+
configurable: false
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Valid lifecycle callback names that can be forwarded to features.
|
|
269
|
+
*/
|
|
270
|
+
const VALID_CALLBACKS = new Set([
|
|
271
|
+
'connectedCallback',
|
|
272
|
+
'disconnectedCallback',
|
|
273
|
+
'attributeChangedCallback',
|
|
274
|
+
'adoptedCallback',
|
|
275
|
+
'formDisabledCallback',
|
|
276
|
+
'formResetCallback',
|
|
277
|
+
'formStateRestoreCallback'
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* WeakMap tracking which callbacks have been patched on which constructors,
|
|
282
|
+
* and which feature keys are registered for each callback.
|
|
283
|
+
* Structure: Map<Function, Map<callbackName, Set<featureKey>>>
|
|
284
|
+
*/
|
|
285
|
+
const callbackRegistry = new Map<Function, Map<string, Set<string>>>();
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Installs or updates lifecycle callback forwarding on a constructor's prototype.
|
|
289
|
+
* Patches the callback once per type, accumulating feature keys for each.
|
|
290
|
+
*/
|
|
291
|
+
function installCallbackForwarding(
|
|
292
|
+
ctr: Function,
|
|
293
|
+
key: string,
|
|
294
|
+
callbacks: string[]
|
|
295
|
+
): void {
|
|
296
|
+
let ctrCallbacks = callbackRegistry.get(ctr);
|
|
297
|
+
if (!ctrCallbacks) {
|
|
298
|
+
ctrCallbacks = new Map();
|
|
299
|
+
callbackRegistry.set(ctr, ctrCallbacks);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const callbackName of callbacks) {
|
|
303
|
+
if (!VALID_CALLBACKS.has(callbackName)) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`assignFeatures: invalid callbackForwarding "${callbackName}" for feature "${key}". ` +
|
|
306
|
+
`Valid values: ${[...VALID_CALLBACKS].join(', ')}`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Validate that the spawn class has the method (sync spawners only)
|
|
311
|
+
// For async spawners, validation is deferred to runtime
|
|
312
|
+
|
|
313
|
+
let featureKeys = ctrCallbacks.get(callbackName);
|
|
314
|
+
if (!featureKeys) {
|
|
315
|
+
featureKeys = new Set();
|
|
316
|
+
ctrCallbacks.set(callbackName, featureKeys);
|
|
317
|
+
|
|
318
|
+
// Patch the prototype callback (only once per callback type per class)
|
|
319
|
+
const original = ctr.prototype[callbackName];
|
|
320
|
+
|
|
321
|
+
Object.defineProperty(ctr.prototype, callbackName, {
|
|
322
|
+
value: function (this: any, ...args: any[]) {
|
|
323
|
+
// Call original first
|
|
324
|
+
if (original) original.apply(this, args);
|
|
325
|
+
|
|
326
|
+
// Forward to all registered features
|
|
327
|
+
const keys = callbackRegistry.get(ctr)?.get(callbackName);
|
|
328
|
+
if (keys) {
|
|
329
|
+
for (const featureKey of keys) {
|
|
330
|
+
// Access the getter (triggers lazy spawn on first connectedCallback)
|
|
331
|
+
const feature = this[featureKey];
|
|
332
|
+
|
|
333
|
+
// Only forward if it's a real instance
|
|
334
|
+
if (feature && typeof feature === 'object' &&
|
|
335
|
+
typeof feature[callbackName] === 'function') {
|
|
336
|
+
feature[callbackName](...args);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
writable: true,
|
|
342
|
+
enumerable: false,
|
|
343
|
+
configurable: true
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add this feature key to the set for this callback
|
|
348
|
+
featureKeys.add(key);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function installOneFeature(
|
|
353
|
+
ctr: Function,
|
|
354
|
+
key: string,
|
|
355
|
+
features: FeatureConfigsMap,
|
|
356
|
+
supportedFeatures: SupportedFeaturesMap,
|
|
357
|
+
featuresRegistry: FeaturesRegistry
|
|
358
|
+
): Promise<void> | undefined {
|
|
359
|
+
// 1. Confirm the key is opted-in via supportedFeatures
|
|
360
|
+
if (!(key in supportedFeatures)) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`assignFeatures: "${key}" is not declared in ${ctr.name || 'constructor'}.supportedFeatures`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. Check that the prototype doesn't already have this property defined
|
|
367
|
+
const existingDescriptor = Object.getOwnPropertyDescriptor(ctr.prototype, key);
|
|
368
|
+
if (existingDescriptor) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`assignFeatures: "${key}" already exists on ${ctr.name || 'constructor'}.prototype`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 3. Check that this key hasn't already been registered for this constructor
|
|
375
|
+
if (featuresRegistry.hasKey(ctr, key)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`assignFeatures: "${key}" has already been assigned for ${ctr.name || 'constructor'}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 4. Register the injection
|
|
382
|
+
featuresRegistry.set(ctr, key, features[key]);
|
|
383
|
+
|
|
384
|
+
// 5. Install the lazy getter on the prototype
|
|
385
|
+
installFeatureGetter(ctr, key, featuresRegistry);
|
|
386
|
+
|
|
387
|
+
// 6. Install callback forwarding if configured (merge author + consumer)
|
|
388
|
+
const featureConfig = features[key];
|
|
389
|
+
const optIn = supportedFeatures[key];
|
|
390
|
+
const authorCallbacks = optIn.callbackForwarding || [];
|
|
391
|
+
const consumerCallbacks = featureConfig.callbackForwarding || [];
|
|
392
|
+
// Union of both (author defaults + consumer additions)
|
|
393
|
+
const allCallbacks = [...new Set([...authorCallbacks, ...consumerCallbacks])];
|
|
394
|
+
if (allCallbacks.length > 0) {
|
|
395
|
+
installCallbackForwarding(ctr, key, allCallbacks);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 7. Call static onAssigned if the resolved spawn class defines it
|
|
399
|
+
const classCache = getOrCreateClassCache(ctr);
|
|
400
|
+
const resolvedSpawn = classCache.get(key);
|
|
401
|
+
if (resolvedSpawn && !isAsyncSpawn(resolvedSpawn) &&
|
|
402
|
+
Object.hasOwn(resolvedSpawn, 'onAssigned') &&
|
|
403
|
+
typeof (resolvedSpawn as any).onAssigned === 'function') {
|
|
404
|
+
const result = (resolvedSpawn as any).onAssigned(ctr, featureConfig, key);
|
|
405
|
+
if (result && typeof result.then === 'function') {
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function installAllFeatures(
|
|
413
|
+
ctr: Function,
|
|
414
|
+
features: FeatureConfigsMap,
|
|
415
|
+
supportedFeatures: SupportedFeaturesMap,
|
|
416
|
+
featuresRegistry: FeaturesRegistry
|
|
417
|
+
): Promise<void> | undefined {
|
|
418
|
+
let asyncResult: Promise<void> | undefined;
|
|
419
|
+
for (const key of Object.keys(features)) {
|
|
420
|
+
const result = installOneFeature(ctr, key, features, supportedFeatures, featuresRegistry);
|
|
421
|
+
if (result) {
|
|
422
|
+
if (!asyncResult) {
|
|
423
|
+
asyncResult = result.then(() => undefined);
|
|
424
|
+
} else {
|
|
425
|
+
asyncResult = asyncResult.then(() => result.then(() => undefined));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return asyncResult;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Core assignFeatures implementation.
|
|
434
|
+
* Validates inputs, resolves all configured spawns (async if needed), caches them,
|
|
435
|
+
* and installs lazy getters on the prototype.
|
|
436
|
+
*
|
|
437
|
+
* Important: Call assignFeatures BEFORE customElements.define(), or at minimum
|
|
438
|
+
* before any instances of the element are created. The lazy getters must be on
|
|
439
|
+
* the prototype before instances exist to properly capture pre-set properties.
|
|
440
|
+
*
|
|
441
|
+
* @param ctr - The constructor (class) to assign features to
|
|
442
|
+
* @param features - Map of feature keys to their injection configs
|
|
443
|
+
* @param featuresRegistry - The registry to store injections in
|
|
444
|
+
*/
|
|
445
|
+
export function assignFeatures(
|
|
446
|
+
ctr: Function,
|
|
447
|
+
features: FeatureConfigsMap,
|
|
448
|
+
featuresRegistry: FeaturesRegistry
|
|
449
|
+
): Promise<void> | undefined {
|
|
450
|
+
// Validate that the constructor has static supportedFeatures
|
|
451
|
+
const supportedFeatures: SupportedFeaturesMap | undefined = (ctr as any).supportedFeatures;
|
|
452
|
+
|
|
453
|
+
if (!supportedFeatures) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`assignFeatures: ${ctr.name || 'constructor'} does not define static supportedFeatures`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Resolve all configured spawns before installing getters. This is the single
|
|
460
|
+
// source of truth for spawn resolution; other callers (defineWithFeatures, etc.)
|
|
461
|
+
// delegate to assignFeatures.
|
|
462
|
+
const spawnResolution = resolveAndCacheSpawns(ctr, features, supportedFeatures);
|
|
463
|
+
|
|
464
|
+
if (spawnResolution) {
|
|
465
|
+
return spawnResolution.then(() =>
|
|
466
|
+
installAllFeatures(ctr, features, supportedFeatures, featuresRegistry)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return installAllFeatures(ctr, features, supportedFeatures, featuresRegistry);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Captures own-properties that shadow feature getters and stores them as initVals.
|
|
475
|
+
* Call this in the custom element constructor to handle pre-upgrade property values.
|
|
476
|
+
*
|
|
477
|
+
* When an element exists in the DOM before `define()` is called, properties may have
|
|
478
|
+
* been set on it directly. After upgrade, these own-properties shadow the prototype
|
|
479
|
+
* getters installed by `assignFeatures`. This helper captures those values and deletes
|
|
480
|
+
* the own-properties so the getters can function properly.
|
|
481
|
+
*
|
|
482
|
+
* @param instance - The custom element instance (typically `this` in the constructor)
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* class ClubMember extends HTMLElement {
|
|
486
|
+
* static supportedFeatures = { photoTaker: { fallbackSpawn: PhotoTakerImpl } }
|
|
487
|
+
* constructor() {
|
|
488
|
+
* super();
|
|
489
|
+
* captureFeatureInitVals(this);
|
|
490
|
+
* }
|
|
491
|
+
* }
|
|
492
|
+
*/
|
|
493
|
+
export function captureFeatureInitVals(instance: any): void {
|
|
494
|
+
const ctr = instance.constructor;
|
|
495
|
+
const supportedFeatures = ctr.supportedFeatures;
|
|
496
|
+
if (!supportedFeatures) return;
|
|
497
|
+
|
|
498
|
+
for (const key of Object.keys(supportedFeatures)) {
|
|
499
|
+
if (Object.hasOwn(instance, key)) {
|
|
500
|
+
const value = instance[key];
|
|
501
|
+
delete instance[key];
|
|
502
|
+
|
|
503
|
+
// Store in the WeakMap so the getter can pick it up
|
|
504
|
+
let storage = featureStorage.get(instance);
|
|
505
|
+
if (!storage) {
|
|
506
|
+
storage = new Map();
|
|
507
|
+
featureStorage.set(instance, storage);
|
|
508
|
+
}
|
|
509
|
+
storage.set(key, { [RAW_INIT_VALS]: value });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// =============================================================================
|
|
515
|
+
// Inter-feature communication: suggestFeatureInfo / getFeatureInfoSuggestions
|
|
516
|
+
// =============================================================================
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* A suggestion from one feature to another, containing config fragments to merge.
|
|
520
|
+
*/
|
|
521
|
+
export interface FeatureInfoSuggestion {
|
|
522
|
+
/** The feature class that made the suggestion (for debugging/tracing) */
|
|
523
|
+
from: Function;
|
|
524
|
+
/** Attribute patterns to merge into the target feature's withAttrs */
|
|
525
|
+
withAttrs?: any;
|
|
526
|
+
/** Custom data to merge into the target feature's customData */
|
|
527
|
+
customData?: any;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Global store for inter-feature suggestions.
|
|
532
|
+
* Structure: Map<targetSymbol, Map<targetClass, Array<FeatureInfoSuggestion>>>
|
|
533
|
+
* Scoped per target class to prevent leaking between different custom elements.
|
|
534
|
+
*/
|
|
535
|
+
const featureInfoSuggestions = new Map<symbol, Map<Function, FeatureInfoSuggestion[]>>();
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Suggest configuration to another feature during registration.
|
|
539
|
+
*
|
|
540
|
+
* Call this in a feature's `static onAssigned` to provide config fragments
|
|
541
|
+
* (withAttrs, customData) that another feature should merge into its own config.
|
|
542
|
+
*
|
|
543
|
+
* The target feature is identified by a Symbol (stable across versions and mocks).
|
|
544
|
+
* Suggestions are scoped per target class to prevent leaking between different
|
|
545
|
+
* custom elements that use the same features.
|
|
546
|
+
*
|
|
547
|
+
* @param fromFeatureCtr - The feature class making the suggestion (for tracing)
|
|
548
|
+
* @param toFeatureSymbol - Symbol identifying the target feature
|
|
549
|
+
* @param featureInfo - Config fragments to suggest (withAttrs, customData)
|
|
550
|
+
* @param targetClass - The custom element class being configured
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* import { suggestFeatureInfo } from 'assign-gingerly/assignFeatures.js';
|
|
554
|
+
* import { ROUNDABOUT_FEATURE } from 'roundabout/symbols.js';
|
|
555
|
+
*
|
|
556
|
+
* class FaceUp {
|
|
557
|
+
* static onAssigned(ctr, featureConfig) {
|
|
558
|
+
* suggestFeatureInfo(FaceUp, ROUNDABOUT_FEATURE, {
|
|
559
|
+
* customData: { formBindings: { value: 'value' } }
|
|
560
|
+
* }, ctr);
|
|
561
|
+
* }
|
|
562
|
+
* }
|
|
563
|
+
*/
|
|
564
|
+
export function suggestFeatureInfo(
|
|
565
|
+
fromFeatureCtr: Function,
|
|
566
|
+
toFeatureSymbol: symbol,
|
|
567
|
+
featureInfo: { withAttrs?: any; customData?: any },
|
|
568
|
+
targetClass: Function
|
|
569
|
+
): void {
|
|
570
|
+
let symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
|
|
571
|
+
if (!symbolMap) {
|
|
572
|
+
symbolMap = new Map();
|
|
573
|
+
featureInfoSuggestions.set(toFeatureSymbol, symbolMap);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
let suggestions = symbolMap.get(targetClass);
|
|
577
|
+
if (!suggestions) {
|
|
578
|
+
suggestions = [];
|
|
579
|
+
symbolMap.set(targetClass, suggestions);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
suggestions.push({
|
|
583
|
+
from: fromFeatureCtr,
|
|
584
|
+
...featureInfo
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Retrieve suggestions made to a feature by other features.
|
|
590
|
+
*
|
|
591
|
+
* Call this in a feature's `static onAssigned` to read config fragments
|
|
592
|
+
* suggested by other features that were processed earlier.
|
|
593
|
+
*
|
|
594
|
+
* @param toFeatureSymbol - Symbol identifying this feature (the target)
|
|
595
|
+
* @param targetClass - The custom element class being configured
|
|
596
|
+
* @returns Array of suggestions (empty if none)
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* import { getFeatureInfoSuggestions } from 'assign-gingerly/assignFeatures.js';
|
|
600
|
+
* import { ROUNDABOUT_FEATURE } from './symbols.js';
|
|
601
|
+
*
|
|
602
|
+
* class RoundaboutFeature {
|
|
603
|
+
* static onAssigned(ctr, featureConfig) {
|
|
604
|
+
* const suggestions = getFeatureInfoSuggestions(ROUNDABOUT_FEATURE, ctr);
|
|
605
|
+
* for (const suggestion of suggestions) {
|
|
606
|
+
* if (suggestion.customData) {
|
|
607
|
+
* featureConfig.customData = { ...featureConfig.customData, ...suggestion.customData };
|
|
608
|
+
* }
|
|
609
|
+
* }
|
|
610
|
+
* }
|
|
611
|
+
* }
|
|
612
|
+
*/
|
|
613
|
+
export function getFeatureInfoSuggestions(
|
|
614
|
+
toFeatureSymbol: symbol,
|
|
615
|
+
targetClass: Function
|
|
616
|
+
): FeatureInfoSuggestion[] {
|
|
617
|
+
const symbolMap = featureInfoSuggestions.get(toFeatureSymbol);
|
|
618
|
+
if (!symbolMap) return [];
|
|
619
|
+
return symbolMap.get(targetClass) || [];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// =============================================================================
|
|
623
|
+
// PropertyBag — base class for nested feature containers
|
|
624
|
+
// =============================================================================
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* PropertyBag is a base class for creating nested feature containers.
|
|
628
|
+
*
|
|
629
|
+
* Subclass it to group related features under a single namespace property.
|
|
630
|
+
* PropertyBag carries the `customElementRegistry` reference from the host element
|
|
631
|
+
* so that nested features can resolve their registries correctly.
|
|
632
|
+
*
|
|
633
|
+
* PropertyBag must be subclassed — direct instantiation throws an error.
|
|
634
|
+
* Subclasses must define `static supportedFeatures` to declare their feature slots.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* class ClubMemberBehaviors extends PropertyBag {
|
|
638
|
+
* static supportedFeatures = {
|
|
639
|
+
* commandBehavior: { fallbackSpawn: CommandFeatureImpl },
|
|
640
|
+
* ariaBehavior: { fallbackSpawn: AriaFeatureImpl }
|
|
641
|
+
* }
|
|
642
|
+
* }
|
|
643
|
+
*
|
|
644
|
+
* class ClubMember extends HTMLElement {
|
|
645
|
+
* static supportedFeatures = {
|
|
646
|
+
* behaviors: { fallbackSpawn: ClubMemberBehaviors }
|
|
647
|
+
* }
|
|
648
|
+
* }
|
|
649
|
+
*
|
|
650
|
+
* customElements.assignFeatures(ClubMember, { behaviors: { spawn: ClubMemberBehaviors } });
|
|
651
|
+
* customElements.assignFeatures(ClubMemberBehaviors, {
|
|
652
|
+
* commandBehavior: { spawn: CommandFeatureImpl }
|
|
653
|
+
* });
|
|
654
|
+
*/
|
|
655
|
+
export class PropertyBag {
|
|
656
|
+
/** Registry reference carried from the host element */
|
|
657
|
+
customElementRegistry: any;
|
|
658
|
+
|
|
659
|
+
constructor(hostElement: any, ctx?: FeatureSpawnContext, initVals?: any) {
|
|
660
|
+
if (this.constructor === PropertyBag) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
'PropertyBag must be subclassed. Define static supportedFeatures on your subclass.'
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Carry the registry reference from the host element
|
|
667
|
+
this.customElementRegistry = hostElement.customElementRegistry ||
|
|
668
|
+
(typeof customElements !== 'undefined' ? customElements : undefined);
|
|
669
|
+
|
|
670
|
+
// Apply any initVals
|
|
671
|
+
if (initVals && typeof initVals === 'object') {
|
|
672
|
+
Object.assign(this, initVals);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// =============================================================================
|
|
678
|
+
// Self-installing: adds featuresRegistry and assignFeatures to CustomElementRegistry
|
|
679
|
+
// This code runs as a side effect when this module is imported.
|
|
680
|
+
// =============================================================================
|
|
681
|
+
|
|
682
|
+
declare global {
|
|
683
|
+
interface CustomElementRegistry {
|
|
684
|
+
featuresRegistry: FeaturesRegistry;
|
|
685
|
+
assignFeatures(ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (typeof CustomElementRegistry !== 'undefined') {
|
|
690
|
+
Object.defineProperty(CustomElementRegistry.prototype, 'featuresRegistry', {
|
|
691
|
+
get: function () {
|
|
692
|
+
const registry = new FeaturesRegistry();
|
|
693
|
+
Object.defineProperty(this, 'featuresRegistry', {
|
|
694
|
+
value: registry,
|
|
695
|
+
writable: true,
|
|
696
|
+
enumerable: false,
|
|
697
|
+
configurable: true,
|
|
698
|
+
});
|
|
699
|
+
return registry;
|
|
700
|
+
},
|
|
701
|
+
enumerable: false,
|
|
702
|
+
configurable: true,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
Object.defineProperty(CustomElementRegistry.prototype, 'assignFeatures', {
|
|
706
|
+
value: function (ctr: Function, features: FeatureConfigsMap): Promise<void> | undefined {
|
|
707
|
+
return assignFeatures(ctr, features, this.featuresRegistry);
|
|
708
|
+
},
|
|
709
|
+
writable: true,
|
|
710
|
+
enumerable: false,
|
|
711
|
+
configurable: true,
|
|
712
|
+
});
|
|
713
|
+
}
|