activeSidebarLink.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { onMounted, onUnmounted, onUpdated } from 'vue'
  2. export function useActiveSidebarLinks() {
  3. let rootActiveLink: HTMLAnchorElement | null = null
  4. let activeLink: HTMLAnchorElement | null = null
  5. const onScroll = throttleAndDebounce(setActiveLink, 300)
  6. function setActiveLink(): void {
  7. const sidebarLinks = getSidebarLinks()
  8. const anchors = getAnchors(sidebarLinks)
  9. for (let i = 0; i < anchors.length; i++) {
  10. const anchor = anchors[i]
  11. const nextAnchor = anchors[i + 1]
  12. const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
  13. if (isActive) {
  14. history.replaceState(null, document.title, hash ? hash : ' ')
  15. activateLink(hash)
  16. return
  17. }
  18. }
  19. }
  20. function activateLink(hash: string | null): void {
  21. deactiveLink(activeLink)
  22. deactiveLink(rootActiveLink)
  23. activeLink = document.querySelector(`.sidebar a[href="${hash}"]`)
  24. if (!activeLink) {
  25. return
  26. }
  27. activeLink.classList.add('active')
  28. // also add active class to parent h2 anchors
  29. const rootLi = activeLink.closest('.sidebar-links > ul > li')
  30. if (rootLi && rootLi !== activeLink.parentElement) {
  31. rootActiveLink = rootLi.querySelector('a')
  32. rootActiveLink && rootActiveLink.classList.add('active')
  33. } else {
  34. rootActiveLink = null
  35. }
  36. }
  37. function deactiveLink(link: HTMLAnchorElement | null): void {
  38. link && link.classList.remove('active')
  39. }
  40. onMounted(() => {
  41. setActiveLink()
  42. window.addEventListener('scroll', onScroll)
  43. })
  44. onUpdated(() => {
  45. // sidebar update means a route change
  46. activateLink(decodeURIComponent(location.hash))
  47. })
  48. onUnmounted(() => {
  49. window.removeEventListener('scroll', onScroll)
  50. })
  51. }
  52. function getSidebarLinks(): HTMLAnchorElement[] {
  53. return [].slice.call(
  54. document.querySelectorAll('.sidebar a.sidebar-link-item')
  55. )
  56. }
  57. function getAnchors(sidebarLinks: HTMLAnchorElement[]): HTMLAnchorElement[] {
  58. return [].slice
  59. .call(document.querySelectorAll('.header-anchor'))
  60. .filter((anchor: HTMLAnchorElement) =>
  61. sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
  62. ) as HTMLAnchorElement[]
  63. }
  64. function getPageOffset(): number {
  65. return (document.querySelector('.nav-bar') as HTMLElement).offsetHeight
  66. }
  67. function getAnchorTop(anchor: HTMLAnchorElement): number {
  68. const pageOffset = getPageOffset()
  69. return anchor.parentElement!.offsetTop - pageOffset - 15
  70. }
  71. function isAnchorActive(
  72. index: number,
  73. anchor: HTMLAnchorElement,
  74. nextAnchor: HTMLAnchorElement
  75. ): [boolean, string | null] {
  76. const scrollTop = window.scrollY
  77. if (index === 0 && scrollTop === 0) {
  78. return [true, null]
  79. }
  80. if (scrollTop < getAnchorTop(anchor)) {
  81. return [false, null]
  82. }
  83. if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
  84. return [true, decodeURIComponent(anchor.hash)]
  85. }
  86. return [false, null]
  87. }
  88. function throttleAndDebounce(fn: () => void, delay: number): () => void {
  89. let timeout: number
  90. let called = false
  91. return () => {
  92. if (timeout) {
  93. clearTimeout(timeout)
  94. }
  95. if (!called) {
  96. fn()
  97. called = true
  98. setTimeout(() => {
  99. called = false
  100. }, delay)
  101. } else {
  102. timeout = setTimeout(fn, delay)
  103. }
  104. }
  105. }