|
|
@ -3,19 +3,42 @@ 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')
|
|
|
|
|
|
|
|
|
|
|
|
function setActiveLink(): void {
|
|
|
|
|
|
|
|
const sidebarLinks = getSidebarLinks()
|
|
|
|
|
|
|
|
const anchors = getAnchors(sidebarLinks)
|
|
|
|
|
|
|
|
|
|
|
|
const activateLink = (hash: string) => {
|
|
|
|
for (let i = 0; i < anchors.length; i++) {
|
|
|
|
|
|
|
|
const anchor = anchors[i]
|
|
|
|
|
|
|
|
const nextAnchor = anchors[i + 1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
|
|
|
|
history.replaceState(null, document.title, hash ? hash : ' ')
|
|
|
|
|
|
|
|
activateLink(hash)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function activateLink(hash: string | null): void {
|
|
|
|
deactiveLink(activeLink)
|
|
|
|
deactiveLink(activeLink)
|
|
|
|
deactiveLink(rootActiveLink)
|
|
|
|
deactiveLink(rootActiveLink)
|
|
|
|
|
|
|
|
|
|
|
|
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
|
|
|
|
activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
|
|
|
|
if (activeLink) {
|
|
|
|
|
|
|
|
|
|
|
|
if (!activeLink) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
activeLink.classList.add('active')
|
|
|
|
activeLink.classList.add('active')
|
|
|
|
|
|
|
|
|
|
|
|
// also add active class to parent h2 anchors
|
|
|
|
// also add active class to parent h2 anchors
|
|
|
|
const rootLi = activeLink.closest('.sidebar > ul > li')
|
|
|
|
const rootLi = activeLink.closest('.sidebar-links > ul > li')
|
|
|
|
|
|
|
|
|
|
|
|
if (rootLi && rootLi !== activeLink.parentElement) {
|
|
|
|
if (rootLi && rootLi !== activeLink.parentElement) {
|
|
|
|
rootActiveLink = rootLi.querySelector('a')
|
|
|
|
rootActiveLink = rootLi.querySelector('a')
|
|
|
|
rootActiveLink && rootActiveLink.classList.add('active')
|
|
|
|
rootActiveLink && rootActiveLink.classList.add('active')
|
|
|
@ -23,66 +46,81 @@ export function useActiveSidebarLinks() {
|
|
|
|
rootActiveLink = null
|
|
|
|
rootActiveLink = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function deactiveLink(link: HTMLAnchorElement | null): void {
|
|
|
|
|
|
|
|
link && link.classList.remove('active')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const setActiveLink = () => {
|
|
|
|
onMounted(() => {
|
|
|
|
const sidebarLinks = [].slice.call(
|
|
|
|
setActiveLink()
|
|
|
|
document.querySelectorAll('.sidebar a')
|
|
|
|
window.addEventListener('scroll', onScroll)
|
|
|
|
) as HTMLAnchorElement[]
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onUpdated(() => {
|
|
|
|
|
|
|
|
// sidebar update means a route change
|
|
|
|
|
|
|
|
activateLink(decodeURIComponent(location.hash))
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const anchors = [].slice
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
|
|
window.removeEventListener('scroll', onScroll)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'))
|
|
|
|
.call(document.querySelectorAll('.header-anchor'))
|
|
|
|
.filter((anchor: HTMLAnchorElement) =>
|
|
|
|
.filter((anchor: HTMLAnchorElement) =>
|
|
|
|
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
|
|
|
|
sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
|
|
|
|
) as HTMLAnchorElement[]
|
|
|
|
) as HTMLAnchorElement[]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const pageOffset = (document.querySelector('.navbar') as HTMLElement)
|
|
|
|
function getPageOffset(): number {
|
|
|
|
.offsetHeight
|
|
|
|
return (document.querySelector('.navbar') as HTMLElement).offsetHeight
|
|
|
|
const scrollTop = window.scrollY
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getAnchorTop = (anchor: HTMLAnchorElement): number =>
|
|
|
|
function getAnchorTop(anchor: HTMLAnchorElement): number {
|
|
|
|
anchor.parentElement!.offsetTop - pageOffset - 15
|
|
|
|
const pageOffset = getPageOffset()
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < anchors.length; i++) {
|
|
|
|
return anchor.parentElement!.offsetTop - pageOffset - 15
|
|
|
|
const anchor = anchors[i]
|
|
|
|
}
|
|
|
|
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
|
|
|
|
function isAnchorActive(
|
|
|
|
|
|
|
|
index: number,
|
|
|
|
|
|
|
|
anchor: HTMLAnchorElement,
|
|
|
|
|
|
|
|
nextAnchor: HTMLAnchorElement
|
|
|
|
|
|
|
|
): [boolean, string | null] {
|
|
|
|
|
|
|
|
const scrollTop = window.scrollY
|
|
|
|
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
if (index === 0 && scrollTop === 0) {
|
|
|
|
const targetHash = decode(anchor.hash)
|
|
|
|
return [true, null]
|
|
|
|
history.replaceState(null, document.title, targetHash)
|
|
|
|
|
|
|
|
activateLink(targetHash)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onScroll = throttleAndDebounce(setActiveLink, 300)
|
|
|
|
if (scrollTop < getAnchorTop(anchor)) {
|
|
|
|
onMounted(() => {
|
|
|
|
return [false, null]
|
|
|
|
setActiveLink()
|
|
|
|
}
|
|
|
|
window.addEventListener('scroll', onScroll)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onUpdated(() => {
|
|
|
|
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
|
|
|
|
// sidebar update means a route change
|
|
|
|
return [true, decodeURIComponent(anchor.hash)]
|
|
|
|
activateLink(decode(location.hash))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
return [false, null]
|
|
|
|
window.removeEventListener('scroll', onScroll)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|