1- import type { ReactiveController , ReactiveControllerHost } from 'lit' ;
1+ import { isServer , type ReactiveController , type ReactiveControllerHost } from 'lit' ;
22
33export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
44 /**
@@ -18,11 +18,13 @@ export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
1818 * @default the host's root node
1919 */
2020 rootNode ?: Node ;
21+
2122 /**
2223 * function to call on link children to get their URL hash (i.e. id to scroll to)
2324 * @default el => el.getAttribute('href');
2425 */
2526 getHash ?: ( el : Element ) => string | null ;
27+
2628 /**
2729 * Optional callback for when an intersection occurs
2830 */
@@ -33,16 +35,24 @@ export class ScrollSpyController implements ReactiveController {
3335 static #instances = new Set < ScrollSpyController > ;
3436
3537 static {
36- addEventListener ( 'scroll' , ( ) => {
37- if ( Math . round ( window . innerHeight + window . scrollY ) >= document . body . scrollHeight ) {
38+ if ( ! isServer ) {
39+ addEventListener ( 'scroll' , ( ) => {
40+ if ( Math . round ( window . innerHeight + window . scrollY ) >= document . body . scrollHeight ) {
41+ this . #instances. forEach ( ssc => {
42+ ssc . #setActive( ssc . #linkChildren. at ( - 1 ) ) ;
43+ } ) ;
44+ }
45+ } , { passive : true } ) ;
46+ addEventListener ( 'hashchange' , ( ) => {
3847 this . #instances. forEach ( ssc => {
39- ssc . #setActive ( ssc . #linkChildren . at ( - 1 ) ) ;
48+ ssc . #activateHash ( ) ;
4049 } ) ;
41- }
42- } , { passive : true } ) ;
50+ } ) ;
51+ }
4352 }
4453
4554 #tagNames: string [ ] ;
55+
4656 #activeAttribute: string ;
4757
4858 #io?: IntersectionObserver ;
@@ -57,17 +67,28 @@ export class ScrollSpyController implements ReactiveController {
5767 #intersected = false ;
5868
5969 #root: ScrollSpyControllerOptions [ 'root' ] ;
70+
6071 #rootMargin?: string ;
72+
6173 #threshold: number | number [ ] ;
62- #intersectingElements: Element [ ] = [ ] ;
74+
75+ #intersectingTargets = new Set < Element > ( ) ;
76+
77+ #linkTargetMap = new Map < Element , Element | null > ( ) ;
6378
6479 #getRootNode: ( ) => Node | null ;
80+
6581 #getHash: ( el : Element ) => string | null ;
82+
6683 #onIntersection?: ( ) => void ;
6784
6885 get #linkChildren( ) : Element [ ] {
69- return Array . from ( this . host . querySelectorAll ( this . #tagNames. join ( ',' ) ) )
70- . filter ( this . #getHash) ;
86+ if ( isServer ) {
87+ return [ ] ;
88+ } else {
89+ return Array . from ( this . host . querySelectorAll ( this . #tagNames. join ( ',' ) ) )
90+ . filter ( this . #getHash) ;
91+ }
7192 }
7293
7394 get root ( ) : Element | Document | null | undefined {
@@ -132,12 +153,16 @@ export class ScrollSpyController implements ReactiveController {
132153 if ( rootNode instanceof Document || rootNode instanceof ShadowRoot ) {
133154 const { rootMargin, threshold, root } = this ;
134155 this . #io = new IntersectionObserver ( r => this . #onIo( r ) , { root, rootMargin, threshold } ) ;
135- this . #linkChildren
136- . map ( x => this . #getHash( x ) )
137- . filter ( ( x ) : x is string => ! ! x )
138- . map ( x => rootNode . getElementById ( x . replace ( '#' , '' ) ) )
139- . filter ( ( x ) : x is HTMLElement => ! ! x )
140- . forEach ( target => this . #io?. observe ( target ) ) ;
156+ for ( const link of this . #linkChildren) {
157+ const id = this . #getHash( link ) ?. replace ( '#' , '' ) ;
158+ if ( id ) {
159+ const target = document . getElementById ( id ) ;
160+ if ( target ) {
161+ this . #io?. observe ( target ) ;
162+ this . #linkTargetMap. set ( link , target ) ;
163+ }
164+ }
165+ }
141166 }
142167 }
143168
@@ -155,6 +180,17 @@ export class ScrollSpyController implements ReactiveController {
155180 }
156181 }
157182
183+ async #activateHash( ) {
184+ const links = this . #linkChildren;
185+ const { hash } = location ;
186+ if ( ! hash ) {
187+ this . setActive ( links . at ( 0 ) ?? null ) ;
188+ } else {
189+ await this . #nextIntersection( ) ;
190+ this . setActive ( links . find ( x => this . #getHash( x ) === hash ) ?? null ) ;
191+ }
192+ }
193+
158194 async #nextIntersection( ) {
159195 this . #intersected = false ;
160196 // safeguard the loop
@@ -178,13 +214,15 @@ export class ScrollSpyController implements ReactiveController {
178214 this . #setActive( last ?? this . #linkChildren. at ( 0 ) ) ;
179215 }
180216 this . #intersected = true ;
181- this . #intersectingElements =
182- entries
183- . filter ( x => x . isIntersecting )
184- . map ( x => x . target ) ;
217+ this . #intersectingTargets. clear ( ) ;
218+ for ( const entry of entries ) {
219+ if ( entry . isIntersecting ) {
220+ this . #intersectingTargets. add ( entry . target ) ;
221+ }
222+ }
185223 if ( this . #initializing) {
186224 const ints = entries ?. filter ( x => x . isIntersecting ) ?? [ ] ;
187- if ( this . #intersectingElements ) {
225+ if ( this . #intersectingTargets . size > 0 ) {
188226 const [ { target = null } = { } ] = ints ;
189227 const { id } = target ?? { } ;
190228 if ( id ) {
0 commit comments