Skip to content

Commit 0b181e7

Browse files
authored
fix: fix sidebar active status not working as expected (#140) (#149)
1 parent a28c737 commit 0b181e7

File tree

1 file changed

+86
-48
lines changed

1 file changed

+86
-48
lines changed

src/client/theme-default/composables/activeSidebarLink.ts

Lines changed: 86 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,86 +3,124 @@ import { onMounted, onUnmounted, onUpdated } from 'vue'
33
export function useActiveSidebarLinks() {
44
let rootActiveLink: HTMLAnchorElement | null = null
55
let activeLink: HTMLAnchorElement | null = null
6-
const decode = decodeURIComponent
76

8-
const deactiveLink = (link: HTMLAnchorElement | null) =>
9-
link && link.classList.remove('active')
10-
11-
const activateLink = (hash: string) => {
12-
deactiveLink(activeLink)
13-
deactiveLink(rootActiveLink)
14-
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
15-
if (activeLink) {
16-
activeLink.classList.add('active')
17-
// also add active class to parent h2 anchors
18-
const rootLi = activeLink.closest('.sidebar > ul > li')
19-
if (rootLi && rootLi !== activeLink.parentElement) {
20-
rootActiveLink = rootLi.querySelector('a')
21-
rootActiveLink && rootActiveLink.classList.add('active')
22-
} else {
23-
rootActiveLink = null
24-
}
25-
}
26-
}
27-
28-
const setActiveLink = () => {
29-
const sidebarLinks = [].slice.call(
30-
document.querySelectorAll('.sidebar a')
31-
) as HTMLAnchorElement[]
32-
33-
const anchors = [].slice
34-
.call(document.querySelectorAll('.header-anchor'))
35-
.filter((anchor: HTMLAnchorElement) =>
36-
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
37-
) as HTMLAnchorElement[]
38-
39-
const pageOffset = (document.querySelector('.navbar') as HTMLElement)
40-
.offsetHeight
41-
const scrollTop = window.scrollY
7+
const onScroll = throttleAndDebounce(setActiveLink, 300)
428

43-
const getAnchorTop = (anchor: HTMLAnchorElement): number =>
44-
anchor.parentElement!.offsetTop - pageOffset - 15
9+
function setActiveLink(): void {
10+
const sidebarLinks = getSidebarLinks()
11+
const anchors = getAnchors(sidebarLinks)
4512

4613
for (let i = 0; i < anchors.length; i++) {
4714
const anchor = anchors[i]
4815
const nextAnchor = anchors[i + 1]
49-
const isActive =
50-
(i === 0 && scrollTop === 0) ||
51-
(scrollTop >= getAnchorTop(anchor) &&
52-
(!nextAnchor || scrollTop < getAnchorTop(nextAnchor)))
5316

54-
// TODO: fix case when at page bottom
17+
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
5518

5619
if (isActive) {
57-
const targetHash = decode(anchor.hash)
58-
history.replaceState(null, document.title, targetHash)
59-
activateLink(targetHash)
20+
history.replaceState(null, document.title, hash ? hash : ' ')
21+
activateLink(hash)
6022
return
6123
}
6224
}
6325
}
6426

65-
const onScroll = throttleAndDebounce(setActiveLink, 300)
27+
function activateLink(hash: string | null): void {
28+
deactiveLink(activeLink)
29+
deactiveLink(rootActiveLink)
30+
31+
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
32+
33+
if (!activeLink) {
34+
return
35+
}
36+
37+
activeLink.classList.add('active')
38+
39+
// also add active class to parent h2 anchors
40+
const rootLi = activeLink.closest('.sidebar-links > ul > li')
41+
42+
if (rootLi && rootLi !== activeLink.parentElement) {
43+
rootActiveLink = rootLi.querySelector('a')
44+
rootActiveLink && rootActiveLink.classList.add('active')
45+
} else {
46+
rootActiveLink = null
47+
}
48+
}
49+
50+
function deactiveLink(link: HTMLAnchorElement | null): void {
51+
link && link.classList.remove('active')
52+
}
53+
6654
onMounted(() => {
6755
setActiveLink()
6856
window.addEventListener('scroll', onScroll)
6957
})
7058

7159
onUpdated(() => {
7260
// sidebar update means a route change
73-
activateLink(decode(location.hash))
61+
activateLink(decodeURIComponent(location.hash))
7462
})
7563

7664
onUnmounted(() => {
7765
window.removeEventListener('scroll', onScroll)
7866
})
7967
}
8068

69+
function getSidebarLinks(): HTMLAnchorElement[] {
70+
return [].slice.call(
71+
document.querySelectorAll('.sidebar a.sidebar-link-item')
72+
)
73+
}
74+
75+
function getAnchors(sidebarLinks: HTMLAnchorElement[]): HTMLAnchorElement[] {
76+
return [].slice
77+
.call(document.querySelectorAll('.header-anchor'))
78+
.filter((anchor: HTMLAnchorElement) =>
79+
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
80+
) as HTMLAnchorElement[]
81+
}
82+
83+
function getPageOffset(): number {
84+
return (document.querySelector('.navbar') as HTMLElement).offsetHeight
85+
}
86+
87+
function getAnchorTop(anchor: HTMLAnchorElement): number {
88+
const pageOffset = getPageOffset()
89+
90+
return anchor.parentElement!.offsetTop - pageOffset - 15
91+
}
92+
93+
function isAnchorActive(
94+
index: number,
95+
anchor: HTMLAnchorElement,
96+
nextAnchor: HTMLAnchorElement
97+
): [boolean, string | null] {
98+
const scrollTop = window.scrollY
99+
100+
if (index === 0 && scrollTop === 0) {
101+
return [true, null]
102+
}
103+
104+
if (scrollTop < getAnchorTop(anchor)) {
105+
return [false, null]
106+
}
107+
108+
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
109+
return [true, decodeURIComponent(anchor.hash)]
110+
}
111+
112+
return [false, null]
113+
}
114+
81115
function throttleAndDebounce(fn: () => void, delay: number): () => void {
82116
let timeout: NodeJS.Timeout
83117
let called = false
118+
84119
return () => {
85-
if (timeout) clearTimeout(timeout)
120+
if (timeout) {
121+
clearTimeout(timeout)
122+
}
123+
86124
if (!called) {
87125
fn()
88126
called = true

0 commit comments

Comments
 (0)