| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364 |
- import { useEffect, useState } from 'react'
- /**
- * Find the active heading of page
- *
- * It selects the top heading by default, and the last item when reached the bottom of page.
- *
- * @param watch - An array of element ids to watch
- * @param single - only one active item at most
- * @returns Active anchor
- */
- export function useAnchorObserver(watch: string[], single: boolean): string[] {
- const [activeAnchor, setActiveAnchor] = useState<string[]>([])
- useEffect(() => {
- let visible: string[] = []
- const observer = new IntersectionObserver(
- (entries) => {
- for (const entry of entries) {
- if (entry.isIntersecting && !visible.includes(entry.target.id)) {
- visible = [...visible, entry.target.id]
- } else if (!entry.isIntersecting && visible.includes(entry.target.id)) {
- visible = visible.filter((v) => v !== entry.target.id)
- }
- }
- if (visible.length > 0) setActiveAnchor(visible)
- },
- {
- rootMargin: single ? '-80px 0% -70% 0%' : `-20px 0% -40% 0%`,
- threshold: 1,
- }
- )
- function onScroll(): void {
- const element = document.scrollingElement
- if (!element) return
- if (element.scrollTop === 0 && single) setActiveAnchor(watch.slice(0, 1))
- else if (element.scrollTop + element.clientHeight >= element.scrollHeight - 6) {
- setActiveAnchor((active) => {
- return active.length > 0 && !single
- ? watch.slice(watch.indexOf(active[0]))
- : watch.slice(-1)
- })
- }
- }
- for (const heading of watch) {
- const element = document.getElementById(heading)
- if (element) observer.observe(element)
- }
- onScroll()
- window.addEventListener('scroll', onScroll)
- return () => {
- window.removeEventListener('scroll', onScroll)
- observer.disconnect()
- }
- }, [single, watch])
- return single ? activeAnchor.slice(0, 1) : activeAnchor
- }
|