useAnchorObserver.ts64 lines · main
| 1 | import { useEffect, useState } from 'react' |
| 2 | |
| 3 | /** |
| 4 | * Find the active heading of page |
| 5 | * |
| 6 | * It selects the top heading by default, and the last item when reached the bottom of page. |
| 7 | * |
| 8 | * @param watch - An array of element ids to watch |
| 9 | * @param single - only one active item at most |
| 10 | * @returns Active anchor |
| 11 | */ |
| 12 | export function useAnchorObserver(watch: string[], single: boolean): string[] { |
| 13 | const [activeAnchor, setActiveAnchor] = useState<string[]>([]) |
| 14 | |
| 15 | useEffect(() => { |
| 16 | let visible: string[] = [] |
| 17 | const observer = new IntersectionObserver( |
| 18 | (entries) => { |
| 19 | for (const entry of entries) { |
| 20 | if (entry.isIntersecting && !visible.includes(entry.target.id)) { |
| 21 | visible = [...visible, entry.target.id] |
| 22 | } else if (!entry.isIntersecting && visible.includes(entry.target.id)) { |
| 23 | visible = visible.filter((v) => v !== entry.target.id) |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | if (visible.length > 0) setActiveAnchor(visible) |
| 28 | }, |
| 29 | { |
| 30 | rootMargin: single ? '-80px 0% -70% 0%' : `-20px 0% -40% 0%`, |
| 31 | threshold: 1, |
| 32 | } |
| 33 | ) |
| 34 | |
| 35 | function onScroll(): void { |
| 36 | const element = document.scrollingElement |
| 37 | if (!element) return |
| 38 | |
| 39 | if (element.scrollTop === 0 && single) setActiveAnchor(watch.slice(0, 1)) |
| 40 | else if (element.scrollTop + element.clientHeight >= element.scrollHeight - 6) { |
| 41 | setActiveAnchor((active) => { |
| 42 | return active.length > 0 && !single |
| 43 | ? watch.slice(watch.indexOf(active[0])) |
| 44 | : watch.slice(-1) |
| 45 | }) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | for (const heading of watch) { |
| 50 | const element = document.getElementById(heading) |
| 51 | |
| 52 | if (element) observer.observe(element) |
| 53 | } |
| 54 | |
| 55 | onScroll() |
| 56 | window.addEventListener('scroll', onScroll) |
| 57 | return () => { |
| 58 | window.removeEventListener('scroll', onScroll) |
| 59 | observer.disconnect() |
| 60 | } |
| 61 | }, [single, watch]) |
| 62 | |
| 63 | return single ? activeAnchor.slice(0, 1) : activeAnchor |
| 64 | } |