active-bar.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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(container: Ref<HTMLElement>, marker: Ref<HTMLElement>) {
  6. if (!isClient) return
  7. const onScroll = throttleAndDebounce(setActiveLink, 150)
  8. function setActiveLink() {
  9. const sidebarLinks = getSidebarLinks()
  10. const anchors = getAnchors(sidebarLinks)
  11. // Cancel the processing of the anchor point being forced to be the last one in the storefront
  12. // if (
  13. // anchors.length &&
  14. // scrollDom &&
  15. // scrollDom.scrollTop + scrollDom.clientHeight === scrollDom.scrollHeight
  16. // ) {
  17. // activateLink(anchors[anchors.length - 1].hash)
  18. // return
  19. // }
  20. for (let i = 0; i < anchors.length; i++) {
  21. const anchor = anchors[i]
  22. const nextAnchor = anchors[i + 1]
  23. const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
  24. if (isActive) {
  25. history.replaceState(null, document.title, hash ? (hash as string) : ' ')
  26. activateLink(hash as string)
  27. return
  28. }
  29. }
  30. }
  31. let prevActiveLink: HTMLAnchorElement | null = null
  32. function activateLink(hash: string) {
  33. deactiveLink(prevActiveLink)
  34. const activeLink = (prevActiveLink = hash == null ? null : (container.value.querySelector(`.toc-item a[href="${decodeURIComponent(hash)}"]`) as HTMLAnchorElement))
  35. if (activeLink) {
  36. activeLink.classList.add('active')
  37. marker.value.style.opacity = '1'
  38. marker.value.style.top = `${activeLink.offsetTop}px`
  39. } else {
  40. marker.value.style.opacity = '0'
  41. marker.value.style.top = '33px'
  42. }
  43. }
  44. function deactiveLink(link: HTMLElement | null) {
  45. link && link.classList.remove('active')
  46. }
  47. onMounted(() => {
  48. window.requestAnimationFrame(setActiveLink)
  49. window.addEventListener('scroll', onScroll)
  50. })
  51. onUpdated(() => {
  52. activateLink(location.hash)
  53. })
  54. onUnmounted(() => {
  55. window.removeEventListener('scroll', onScroll)
  56. })
  57. }
  58. function getSidebarLinks() {
  59. return Array.from(document.querySelectorAll('.toc-content .toc-link')) as HTMLAnchorElement[]
  60. }
  61. function getAnchors(sidebarLinks: HTMLAnchorElement[]) {
  62. return (Array.from(document.querySelectorAll('.doc-content .header-anchor')) as HTMLAnchorElement[]).filter(anchor => sidebarLinks.some(sidebarLink => sidebarLink.hash === anchor.hash))
  63. }
  64. function getPageOffset() {
  65. return (document.querySelector('.navbar') as HTMLElement).offsetHeight
  66. }
  67. function getAnchorTop(anchor: HTMLAnchorElement) {
  68. const pageOffset = getPageOffset()
  69. try {
  70. return anchor.parentElement!.offsetTop - pageOffset - 15
  71. } catch {
  72. return 0
  73. }
  74. }
  75. function isAnchorActive(index: number, anchor: HTMLAnchorElement, nextAnchor: HTMLAnchorElement) {
  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. }