import { useDebounceFn, useEventListener } from '@vueuse/core'; import { useRouter } from 'vuepress/client'; /** * Update current hash and do not trigger `scrollBehavior` */ const updateHash = async (router, hash) => { const { path, query } = router.currentRoute.value; const { scrollBehavior } = router.options; // temporarily disable `scrollBehavior` router.options.scrollBehavior = undefined; await router.replace({ path, query, hash }); // restore it after navigation router.options.scrollBehavior = scrollBehavior; }; export const useActiveHeaderLinks = ({ headerLinkSelector, headerAnchorSelector, delay, offset = 5, }) => { const router = useRouter(); const setActiveRouteHash = () => { // get current scrollTop const scrollTop = Math.max(window.scrollY, document.documentElement.scrollTop, document.body.scrollTop); // check if we are at page top const isAtPageTop = Math.abs(scrollTop - 0) < offset; // replace current route hash with empty string when scrolling back to the top if (isAtPageTop) { updateHash(router, ''); return; } // get current scrollBottom const scrollBottom = window.innerHeight + scrollTop; // get the total scroll length of current page const scrollHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); // check if we have reached page bottom // notice the `scrollBottom` might not be exactly equal to `scrollHeight`, so we add offset here const isAtPageBottom = Math.abs(scrollHeight - scrollBottom) < offset; // get all header links const headerLinks = Array.from(document.querySelectorAll(headerLinkSelector)); // get all header anchors const headerAnchors = Array.from(document.querySelectorAll(headerAnchorSelector)); // filter anchors that do not have corresponding links const existedHeaderAnchors = headerAnchors.filter((anchor) => headerLinks.some((link) => link.hash === anchor.hash)); for (let i = 0; i < existedHeaderAnchors.length; i++) { const anchor = existedHeaderAnchors[i]; const nextAnchor = existedHeaderAnchors[i + 1]; // notice the `scrollTop` might not be exactly equal to `offsetTop` after clicking the anchor // so we add offset // if has scrolled past this anchor const hasPassedCurrentAnchor = scrollTop >= (anchor.parentElement?.offsetTop ?? 0) - offset; // if has not scrolled past next anchor const hasNotPassedNextAnchor = !nextAnchor || scrollTop < (nextAnchor.parentElement?.offsetTop ?? 0) - offset; // if this anchor is the active anchor const isActive = hasPassedCurrentAnchor && hasNotPassedNextAnchor; // continue to find the active anchor if (!isActive) continue; const routeHash = decodeURIComponent(router.currentRoute.value.hash); const anchorHash = decodeURIComponent(anchor.hash); // if the active anchor hash is current route hash, do nothing if (routeHash === anchorHash) return; // check if anchor is at the bottom of the page to keep hash consistent if (isAtPageBottom) { for (let j = i + 1; j < existedHeaderAnchors.length; j++) { // if current route hash is below the active hash, do nothing if (routeHash === decodeURIComponent(existedHeaderAnchors[j].hash)) { return; } } } // replace current route hash with the active anchor hash updateHash(router, anchorHash); return; } }; useEventListener('scroll', useDebounceFn(setActiveRouteHash, delay)); };