76 lines
3.8 KiB
JavaScript
76 lines
3.8 KiB
JavaScript
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));
|
|
};
|