useAnchorObserver.ts 1.9 KB

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