diff --git a/src/client/theme-default/composables/activeSidebarLink.ts b/src/client/theme-default/composables/activeSidebarLink.ts index 2af89ed3..de98c7a0 100644 --- a/src/client/theme-default/composables/activeSidebarLink.ts +++ b/src/client/theme-default/composables/activeSidebarLink.ts @@ -3,66 +3,54 @@ 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.querySelector('.navbar') as HTMLElement) - .offsetHeight - const scrollTop = window.scrollY + const onScroll = throttleAndDebounce(setActiveLink, 300) - const getAnchorTop = (anchor: HTMLAnchorElement): number => - anchor.parentElement!.offsetTop - pageOffset - 15 + function setActiveLink(): void { + const sidebarLinks = getSidebarLinks() + const anchors = getAnchors(sidebarLinks) 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))) - // TODO: fix case when at page bottom + const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor) if (isActive) { - const targetHash = decode(anchor.hash) - history.replaceState(null, document.title, targetHash) - activateLink(targetHash) + history.replaceState(null, document.title, hash ? hash : ' ') + activateLink(hash) 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(() => { setActiveLink() window.addEventListener('scroll', onScroll) @@ -70,7 +58,7 @@ export function useActiveSidebarLinks() { onUpdated(() => { // sidebar update means a route change - activateLink(decode(location.hash)) + activateLink(decodeURIComponent(location.hash)) }) 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 { let timeout: NodeJS.Timeout let called = false + return () => { - if (timeout) clearTimeout(timeout) + if (timeout) { + clearTimeout(timeout) + } + if (!called) { fn() called = true