valaxy-theme-sakura
Version:
<h1 align="center">valaxy-theme-sakura</h1> <pre align="center"> 一个简单、个性化、可爱的动漫风格博客主题 ❥(ゝω・✿ฺ) </pre>
144 lines (114 loc) • 3.74 kB
text/typescript
import type { Ref } from 'vue'
import { throttleAndDebounce } from 'valaxy'
import { onMounted, onUnmounted, onUpdated } from 'vue'
import { useAside } from './aside'
// magic number to avoid repeated retrieval
const PAGE_OFFSET = 56
const topOffset = 33
export function useActiveAnchor(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>,
enableScrollIntoView = false,
) {
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 = container.value.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash)
// activateLink(null)
return
}
// isTop
// if (anchors.length && scrollY === 0)
// activateLink('#')
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
}
}
}
const checkActiveLinkInViewport = () => {
const activeLink = prevActiveLink
if (!activeLink) {
// container.value.scrollIntoView({ behavior: 'smooth', block: 'start' })
return
}
if (enableScrollIntoView)
return
const top = activeLink.getBoundingClientRect().top
const bottom = activeLink.getBoundingClientRect().bottom
if (top < topOffset || bottom > window.innerHeight - topOffset)
activeLink.scrollIntoView()
}
function activateLink(hash: string | null) {
if (prevActiveLink)
prevActiveLink.classList.remove('active')
if (hash == null) {
prevActiveLink = null
}
else {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`,
)
}
const activeLink = prevActiveLink
checkActiveLinkInViewport()
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = `${activeLink.offsetTop + topOffset}px`
marker.value.style.opacity = '1'
}
else {
marker.value.style.top = `${topOffset}px`
marker.value.style.opacity = '0'
}
}
}
function getAnchorTop(anchor: HTMLAnchorElement): number {
return anchor.parentElement!.offsetTop - PAGE_OFFSET - 15
}
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]
}