active-bar.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { onMounted, onUnmounted, onUpdated } from 'vue'
  2. import { isClient } from '@vueuse/core'
  3. import { throttleAndDebounce } from '../utils'
  4. import type { Ref } from 'vue'
  5. export function useActiveSidebarLinks(
  6. container: Ref<HTMLElement>,
  7. marker: Ref<HTMLElement>
  8. ) {
  9. if (!isClient) return
  10. const onScroll = throttleAndDebounce(setActiveLink, 150)
  11. function setActiveLink() {
  12. const sidebarLinks = getSidebarLinks()
  13. const anchors = getAnchors(sidebarLinks)
  14. // Cancel the processing of the anchor point being forced to be the last one in the storefront
  15. // if (
  16. // anchors.length &&
  17. // scrollDom &&
  18. // scrollDom.scrollTop + scrollDom.clientHeight === scrollDom.scrollHeight
  19. // ) {
  20. // activateLink(anchors[anchors.length - 1].hash)
  21. // return
  22. // }
  23. for (let i = 0; i < anchors.length; i++) {
  24. const anchor = anchors[i]
  25. const nextAnchor = anchors[i + 1]
  26. const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
  27. if (isActive) {
  28. history.replaceState(
  29. null,
  30. document.title,
  31. hash ? (hash as string) : ' '
  32. )
  33. activateLink(hash as string)
  34. return
  35. }
  36. }
  37. }
  38. let prevActiveLink: HTMLAnchorElement | null = null
  39. function activateLink(hash: string) {
  40. deactiveLink(prevActiveLink)
  41. const activeLink = (prevActiveLink =
  42. hash == null
  43. ? null
  44. : (container.value.querySelector(
  45. `.toc-item a[href="${decodeURIComponent(hash)}"]`
  46. ) as HTMLAnchorElement))
  47. if (activeLink) {
  48. activeLink.classList.add('active')
  49. marker.value.style.opacity = '1'
  50. marker.value.style.top = `${activeLink.offsetTop}px`
  51. } else {
  52. marker.value.style.opacity = '0'
  53. marker.value.style.top = '33px'
  54. }
  55. }
  56. function deactiveLink(link: HTMLElement | null) {
  57. link && link.classList.remove('active')
  58. }
  59. onMounted(() => {
  60. window.requestAnimationFrame(setActiveLink)
  61. window.addEventListener('scroll', onScroll)
  62. })
  63. onUpdated(() => {
  64. activateLink(location.hash)
  65. })
  66. onUnmounted(() => {
  67. window.removeEventListener('scroll', onScroll)
  68. })
  69. }
  70. function getSidebarLinks() {
  71. return Array.from(
  72. document.querySelectorAll('.toc-content .toc-link')
  73. ) as HTMLAnchorElement[]
  74. }
  75. function getAnchors(sidebarLinks: HTMLAnchorElement[]) {
  76. return (
  77. Array.from(
  78. document.querySelectorAll('.doc-content .header-anchor')
  79. ) as HTMLAnchorElement[]
  80. ).filter((anchor) =>
  81. sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)
  82. )
  83. }
  84. function getPageOffset() {
  85. return (document.querySelector('.navbar') as HTMLElement).offsetHeight
  86. }
  87. function getAnchorTop(anchor: HTMLAnchorElement) {
  88. const pageOffset = getPageOffset()
  89. try {
  90. return anchor.parentElement!.offsetTop - pageOffset - 15
  91. } catch {
  92. return 0
  93. }
  94. }
  95. function isAnchorActive(
  96. index: number,
  97. anchor: HTMLAnchorElement,
  98. nextAnchor: HTMLAnchorElement
  99. ) {
  100. const scrollTop = window.scrollY
  101. if (index === 0 && scrollTop === 0) {
  102. return [true, null]
  103. }
  104. if (scrollTop < getAnchorTop(anchor)) {
  105. return [false, null]
  106. }
  107. if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
  108. return [true, decodeURIComponent(anchor.hash)]
  109. }
  110. return [false, null]
  111. }