@@ -76,6 +76,8 @@ interface CustomHTMLElement {
76
76
77
77
interface CustomElementRegistry {
78
78
_getDefinition ( tagName : string ) : CustomElementDefinition | undefined ;
79
+ createElement ( tagName : string ) : Node ;
80
+ cloneSubtree ( node : Node ) : Node ;
79
81
}
80
82
81
83
interface CustomElementDefinition {
@@ -106,13 +108,13 @@ interface CustomElementDefinition {
106
108
// Note, `registry` matches proposal but `customElements` was previously
107
109
// proposed. It's supported for back compat.
108
110
interface ShadowRootWithSettableCustomElements extends ShadowRoot {
109
- registry ?: CustomElementRegistry ;
110
- customElements ? : CustomElementRegistry ;
111
+ registry ?: CustomElementRegistry | null ;
112
+ customElements : CustomElementRegistry | null ;
111
113
}
112
114
113
115
interface ShadowRootInitWithSettableCustomElements extends ShadowRootInit {
114
- registry ?: CustomElementRegistry ;
115
- customElements ?: CustomElementRegistry ;
116
+ registry ?: CustomElementRegistry | null ;
117
+ customElements ?: CustomElementRegistry | null ;
116
118
}
117
119
118
120
type ParametersOf <
@@ -137,12 +139,29 @@ const globalDefinitionForConstructor = new WeakMap<
137
139
CustomElementConstructor ,
138
140
CustomElementDefinition
139
141
> ( ) ;
140
- // TBD: This part of the spec proposal is unclear:
141
- // > Another option for looking up registries is to store an element's
142
- // > originating registry with the element. The Chrome DOM team was concerned
143
- // > about the small additional memory overhead on all elements. Looking up the
144
- // > root avoids this.
145
- const scopeForElement = new WeakMap < Node , Element | ShadowRoot > ( ) ;
142
+
143
+ const registryForElement = new WeakMap <
144
+ Node ,
145
+ ShimmedCustomElementsRegistry | null
146
+ > ( ) ;
147
+ const registryToSubtree = (
148
+ node : Node ,
149
+ registry : ShimmedCustomElementsRegistry | null ,
150
+ shouldUpgrade ?: boolean
151
+ ) => {
152
+ if ( registryForElement . get ( node ) == null ) {
153
+ registryForElement . set ( node , registry ) ;
154
+ }
155
+ if ( shouldUpgrade && registryForElement . get ( node ) === registry ) {
156
+ registry ?. _upgradeElement ( node as HTMLElement ) ;
157
+ }
158
+ const { children} = node as Element ;
159
+ if ( children ?. length ) {
160
+ Array . from ( children ) . forEach ( ( child ) =>
161
+ registryToSubtree ( child , registry , shouldUpgrade )
162
+ ) ;
163
+ }
164
+ } ;
146
165
147
166
class AsyncInfo < T > {
148
167
readonly promise : Promise < T > ;
@@ -251,8 +270,7 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
251
270
if ( awaiting ) {
252
271
this . _awaitingUpgrade . delete ( tagName ) ;
253
272
for ( const element of awaiting ) {
254
- pendingRegistryForElement . delete ( element ) ;
255
- customize ( element , definition , true ) ;
273
+ this . _upgradeElement ( element , definition ) ;
256
274
}
257
275
}
258
276
// Flush whenDefined callbacks
@@ -268,6 +286,7 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
268
286
creationContext . push ( this ) ;
269
287
nativeRegistry . upgrade ( ...args ) ;
270
288
creationContext . pop ( ) ;
289
+ args . forEach ( ( n ) => registryToSubtree ( n , this ) ) ;
271
290
}
272
291
273
292
get ( tagName : string ) {
@@ -312,6 +331,39 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
312
331
awaiting . delete ( element ) ;
313
332
}
314
333
}
334
+
335
+ // upgrades the given element if defined or queues it for upgrade when defined.
336
+ _upgradeElement ( element : HTMLElement , definition ?: CustomElementDefinition ) {
337
+ definition ??= this . _getDefinition ( element . localName ) ;
338
+ if ( definition !== undefined ) {
339
+ pendingRegistryForElement . delete ( element ) ;
340
+ customize ( element , definition ! , true ) ;
341
+ } else {
342
+ this . _upgradeWhenDefined ( element , element . localName , true ) ;
343
+ }
344
+ }
345
+
346
+ [ 'createElement' ] ( localName : string ) {
347
+ creationContext . push ( this ) ;
348
+ const el = document . createElement ( localName ) ;
349
+ creationContext . pop ( ) ;
350
+ registryToSubtree ( el , this ) ;
351
+ return el ;
352
+ }
353
+
354
+ [ 'cloneSubtree' ] ( node : Node ) {
355
+ creationContext . push ( this ) ;
356
+ // Note, cannot use `cloneNode` here becuase the node may not be in this document
357
+ const subtree = document . importNode ( node , true ) ;
358
+ creationContext . pop ( ) ;
359
+ registryToSubtree ( subtree , this ) ;
360
+ return subtree ;
361
+ }
362
+
363
+ [ 'initializeSubtree' ] ( node : Node ) {
364
+ registryToSubtree ( node , this , true ) ;
365
+ return node ;
366
+ }
315
367
}
316
368
317
369
// User extends this HTMLElement, which returns the CE being upgraded
@@ -345,35 +397,23 @@ window.HTMLElement = (function HTMLElement(this: HTMLElement) {
345
397
window . HTMLElement . prototype = NativeHTMLElement . prototype ;
346
398
347
399
// Helpers to return the scope for a node where its registry would be located
348
- const isValidScope = ( node : Node ) =>
349
- node === document || node instanceof ShadowRoot ;
400
+ // const isValidScope = (node: Node) =>
401
+ // node === document || node instanceof ShadowRoot;
350
402
const registryForNode = ( node : Node ) : ShimmedCustomElementsRegistry | null => {
351
- // TODO: the algorithm for finding the scope is a bit up in the air; assigning
352
- // a one-time scope at creation time would require walking every tree ever
353
- // created, which is avoided for now
354
- let scope = node . getRootNode ( ) ;
355
- // If we're not attached to the document (i.e. in a disconnected tree or
356
- // fragment), we need to get the scope from the creation context; that should
357
- // be a Document or ShadowRoot, unless it was created via innerHTML
358
- if ( ! isValidScope ( scope ) ) {
359
- const context = creationContext [ creationContext . length - 1 ] ;
360
- // When upgrading via registry.upgrade(), the registry itself is put on the
361
- // creationContext stack
362
- if ( context instanceof CustomElementRegistry ) {
363
- return context as ShimmedCustomElementsRegistry ;
364
- }
365
- // Otherwise, get the root node of the element this was created from
366
- scope = context . getRootNode ( ) ;
367
- // The creation context wasn't a Document or ShadowRoot or in one; this
368
- // means we're being innerHTML'ed into a disconnected element; for now, we
369
- // hope that root node was created imperatively, where we stash _its_
370
- // scopeForElement. Beyond that, we'd need more costly tracking.
371
- if ( ! isValidScope ( scope ) ) {
372
- scope = scopeForElement . get ( scope ) ?. getRootNode ( ) || document ;
373
- }
403
+ const context = creationContext [ creationContext . length - 1 ] ;
404
+ if ( context instanceof CustomElementRegistry ) {
405
+ return context as ShimmedCustomElementsRegistry ;
374
406
}
375
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
376
- return ( scope as any ) [ 'registry' ] as ShimmedCustomElementsRegistry | null ;
407
+ if (
408
+ context ?. nodeType === Node . ELEMENT_NODE ||
409
+ context ?. nodeType === Node . DOCUMENT_FRAGMENT_NODE
410
+ ) {
411
+ return context . customElements as ShimmedCustomElementsRegistry ;
412
+ }
413
+ return node . nodeType === Node . ELEMENT_NODE
414
+ ? ( ( node as Element ) . customElements as ShimmedCustomElementsRegistry ) ??
415
+ null
416
+ : null ;
377
417
} ;
378
418
379
419
// Helper to create stand-in element for each tagName registered that delegates
@@ -400,13 +440,11 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
400
440
// upgrade will eventually install the full CE prototype
401
441
Object . setPrototypeOf ( instance , HTMLElement . prototype ) ;
402
442
// Get the node's scope, and its registry (falls back to global registry)
403
- const registry =
404
- registryForNode ( instance ) ||
405
- ( window . customElements as ShimmedCustomElementsRegistry ) ;
406
- const definition = registry . _getDefinition ( tagName ) ;
443
+ const registry = registryForNode ( instance ) ;
444
+ const definition = registry ?. _getDefinition ( tagName ) ;
407
445
if ( definition ) {
408
446
customize ( instance , definition ) ;
409
- } else {
447
+ } else if ( registry ) {
410
448
pendingRegistryForElement . set ( instance , registry ) ;
411
449
}
412
450
return instance ;
@@ -423,10 +461,25 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
423
461
definition . connectedCallback &&
424
462
definition . connectedCallback . apply ( this , args ) ;
425
463
} else {
464
+ // NOTE, if this has a null registry, then it should be changed
465
+ // to the registry into which it's inserted.
466
+ // LIMITATION: this is only done for custom elements and not built-ins
467
+ // since we can't easily see their connection state changing.
426
468
// Register for upgrade when defined (only when connected, so we don't leak)
427
- pendingRegistryForElement
428
- . get ( this ) !
429
- . _upgradeWhenDefined ( this , tagName , true ) ;
469
+ const pendingRegistry = pendingRegistryForElement . get ( this ) ;
470
+ if ( pendingRegistry !== undefined ) {
471
+ pendingRegistry . _upgradeWhenDefined ( this , tagName , true ) ;
472
+ } else {
473
+ const registry =
474
+ this . customElements ?? this . parentElement ?. customElements ;
475
+ if ( registry ) {
476
+ registryToSubtree (
477
+ this ,
478
+ registry as ShimmedCustomElementsRegistry ,
479
+ true
480
+ ) ;
481
+ }
482
+ }
430
483
}
431
484
}
432
485
@@ -677,15 +730,51 @@ Element.prototype.attachShadow = function (
677
730
...args ,
678
731
] as unknown ) as [ init : ShadowRootInit ] ;
679
732
const shadowRoot = nativeAttachShadow . apply ( this , nativeArgs ) ;
680
- const registry = init [ 'registry' ] ?? init . customElements ;
733
+ // Note, this allows a `null` customElements purely for testing.
734
+ const registry =
735
+ init [ 'customElements' ] === undefined
736
+ ? init [ 'registry' ]
737
+ : init [ 'customElements' ] ;
681
738
if ( registry !== undefined ) {
682
- ( shadowRoot as ShadowRootWithSettableCustomElements ) . customElements = ( shadowRoot as ShadowRootWithSettableCustomElements ) [
683
- 'registry'
684
- ] = registry ;
739
+ registryForElement . set (
740
+ shadowRoot ,
741
+ registry as ShimmedCustomElementsRegistry
742
+ ) ;
743
+ ( shadowRoot as ShadowRootWithSettableCustomElements ) [ 'registry' ] = registry ;
685
744
}
686
745
return shadowRoot ;
687
746
} ;
688
747
748
+ const customElementsDescriptor = {
749
+ get ( this : Element ) {
750
+ const registry = registryForElement . get ( this ) ;
751
+ return registry === undefined
752
+ ? ( ( this . nodeType === Node . DOCUMENT_NODE
753
+ ? this
754
+ : this . ownerDocument ) as Document ) ?. defaultView ?. customElements ||
755
+ null
756
+ : registry ;
757
+ } ,
758
+ enumerable : true ,
759
+ configurable : true ,
760
+ } ;
761
+
762
+ Object . defineProperty (
763
+ Element . prototype ,
764
+ 'customElements' ,
765
+ customElementsDescriptor
766
+ ) ;
767
+ Object . defineProperty (
768
+ Document . prototype ,
769
+ 'customElements' ,
770
+ customElementsDescriptor
771
+ ) ;
772
+ Object . defineProperty (
773
+ ShadowRoot . prototype ,
774
+ 'customElements' ,
775
+ customElementsDescriptor
776
+ ) ;
777
+
689
778
// Install scoped creation API on Element & ShadowRoot
690
779
const creationContext : Array <
691
780
Document | CustomElementRegistry | Element | ShadowRoot
@@ -707,15 +796,15 @@ const installScopedCreationMethod = (
707
796
// insertAdjacentHTML doesn't return an element, but that's fine since
708
797
// it will have a parent that should have a scope
709
798
if ( ret !== undefined ) {
710
- scopeForElement . set ( ret , this ) ;
799
+ registryToSubtree (
800
+ ret ,
801
+ this . customElements as ShimmedCustomElementsRegistry
802
+ ) ;
711
803
}
712
804
creationContext . pop ( ) ;
713
805
return ret ;
714
806
} ;
715
807
} ;
716
- installScopedCreationMethod ( ShadowRoot , 'createElement' , document ) ;
717
- installScopedCreationMethod ( ShadowRoot , 'createElementNS' , document ) ;
718
- installScopedCreationMethod ( ShadowRoot , 'importNode' , document ) ;
719
808
installScopedCreationMethod ( Element , 'insertAdjacentHTML' ) ;
720
809
721
810
// Install scoped innerHTML on Element & ShadowRoot
@@ -727,6 +816,7 @@ const installScopedCreationSetter = (ctor: Function, name: string) => {
727
816
creationContext . push ( this ) ;
728
817
descriptor . set ! . call ( this , value ) ;
729
818
creationContext . pop ( ) ;
819
+ registryToSubtree ( this , this . customElements ) ;
730
820
} ,
731
821
} ) ;
732
822
} ;
@@ -759,10 +849,10 @@ if (
759
849
return internals ;
760
850
} ;
761
851
852
+ const proto = window [ 'ElementInternals' ] . prototype ;
853
+
762
854
methods . forEach ( ( method ) => {
763
- const proto = window [ 'ElementInternals' ] . prototype ;
764
855
const originalMethod = proto [ method ] as Function ;
765
-
766
856
// eslint-disable-next-line @typescript-eslint/no-explicit-any
767
857
( proto as any ) [ method ] = function ( ...args : Array < unknown > ) {
768
858
const host = internalsToHostMap . get ( this ) ;
0 commit comments