Skip to content

Commit

Permalink
fix: fix sidebar active status not working as expected (#140) (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiaking authored Nov 24, 2020
1 parent a28c737 commit 0b181e7
Showing 1 changed file with 86 additions and 48 deletions.
134 changes: 86 additions & 48 deletions src/client/theme-default/composables/activeSidebarLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,86 +3,124 @@ 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)
})

onUpdated(() => {
// sidebar update means a route change
activateLink(decode(location.hash))
activateLink(decodeURIComponent(location.hash))
})

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'))
.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
Expand Down

0 comments on commit 0b181e7

Please sign in to comment.