136 lines
3.3 KiB
TypeScript
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]
|
|
}
|