1
1
import { isServer , type ReactiveController , type ReactiveElement } from 'lit' ;
2
2
3
- import { Logger } from './logger.js' ;
4
-
5
3
interface AnonymousSlot {
6
4
hasContent : boolean ;
7
5
elements : Element [ ] ;
@@ -32,7 +30,9 @@ export interface SlotsConfig {
32
30
deprecations ?: Record < string , string > ;
33
31
}
34
32
35
- function isObjectConfigSpread (
33
+ export type SlotControllerArgs = [ SlotsConfig ] | ( string | null ) [ ] ;
34
+
35
+ function isObjectSpread (
36
36
config : ( [ SlotsConfig ] | ( string | null ) [ ] ) ,
37
37
) : config is [ SlotsConfig ] {
38
38
return config . length === 1 && typeof config [ 0 ] === 'object' && config [ 0 ] !== null ;
@@ -55,60 +55,108 @@ export class SlotController implements ReactiveController {
55
55
/** @deprecated use `default` */
56
56
public static anonymous : symbol = this . default ;
57
57
58
- #nodes = new Map < string | typeof SlotController . default , Slot > ( ) ;
58
+ private static singletons = new WeakMap < ReactiveElement , SlotController > ( ) ;
59
59
60
- #logger: Logger ;
60
+ #nodes = new Map < string | typeof SlotController . default , Slot > ( ) ;
61
61
62
- #firstUpdated = false ;
62
+ #slotMapInitialized = false ;
63
63
64
- #mo = new MutationObserver ( records => this . #onMutation ( records ) ) ;
64
+ #slotNames: ( string | null ) [ ] = [ ] ;
65
65
66
- #slotNames : ( string | null ) [ ] ;
66
+ #ssrHintHasSlotted : ( string | null ) [ ] = [ ] ;
67
67
68
68
#deprecations: Record < string , string > = { } ;
69
69
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
+ }
72
85
73
- if ( isObjectConfigSpread ( config ) ) {
86
+ #initialize( ...config : SlotControllerArgs ) {
87
+ if ( isObjectSpread ( config ) ) {
74
88
const [ { slots, deprecations } ] = config ;
75
89
this . #slotNames = slots ;
76
90
this . #deprecations = deprecations ?? { } ;
77
91
} else if ( config . length >= 1 ) {
78
92
this . #slotNames = config ;
79
93
this . #deprecations = { } ;
80
- } else {
81
- this . #slotNames = [ null ] ;
82
94
}
83
-
84
-
85
- host . addController ( this ) ;
86
95
}
87
96
88
97
async hostConnected ( ) : Promise < void > {
89
- this . host . addEventListener ( 'slotchange' , this . #onSlotChange as EventListener ) ;
90
- this . #firstUpdated = false ;
91
98
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
+ ?? [ ] ;
92
104
// Map the defined slots into an object that is easier to query
93
105
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( ) ;
98
107
// insurance for framework integrations
99
108
await this . host . updateComplete ;
100
109
this . host . requestUpdate ( ) ;
101
110
}
102
111
112
+ hostDisconnected ( ) : void {
113
+ this . #mo. disconnect ( ) ;
114
+ }
115
+
103
116
hostUpdated ( ) : void {
104
- if ( ! this . #firstUpdated) {
105
- this . #slotNames. forEach ( this . #initSlot) ;
106
- this . #firstUpdated = true ;
117
+ if ( ! this . #slotMapInitialized) {
118
+ this . #initSlotMap( ) ;
107
119
}
108
120
}
109
121
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
+ }
112
160
}
113
161
114
162
/**
@@ -143,19 +191,11 @@ export class SlotController implements ReactiveController {
143
191
* @example this.hasSlotted('header');
144
192
*/
145
193
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 ) ;
158
197
}
198
+ return slotNames . some ( x => this . #nodes. get ( x ) ?. hasContent ?? false ) ;
159
199
}
160
200
161
201
/**
@@ -168,46 +208,4 @@ export class SlotController implements ReactiveController {
168
208
isEmpty ( ...names : ( string | null | undefined ) [ ] ) : boolean {
169
209
return ! this . hasSlotted ( ...names ) ;
170
210
}
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
- } ;
213
211
}
0 commit comments