fix: fix sidebar active status not working as expected (#140) (#149)

pull/150/head
Kia King Ishii 4 years ago committed by GitHub
parent a28c7376be
commit 0b181e7582
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,66 +3,54 @@ import { onMounted, onUnmounted, onUpdated } from 'vue'
export function useActiveSidebarLinks() { export function useActiveSidebarLinks() {
let rootActiveLink: HTMLAnchorElement | null = null let rootActiveLink: HTMLAnchorElement | null = null
let activeLink: HTMLAnchorElement | null = null let activeLink: HTMLAnchorElement | null = null
const decode = decodeURIComponent
const deactiveLink = (link: HTMLAnchorElement | null) => const onScroll = throttleAndDebounce(setActiveLink, 300)
link && link.classList.remove('active')
const activateLink = (hash: string) => {
deactiveLink(activeLink)
deactiveLink(rootActiveLink)
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
if (activeLink) {
activeLink.classList.add('active')
// also add active class to parent h2 anchors
const rootLi = activeLink.closest('.sidebar > ul > li')
if (rootLi && rootLi !== activeLink.parentElement) {
rootActiveLink = rootLi.querySelector('a')
rootActiveLink && rootActiveLink.classList.add('active')
} else {
rootActiveLink = null
}
}
}
const setActiveLink = () => {
const sidebarLinks = [].slice.call(
document.querySelectorAll('.sidebar a')
) as HTMLAnchorElement[]
const anchors = [].slice
.call(document.querySelectorAll('.header-anchor'))
.filter((anchor: HTMLAnchorElement) =>
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
) as HTMLAnchorElement[]
const pageOffset = (document.querySelector('.navbar') as HTMLElement)
.offsetHeight
const scrollTop = window.scrollY
const getAnchorTop = (anchor: HTMLAnchorElement): number => function setActiveLink(): void {
anchor.parentElement!.offsetTop - pageOffset - 15 const sidebarLinks = getSidebarLinks()
const anchors = getAnchors(sidebarLinks)
for (let i = 0; i < anchors.length; i++) { for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i] const anchor = anchors[i]
const nextAnchor = anchors[i + 1] const nextAnchor = anchors[i + 1]
const isActive =
(i === 0 && scrollTop === 0) ||
(scrollTop >= getAnchorTop(anchor) &&
(!nextAnchor || scrollTop < getAnchorTop(nextAnchor)))
// TODO: fix case when at page bottom const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
if (isActive) { if (isActive) {
const targetHash = decode(anchor.hash) history.replaceState(null, document.title, hash ? hash : ' ')
history.replaceState(null, document.title, targetHash) activateLink(hash)
activateLink(targetHash)
return return
} }
} }
} }
const onScroll = throttleAndDebounce(setActiveLink, 300) function activateLink(hash: string | null): void {
deactiveLink(activeLink)
deactiveLink(rootActiveLink)
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
if (!activeLink) {
return
}
activeLink.classList.add('active')
// also add active class to parent h2 anchors
const rootLi = activeLink.closest('.sidebar-links > ul > li')
if (rootLi && rootLi !== activeLink.parentElement) {
rootActiveLink = rootLi.querySelector('a')
rootActiveLink && rootActiveLink.classList.add('active')
} else {
rootActiveLink = null
}
}
function deactiveLink(link: HTMLAnchorElement | null): void {
link && link.classList.remove('active')
}
onMounted(() => { onMounted(() => {
setActiveLink() setActiveLink()
window.addEventListener('scroll', onScroll) window.addEventListener('scroll', onScroll)
@ -70,7 +58,7 @@ export function useActiveSidebarLinks() {
onUpdated(() => { onUpdated(() => {
// sidebar update means a route change // sidebar update means a route change
activateLink(decode(location.hash)) activateLink(decodeURIComponent(location.hash))
}) })
onUnmounted(() => { onUnmounted(() => {
@ -78,11 +66,61 @@ export function useActiveSidebarLinks() {
}) })
} }
function getSidebarLinks(): HTMLAnchorElement[] {
return [].slice.call(
document.querySelectorAll('.sidebar a.sidebar-link-item')
)
}
function getAnchors(sidebarLinks: HTMLAnchorElement[]): HTMLAnchorElement[] {
return [].slice
.call(document.querySelectorAll('.header-anchor'))
.filter((anchor: HTMLAnchorElement) =>
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
) as HTMLAnchorElement[]
}
function getPageOffset(): number {
return (document.querySelector('.navbar') as HTMLElement).offsetHeight
}
function getAnchorTop(anchor: HTMLAnchorElement): number {
const pageOffset = getPageOffset()
return anchor.parentElement!.offsetTop - pageOffset - 15
}
function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement
): [boolean, string | null] {
const scrollTop = window.scrollY
if (index === 0 && scrollTop === 0) {
return [true, null]
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null]
}
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, decodeURIComponent(anchor.hash)]
}
return [false, null]
}
function throttleAndDebounce(fn: () => void, delay: number): () => void { function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeout: NodeJS.Timeout let timeout: NodeJS.Timeout
let called = false let called = false
return () => { return () => {
if (timeout) clearTimeout(timeout) if (timeout) {
clearTimeout(timeout)
}
if (!called) { if (!called) {
fn() fn()
called = true called = true

Loading…
Cancel
Save