1- import type { ReactiveController , ReactiveControllerHost } from 'lit' ;
1+ import { isServer , type ReactiveController , type ReactiveControllerHost } from 'lit' ;
22
33export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
44 /**
@@ -23,6 +23,7 @@ export interface ScrollSpyControllerOptions extends IntersectionObserverInit {
2323 * @default el => el.getAttribute('href');
2424 */
2525 getHash ?: ( el : Element ) => string | null ;
26+
2627 /**
2728 * Optional callback for when an intersection occurs
2829 */
@@ -40,9 +41,15 @@ export class ScrollSpyController implements ReactiveController {
4041 } ) ;
4142 }
4243 } , { passive : true } ) ;
44+ addEventListener ( 'hashchange' , ( ) => {
45+ this . #instances. forEach ( ssc => {
46+ ssc . #activateHash( ) ;
47+ } ) ;
48+ } ) ;
4349 }
4450
4551 #tagNames: string [ ] ;
52+
4653 #activeAttribute: string ;
4754
4855 #io?: IntersectionObserver ;
@@ -57,17 +64,28 @@ export class ScrollSpyController implements ReactiveController {
5764 #intersected = false ;
5865
5966 #root: ScrollSpyControllerOptions [ 'root' ] ;
67+
6068 #rootMargin?: string ;
69+
6170 #threshold: number | number [ ] ;
62- #intersectingElements: Element [ ] = [ ] ;
71+
72+ #intersectingTargets = new Set < Element > ( ) ;
73+
74+ #linkTargetMap = new Map < Element , Element | null > ( ) ;
6375
6476 #getRootNode: ( ) => Node ;
77+
6578 #getHash: ( el : Element ) => string | null ;
79+
6680 #onIntersection?: ( ) => void ;
6781
6882 get #linkChildren( ) : Element [ ] {
69- return Array . from ( this . host . querySelectorAll ( this . #tagNames. join ( ',' ) ) )
70- . filter ( this . #getHash) ;
83+ if ( isServer ) {
84+ return [ ] ;
85+ } else {
86+ return Array . from ( this . host . querySelectorAll ( this . #tagNames. join ( ',' ) ) )
87+ . filter ( this . #getHash) ;
88+ }
7189 }
7290
7391 get root ( ) : Element | Document | null | undefined {
@@ -132,12 +150,16 @@ export class ScrollSpyController implements ReactiveController {
132150 if ( rootNode instanceof Document || rootNode instanceof ShadowRoot ) {
133151 const { rootMargin, threshold, root } = this ;
134152 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 ) ) ;
153+ for ( const link of this . #linkChildren) {
154+ const id = this . #getHash( link ) ?. replace ( '#' , '' ) ;
155+ if ( id ) {
156+ const target = document . getElementById ( id ) ;
157+ if ( target ) {
158+ this . #io?. observe ( target ) ;
159+ this . #linkTargetMap. set ( link , target ) ;
160+ }
161+ }
162+ }
141163 }
142164 }
143165
@@ -155,6 +177,17 @@ export class ScrollSpyController implements ReactiveController {
155177 }
156178 }
157179
180+ async #activateHash( ) {
181+ const links = this . #linkChildren;
182+ const { hash } = location ;
183+ if ( ! hash ) {
184+ this . setActive ( links . at ( 0 ) ?? null ) ;
185+ } else {
186+ await this . #nextIntersection( ) ;
187+ this . setActive ( links . find ( x => this . #getHash( x ) === hash ) ?? null ) ;
188+ }
189+ }
190+
158191 async #nextIntersection( ) {
159192 this . #intersected = false ;
160193 // safeguard the loop
@@ -178,13 +211,15 @@ export class ScrollSpyController implements ReactiveController {
178211 this . #setActive( last ?? this . #linkChildren. at ( 0 ) ) ;
179212 }
180213 this . #intersected = true ;
181- this . #intersectingElements =
182- entries
183- . filter ( x => x . isIntersecting )
184- . map ( x => x . target ) ;
214+ this . #intersectingTargets. clear ( ) ;
215+ for ( const entry of entries ) {
216+ if ( entry . isIntersecting ) {
217+ this . #intersectingTargets. add ( entry . target ) ;
218+ }
219+ }
185220 if ( this . #initializing) {
186221 const ints = entries ?. filter ( x => x . isIntersecting ) ?? [ ] ;
187- if ( this . #intersectingElements ) {
222+ if ( this . #intersectingTargets . size > 0 ) {
188223 const [ { target = null } = { } ] = ints ;
189224 const { id } = target ?? { } ;
190225 if ( id ) {
0 commit comments