import { onMounted, onUnmounted, onUpdated } from 'vue'; export function useActiveSidebarLinks() { let rootActiveLink = null; let activeLink = null; const decode = decodeURIComponent; const deactiveLink = (link) => link && link.classList.remove('active'); const activateLink = (hash) => { deactiveLink(activeLink); deactiveLink(rootActiveLink); activeLink = document.querySelector(`.sidebar a[href="${hash}"]`); if (activeLink) { activeLink.classList.add('active'); // also add active class to parent h2 anchors const rootLi = activeLink.closest('.sidebar > ul > li'); if (rootLi && rootLi !== activeLink.parentElement) { rootActiveLink = rootLi.querySelector('a'); rootActiveLink && rootActiveLink.classList.add('active'); } else { rootActiveLink = null; } } }; const setActiveLink = () => { const sidebarLinks = [].slice.call(document.querySelectorAll('.sidebar a')); const anchors = [].slice .call(document.querySelectorAll('.header-anchor')) .filter((anchor) => sidebarLinks.some((sidebarLink) => sidebarLink.hash === anchor.hash)); const pageOffset = document.querySelector('.navbar') .offsetHeight; const scrollTop = window.scrollY; const getAnchorTop = (anchor) => anchor.parentElement.offsetTop - pageOffset - 15; for (let i = 0; i < anchors.length; i++) { const anchor = anchors[i]; const nextAnchor = anchors[i + 1]; const isActive = (i === 0 && scrollTop === 0) || (scrollTop >= getAnchorTop(anchor) && (!nextAnchor || scrollTop < getAnchorTop(nextAnchor))); // TODO: fix case when at page bottom if (isActive) { const targetHash = decode(anchor.hash); history.replaceState(null, document.title, targetHash); activateLink(targetHash); return; } } }; const onScroll = throttleAndDebounce(setActiveLink, 300); onMounted(() => { setActiveLink(); window.addEventListener('scroll', onScroll); }); onUpdated(() => { // sidebar update means a route change activateLink(decode(location.hash)); }); onUnmounted(() => { window.removeEventListener('scroll', onScroll); }); } function throttleAndDebounce(fn, delay) { let timeout; let called = false; return () => { if (timeout) clearTimeout(timeout); if (!called) { fn(); called = true; setTimeout(() => { called = false; }, delay); } else { timeout = setTimeout(fn, delay); } }; }