activeSidebarLink.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { onMounted, onUnmounted, onUpdated } from 'vue';
  2. export function useActiveSidebarLinks() {
  3. let rootActiveLink = null;
  4. let activeLink = null;
  5. const onScroll = throttleAndDebounce(setActiveLink, 300);
  6. function setActiveLink() {
  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) {
  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. }
  34. else {
  35. rootActiveLink = null;
  36. }
  37. }
  38. function deactiveLink(link) {
  39. link && link.classList.remove('active');
  40. }
  41. onMounted(() => {
  42. setActiveLink();
  43. window.addEventListener('scroll', onScroll);
  44. });
  45. onUpdated(() => {
  46. // sidebar update means a route change
  47. activateLink(decodeURIComponent(location.hash));
  48. });
  49. onUnmounted(() => {
  50. window.removeEventListener('scroll', onScroll);
  51. });
  52. }
  53. function getSidebarLinks() {
  54. return [].slice.call(document.querySelectorAll('.sidebar a.sidebar-link-item'));
  55. }
  56. function getAnchors(sidebarLinks) {
  57. return [].slice
  58. .call(document.querySelectorAll('.header-anchor'))
  59. .filter((anchor) => sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash));
  60. }
  61. function getPageOffset() {
  62. return document.querySelector('.nav-bar').offsetHeight;
  63. }
  64. function getAnchorTop(anchor) {
  65. const pageOffset = getPageOffset();
  66. return anchor.parentElement.offsetTop - pageOffset - 15;
  67. }
  68. function isAnchorActive(index, anchor, nextAnchor) {
  69. const scrollTop = window.scrollY;
  70. if (index === 0 && scrollTop === 0) {
  71. return [true, null];
  72. }
  73. if (scrollTop < getAnchorTop(anchor)) {
  74. return [false, null];
  75. }
  76. if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
  77. return [true, decodeURIComponent(anchor.hash)];
  78. }
  79. return [false, null];
  80. }
  81. function throttleAndDebounce(fn, delay) {
  82. let timeout;
  83. let called = false;
  84. return () => {
  85. if (timeout) {
  86. clearTimeout(timeout);
  87. }
  88. if (!called) {
  89. fn();
  90. called = true;
  91. setTimeout(() => {
  92. called = false;
  93. }, delay);
  94. }
  95. else {
  96. timeout = setTimeout(fn, delay);
  97. }
  98. };
  99. }