11import { isServer , type ReactiveController , type ReactiveElement } from 'lit' ;
22
3- import { Logger } from './logger.js' ;
4-
53interface AnonymousSlot {
64 hasContent : boolean ;
75 elements : Element [ ] ;
@@ -32,7 +30,9 @@ export interface SlotsConfig {
3230 deprecations ?: Record < string , string > ;
3331}
3432
35- function isObjectConfigSpread (
33+ export type SlotControllerArgs = [ SlotsConfig ] | ( string | null ) [ ] ;
34+
35+ function isObjectSpread (
3636 config : ( [ SlotsConfig ] | ( string | null ) [ ] ) ,
3737) : config is [ SlotsConfig ] {
3838 return config . length === 1 && typeof config [ 0 ] === 'object' && config [ 0 ] !== null ;
@@ -55,60 +55,108 @@ export class SlotController implements ReactiveController {
5555 /** @deprecated use `default` */
5656 public static anonymous : symbol = this . default ;
5757
58- #nodes = new Map < string | typeof SlotController . default , Slot > ( ) ;
58+ private static singletons = new WeakMap < ReactiveElement , SlotController > ( ) ;
5959
60- #logger: Logger ;
60+ #nodes = new Map < string | typeof SlotController . default , Slot > ( ) ;
6161
62- #firstUpdated = false ;
62+ #slotMapInitialized = false ;
6363
64- #mo = new MutationObserver ( records => this . #onMutation ( records ) ) ;
64+ #slotNames: ( string | null ) [ ] = [ ] ;
6565
66- #slotNames : ( string | null ) [ ] ;
66+ #ssrHintHasSlotted : ( string | null ) [ ] = [ ] ;
6767
6868 #deprecations: Record < string , string > = { } ;
6969
70- constructor ( public host : ReactiveElement , ...config : ( [ SlotsConfig ] | ( string | null ) [ ] ) ) {
71- this . #logger = new Logger ( this . host ) ;
70+ #mo = new MutationObserver ( this . #initSlotMap. bind ( this ) ) ;
71+
72+ constructor ( public host : ReactiveElement , ...args : SlotControllerArgs ) {
73+ const singleton = SlotController . singletons . get ( host ) ;
74+ if ( singleton ) {
75+ singleton . #initialize( ...args ) ;
76+ return singleton ;
77+ }
78+ this . #initialize( ...args ) ;
79+ host . addController ( this ) ;
80+ SlotController . singletons . set ( host , this ) ;
81+ if ( ! this . #slotNames. length ) {
82+ this . #slotNames = [ null ] ;
83+ }
84+ }
7285
73- if ( isObjectConfigSpread ( config ) ) {
86+ #initialize( ...config : SlotControllerArgs ) {
87+ if ( isObjectSpread ( config ) ) {
7488 const [ { slots, deprecations } ] = config ;
7589 this . #slotNames = slots ;
7690 this . #deprecations = deprecations ?? { } ;
7791 } else if ( config . length >= 1 ) {
7892 this . #slotNames = config ;
7993 this . #deprecations = { } ;
80- } else {
81- this . #slotNames = [ null ] ;
8294 }
83-
84-
85- host . addController ( this ) ;
8695 }
8796
8897 async hostConnected ( ) : Promise < void > {
89- this . host . addEventListener ( 'slotchange' , this . #onSlotChange as EventListener ) ;
90- this . #firstUpdated = false ;
9198 this . #mo. observe ( this . host , { childList : true } ) ;
99+ this . #ssrHintHasSlotted =
100+ this . host
101+ // @ts -expect-error: this is a ponyfill for ::has-slotted, is not intended as a public API
102+ . ssrHintHasSlotted
103+ ?? [ ] ;
92104 // Map the defined slots into an object that is easier to query
93105 this . #nodes. clear ( ) ;
94- // Loop over the properties provided by the schema
95- this . #slotNames. forEach ( this . #initSlot) ;
96- Object . values ( this . #deprecations) . forEach ( this . #initSlot) ;
97- this . host . requestUpdate ( ) ;
106+ this . #initSlotMap( ) ;
98107 // insurance for framework integrations
99108 await this . host . updateComplete ;
100109 this . host . requestUpdate ( ) ;
101110 }
102111
112+ hostDisconnected ( ) : void {
113+ this . #mo. disconnect ( ) ;
114+ }
115+
103116 hostUpdated ( ) : void {
104- if ( ! this . #firstUpdated) {
105- this . #slotNames. forEach ( this . #initSlot) ;
106- this . #firstUpdated = true ;
117+ if ( ! this . #slotMapInitialized) {
118+ this . #initSlotMap( ) ;
107119 }
108120 }
109121
110- hostDisconnected ( ) : void {
111- this . #mo. disconnect ( ) ;
122+ #initSlotMap( ) {
123+ // Loop over the properties provided by the schema
124+ for ( const slotName of this . #slotNames
125+ . concat ( Object . values ( this . #deprecations) ) ) {
126+ const slotId = slotName || SlotController . default ;
127+ const name = slotName ?? '' ;
128+ const elements = this . #getChildrenForSlot( slotId ) ;
129+ const slot = this . #getSlotElement( slotId ) ;
130+ const hasContent =
131+ isServer ? this . #ssrHintHasSlotted. includes ( slotName )
132+ : ! ! elements . length || ! ! slot ?. assignedNodes ?.( ) ?. filter ( x => x . textContent ?. trim ( ) ) . length ;
133+ this . #nodes. set ( slotId , { elements, name, hasContent, slot } ) ;
134+ }
135+ this . host . requestUpdate ( ) ;
136+ this . #slotMapInitialized = true ;
137+ }
138+
139+ #getSlotElement( slotId : string | symbol ) {
140+ if ( isServer ) {
141+ return null ;
142+ } else {
143+ const selector =
144+ slotId === SlotController . default ? 'slot:not([name])' : `slot[name="${ slotId as string } "]` ;
145+ return this . host . shadowRoot ?. querySelector ?.< HTMLSlotElement > ( selector ) ?? null ;
146+ }
147+ }
148+
149+ #getChildrenForSlot< T extends Element = Element > (
150+ name : string | typeof SlotController . default ,
151+ ) : T [ ] {
152+ if ( isServer ) {
153+ return [ ] ;
154+ } else if ( this . #nodes. has ( name ) ) {
155+ return this . #nodes. get ( name ) ! . slot ?. assignedElements ?.( ) as T [ ] ;
156+ } else {
157+ const children = Array . from ( this . host . children ) as T [ ] ;
158+ return children . filter ( isSlot ( name ) ) ;
159+ }
112160 }
113161
114162 /**
@@ -143,19 +191,11 @@ export class SlotController implements ReactiveController {
143191 * @example this.hasSlotted('header');
144192 */
145193 hasSlotted ( ...names : ( string | null | undefined ) [ ] ) : boolean {
146- if ( isServer ) {
147- return this . host
148- . getAttribute ( 'ssr-hint-has-slotted' )
149- ?. split ( ',' )
150- . map ( name => name . trim ( ) )
151- . some ( name => names . includes ( name === 'default' ? null : name ) ) ?? false ;
152- } else {
153- const slotNames = Array . from ( names , x => x == null ? SlotController . default : x ) ;
154- if ( ! slotNames . length ) {
155- slotNames . push ( SlotController . default ) ;
156- }
157- return slotNames . some ( x => this . #nodes. get ( x ) ?. hasContent ?? false ) ;
194+ const slotNames = Array . from ( names , x => x == null ? SlotController . default : x ) ;
195+ if ( ! slotNames . length ) {
196+ slotNames . push ( SlotController . default ) ;
158197 }
198+ return slotNames . some ( x => this . #nodes. get ( x ) ?. hasContent ?? false ) ;
159199 }
160200
161201 /**
@@ -168,46 +208,4 @@ export class SlotController implements ReactiveController {
168208 isEmpty ( ...names : ( string | null | undefined ) [ ] ) : boolean {
169209 return ! this . hasSlotted ( ...names ) ;
170210 }
171-
172- #onSlotChange = ( event : Event & { target : HTMLSlotElement } ) => {
173- const slotName = event . target . name ;
174- this . #initSlot( slotName ) ;
175- this . host . requestUpdate ( ) ;
176- } ;
177-
178- #onMutation = async ( records : MutationRecord [ ] ) => {
179- const changed = [ ] ;
180- for ( const { addedNodes, removedNodes } of records ) {
181- for ( const node of [ ...addedNodes , ...removedNodes ] ) {
182- if ( node instanceof HTMLElement && node . slot ) {
183- this . #initSlot( node . slot ) ;
184- changed . push ( node . slot ) ;
185- }
186- }
187- }
188- this . host . requestUpdate ( ) ;
189- } ;
190-
191- #getChildrenForSlot< T extends Element = Element > (
192- name : string | typeof SlotController . default ,
193- ) : T [ ] {
194- if ( isServer ) {
195- return [ ] ;
196- } else {
197- const children = Array . from ( this . host . children ) as T [ ] ;
198- return children . filter ( isSlot ( name ) ) ;
199- }
200- }
201-
202- #initSlot = ( slotName : string | null ) => {
203- const name = slotName || SlotController . default ;
204- const elements = this . #nodes. get ( name ) ?. slot ?. assignedElements ?.( )
205- ?? this . #getChildrenForSlot( name ) ;
206- const selector = slotName ? `slot[name="${ slotName } "]` : 'slot:not([name])' ;
207- const slot = this . host . shadowRoot ?. querySelector ?.< HTMLSlotElement > ( selector ) ?? null ;
208- const nodes = slot ?. assignedNodes ?.( ) ;
209- const hasContent = ! ! elements . length || ! ! nodes ?. filter ( x => x . textContent ?. trim ( ) ) . length ;
210- this . #nodes. set ( name , { elements, name : slotName ?? '' , hasContent, slot } ) ;
211- this . #logger. debug ( slotName , hasContent ) ;
212- } ;
213211}
0 commit comments