@@ -23,9 +23,25 @@ export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
2323 * @default el => el.getAttribute('href');
2424 */
2525 getHash ?: ( el : Element ) => string | null ;
26+ /**
27+ * Optional callback for when an intersection occurs
28+ */
29+ onIntersection ?( ) : void ;
2630}
2731
2832export class ScrollSpyController implements ReactiveController {
33+ static #instances = new Set < ScrollSpyController > ;
34+
35+ static {
36+ addEventListener ( 'scroll' , ( ) => {
37+ if ( Math . round ( window . innerHeight + window . scrollY ) >= document . body . scrollHeight ) {
38+ this . #instances. forEach ( ssc => {
39+ ssc . #setActive( ssc . #linkChildren. at ( - 1 ) ) ;
40+ } ) ;
41+ }
42+ } , { passive : true } ) ;
43+ }
44+
2945 #tagNames: string [ ] ;
3046 #activeAttribute: string ;
3147
@@ -43,9 +59,11 @@ export class ScrollSpyController implements ReactiveController {
4359 #root: ScrollSpyControllerOptions [ 'root' ] ;
4460 #rootMargin?: string ;
4561 #threshold: number | number [ ] ;
62+ #intersectingElements: Element [ ] = [ ] ;
4663
4764 #getRootNode: ( ) => Node ;
4865 #getHash: ( el : Element ) => string | null ;
66+ #onIntersection?: ( ) => void ;
4967
5068 get #linkChildren( ) : Element [ ] {
5169 return Array . from ( this . host . querySelectorAll ( this . #tagNames. join ( ',' ) ) )
@@ -94,13 +112,22 @@ export class ScrollSpyController implements ReactiveController {
94112 this . #threshold = options . threshold ?? 0.85 ;
95113 this . #getRootNode = ( ) => options . rootNode ?? host . getRootNode ( ) ;
96114 this . #getHash = options ?. getHash ?? ( ( el : Element ) => el . getAttribute ( 'href' ) ) ;
115+ this . #onIntersection = options ?. onIntersection ;
97116 }
98117
99118 hostConnected ( ) : void {
119+ ScrollSpyController . #instances. add ( this ) ;
100120 this . #initIo( ) ;
101121 }
102122
103- #initIo( ) {
123+ hostDisconnected ( ) : void {
124+ ScrollSpyController . #instances. delete ( this ) ;
125+ this . #io?. disconnect ( ) ;
126+ }
127+
128+ #initializing = true ;
129+
130+ async #initIo( ) {
104131 const rootNode = this . #getRootNode( ) ;
105132 if ( rootNode instanceof Document || rootNode instanceof ShadowRoot ) {
106133 const { rootMargin, threshold, root } = this ;
@@ -151,6 +178,25 @@ export class ScrollSpyController implements ReactiveController {
151178 this . #setActive( last ?? this . #linkChildren. at ( 0 ) ) ;
152179 }
153180 this . #intersected = true ;
181+ this . #intersectingElements =
182+ entries
183+ . filter ( x => x . isIntersecting )
184+ . map ( x => x . target ) ;
185+ if ( this . #initializing) {
186+ const ints = entries ?. filter ( x => x . isIntersecting ) ?? [ ] ;
187+ if ( this . #intersectingElements) {
188+ const [ { target = null } = { } ] = ints ;
189+ const { id } = target ?? { } ;
190+ if ( id ) {
191+ const link = this . #linkChildren. find ( link => this . #getHash( link ) === `#${ id } ` ) ;
192+ if ( link ) {
193+ this . #setActive( link ) ;
194+ }
195+ }
196+ }
197+ this . #initializing = false ;
198+ }
199+ this . #onIntersection?.( ) ;
154200 }
155201
156202 /**
0 commit comments