mirror of https://github.com/vuejs/vitepress
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
94 lines
2.7 KiB
94 lines
2.7 KiB
import { onMounted, onUnmounted, onUpdated } from 'vue'
|
|
|
|
export function useActiveSidebarLinks() {
|
|
let rootActiveLink: HTMLAnchorElement | null = null
|
|
let activeLink: HTMLAnchorElement | null = null
|
|
const decode = decodeURIComponent
|
|
|
|
const deactiveLink = (link: HTMLAnchorElement | null) =>
|
|
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.getElementById('app')!.offsetTop
|
|
const scrollTop = window.scrollY
|
|
|
|
const getAnchorTop = (anchor: HTMLAnchorElement): number =>
|
|
anchor.parentElement!.offsetTop - pageOffset - 15
|
|
|
|
for (let i = 0; i < anchors.length; i++) {
|
|
const anchor = anchors[i]
|
|
const nextAnchor = anchors[i + 1]
|
|
const isActive =
|
|
(i === 0 && scrollTop === 0) ||
|
|
(scrollTop >= getAnchorTop(anchor) &&
|
|
(!nextAnchor || scrollTop < getAnchorTop(nextAnchor)))
|
|
|
|
if (isActive) {
|
|
const targetHash = decode(anchor.hash)
|
|
history.replaceState(null, document.title, targetHash)
|
|
activateLink(targetHash)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
const onScroll = throttleAndDebounce(setActiveLink, 300)
|
|
onMounted(() => {
|
|
setActiveLink()
|
|
window.addEventListener('scroll', onScroll)
|
|
})
|
|
|
|
onUpdated(() => {
|
|
// sidebar update means a route change
|
|
activateLink(decode(location.hash))
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('scroll', onScroll)
|
|
})
|
|
}
|
|
|
|
function throttleAndDebounce(fn: () => void, delay: number): () => void {
|
|
let timeout: number
|
|
let called = false
|
|
return () => {
|
|
if (timeout) clearTimeout(timeout)
|
|
if (!called) {
|
|
fn()
|
|
called = true
|
|
setTimeout(() => {
|
|
called = false
|
|
}, delay)
|
|
} else {
|
|
timeout = setTimeout(fn, delay)
|
|
}
|
|
}
|
|
}
|