2023-12-27 02:18:19 +08:00

136 lines
3.3 KiB
TypeScript

import { useMediaQuery } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, onUpdated } from 'vue'
import { throttleAndDebounce } from '../utils/index.js'
import { useSidebar } from './sidebar.js'
const PAGE_OFFSET = 71
export function useAside() {
const { hasSidebar } = useSidebar()
const is960 = useMediaQuery('(min-width: 960px)')
const is1280 = useMediaQuery('(min-width: 1280px)')
const isAsideEnabled = computed(() => {
if (!is1280.value && !is960.value)
return false
return hasSidebar.value ? is1280.value : is960.value
})
return {
isAsideEnabled,
}
}
export function useActiveAnchor(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>,
) {
const { isAsideEnabled } = useAside()
const onScroll = throttleAndDebounce(setActiveLink, 100)
let prevActiveLink: HTMLAnchorElement | null = null
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
function setActiveLink() {
if (!isAsideEnabled.value)
return
const links = [].slice.call(
container.value.querySelectorAll('.outline-link'),
) as HTMLAnchorElement[]
const anchors = [].slice
.call(document.querySelectorAll('.content .header-anchor'))
.filter((anchor: HTMLAnchorElement) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null
})
}) as HTMLAnchorElement[]
const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash)
return
}
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) {
activateLink(hash)
return
}
}
}
function activateLink(hash: string | null) {
if (prevActiveLink)
prevActiveLink.classList.remove('active')
if (hash !== null) {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`,
)
}
const activeLink = prevActiveLink
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = `${activeLink.offsetTop + 33}px`
marker.value.style.opacity = '1'
}
else {
marker.value.style.top = '33px'
marker.value.style.opacity = '0'
}
}
}
function getAnchorTop(anchor: HTMLAnchorElement): number {
return anchor.parentElement!.offsetTop - PAGE_OFFSET
}
function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement | undefined,
): [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, anchor.hash]
return [false, null]
}