Skip to content

Commit fd5a211

Browse files
committed
feat(core): scroll-spy-controller hashchange
1 parent 57a5c57 commit fd5a211

File tree

1 file changed

+50
-15
lines changed

1 file changed

+50
-15
lines changed

core/pfe-core/controllers/scroll-spy-controller.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactiveController, ReactiveControllerHost } from 'lit';
1+
import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit';
22

33
export 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

Comments
 (0)