@@ -76,6 +76,8 @@ interface CustomHTMLElement {
7676
7777interface CustomElementRegistry {
7878 _getDefinition ( tagName : string ) : CustomElementDefinition | undefined ;
79+ createElement ( tagName : string ) : Node ;
80+ cloneSubtree ( node : Node ) : Node ;
7981}
8082
8183interface CustomElementDefinition {
@@ -106,13 +108,13 @@ interface CustomElementDefinition {
106108// Note, `registry` matches proposal but `customElements` was previously
107109// proposed. It's supported for back compat.
108110interface ShadowRootWithSettableCustomElements extends ShadowRoot {
109- registry ?: CustomElementRegistry ;
110- customElements ? : CustomElementRegistry ;
111+ registry ?: CustomElementRegistry | null ;
112+ customElements : CustomElementRegistry | null ;
111113}
112114
113115interface ShadowRootInitWithSettableCustomElements extends ShadowRootInit {
114- registry ?: CustomElementRegistry ;
115- customElements ?: CustomElementRegistry ;
116+ registry ?: CustomElementRegistry | null ;
117+ customElements ?: CustomElementRegistry | null ;
116118}
117119
118120type ParametersOf <
@@ -137,12 +139,29 @@ const globalDefinitionForConstructor = new WeakMap<
137139 CustomElementConstructor ,
138140 CustomElementDefinition
139141> ( ) ;
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+ } ;
146165
147166class AsyncInfo < T > {
148167 readonly promise : Promise < T > ;
@@ -251,8 +270,7 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
251270 if ( awaiting ) {
252271 this . _awaitingUpgrade . delete ( tagName ) ;
253272 for ( const element of awaiting ) {
254- pendingRegistryForElement . delete ( element ) ;
255- customize ( element , definition , true ) ;
273+ this . _upgradeElement ( element , definition ) ;
256274 }
257275 }
258276 // Flush whenDefined callbacks
@@ -268,6 +286,7 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
268286 creationContext . push ( this ) ;
269287 nativeRegistry . upgrade ( ...args ) ;
270288 creationContext . pop ( ) ;
289+ args . forEach ( ( n ) => registryToSubtree ( n , this ) ) ;
271290 }
272291
273292 get ( tagName : string ) {
@@ -312,6 +331,39 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
312331 awaiting . delete ( element ) ;
313332 }
314333 }
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+ }
315367}
316368
317369// User extends this HTMLElement, which returns the CE being upgraded
@@ -345,35 +397,23 @@ window.HTMLElement = (function HTMLElement(this: HTMLElement) {
345397window . HTMLElement . prototype = NativeHTMLElement . prototype ;
346398
347399// 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;
350402const 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 ;
374406 }
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 ;
377417} ;
378418
379419// Helper to create stand-in element for each tagName registered that delegates
@@ -400,13 +440,11 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
400440 // upgrade will eventually install the full CE prototype
401441 Object . setPrototypeOf ( instance , HTMLElement . prototype ) ;
402442 // 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 ) ;
407445 if ( definition ) {
408446 customize ( instance , definition ) ;
409- } else {
447+ } else if ( registry ) {
410448 pendingRegistryForElement . set ( instance , registry ) ;
411449 }
412450 return instance ;
@@ -423,10 +461,25 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
423461 definition . connectedCallback &&
424462 definition . connectedCallback . apply ( this , args ) ;
425463 } 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.
426468 // 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+ }
430483 }
431484 }
432485
@@ -677,15 +730,51 @@ Element.prototype.attachShadow = function (
677730 ...args ,
678731 ] as unknown ) as [ init : ShadowRootInit ] ;
679732 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' ] ;
681738 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 ;
685744 }
686745 return shadowRoot ;
687746} ;
688747
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+
689778// Install scoped creation API on Element & ShadowRoot
690779const creationContext : Array <
691780 Document | CustomElementRegistry | Element | ShadowRoot
@@ -707,15 +796,15 @@ const installScopedCreationMethod = (
707796 // insertAdjacentHTML doesn't return an element, but that's fine since
708797 // it will have a parent that should have a scope
709798 if ( ret !== undefined ) {
710- scopeForElement . set ( ret , this ) ;
799+ registryToSubtree (
800+ ret ,
801+ this . customElements as ShimmedCustomElementsRegistry
802+ ) ;
711803 }
712804 creationContext . pop ( ) ;
713805 return ret ;
714806 } ;
715807} ;
716- installScopedCreationMethod ( ShadowRoot , 'createElement' , document ) ;
717- installScopedCreationMethod ( ShadowRoot , 'createElementNS' , document ) ;
718- installScopedCreationMethod ( ShadowRoot , 'importNode' , document ) ;
719808installScopedCreationMethod ( Element , 'insertAdjacentHTML' ) ;
720809
721810// Install scoped innerHTML on Element & ShadowRoot
@@ -727,6 +816,7 @@ const installScopedCreationSetter = (ctor: Function, name: string) => {
727816 creationContext . push ( this ) ;
728817 descriptor . set ! . call ( this , value ) ;
729818 creationContext . pop ( ) ;
819+ registryToSubtree ( this , this . customElements ) ;
730820 } ,
731821 } ) ;
732822} ;
@@ -759,10 +849,10 @@ if (
759849 return internals ;
760850 } ;
761851
852+ const proto = window [ 'ElementInternals' ] . prototype ;
853+
762854 methods . forEach ( ( method ) => {
763- const proto = window [ 'ElementInternals' ] . prototype ;
764855 const originalMethod = proto [ method ] as Function ;
765-
766856 // eslint-disable-next-line @typescript-eslint/no-explicit-any
767857 ( proto as any ) [ method ] = function ( ...args : Array < unknown > ) {
768858 const host = internalsToHostMap . get ( this ) ;
0 commit comments