2021-03-01 15:06:11 +08:00

79 lines
2.8 KiB
JavaScript

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);
}
};
}