@neosjs/vitepress-theme
Version:
NeosJS VitePress theme
144 lines (143 loc) • 4.5 kB
JavaScript
import { inBrowser } from "vitepress";
import { onMounted, onUnmounted, onUpdated } from "vue";
import { throttleAndDebounce } from "../support/utils.mjs";
import { useAside } from "./aside.mjs";
const PAGE_OFFSET = 71;
export function resolveTitle(theme) {
return typeof theme.outline === "object" && !Array.isArray(theme.outline) && theme.outline.label || theme.outlineTitle || "On this page";
}
export function getHeaders(range) {
const headers = [
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
const level = Number(el.tagName[1]);
return {
title: serializeHeader(el),
link: `#${el.id}`,
level
};
});
return resolveHeaders(headers, range);
}
function serializeHeader(h) {
let ret = "";
for (const node of h.childNodes) {
if (node.nodeType === 1) {
if (node.classList.contains("VPBadge") || node.classList.contains("header-anchor")) {
continue;
}
ret += node.textContent;
} else if (node.nodeType === 3) {
ret += node.textContent;
}
}
return ret.trim();
}
export function resolveHeaders(headers, range) {
if (range === false) {
return [];
}
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
headers = headers.filter((h) => h.level >= high && h.level <= low);
const ret = [];
outer: for (let i = 0; i < headers.length; i++) {
const cur = headers[i];
if (i === 0) {
ret.push(cur);
} else {
for (let j = i - 1; j >= 0; j--) {
const prev = headers[j];
if (prev.level < cur.level) {
;
(prev.children || (prev.children = [])).push(cur);
continue outer;
}
}
ret.push(cur);
}
}
return ret;
}
export function useActiveAnchor(container, marker) {
const { isAsideEnabled } = useAside();
const onScroll = throttleAndDebounce(setActiveLink, 100);
let prevActiveLink = null;
onMounted(() => {
requestAnimationFrame(setActiveLink);
inBrowser && window.addEventListener("scroll", onScroll);
});
onUpdated(() => {
activateLink(location.hash);
});
onUnmounted(() => {
inBrowser && window.removeEventListener("scroll", onScroll);
});
function setActiveLink() {
if (!isAsideEnabled.value || !inBrowser) {
return;
}
const links = [].slice.call(
container.value.querySelectorAll(".outline-link")
);
const anchors = [].slice.call(document.querySelectorAll(".content .header-anchor")).filter((anchor) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null;
});
});
const scrollY = window.scrollY;
const innerHeight = window.innerHeight;
const offsetHeight = document.body.offsetHeight;
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash);
return;
}
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i];
const nextAnchor = anchors[i + 1];
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor);
if (isActive) {
activateLink(hash);
return;
}
}
}
function activateLink(hash) {
if (prevActiveLink) {
prevActiveLink.classList.remove("active");
}
if (hash == null) {
prevActiveLink = null;
} else {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`
);
}
const activeLink = prevActiveLink;
if (activeLink) {
activeLink.classList.add("active");
marker.value.style.top = `${activeLink.offsetTop + 33}px`;
marker.value.style.opacity = "1";
} else {
marker.value.style.top = "33px";
marker.value.style.opacity = "0";
}
}
}
function getAnchorTop(anchor) {
return anchor.parentElement.offsetTop - PAGE_OFFSET;
}
function isAnchorActive(index, anchor, nextAnchor) {
const scrollTop = window.scrollY;
if (index === 0 && scrollTop === 0) {
return [true, null];
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null];
}
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, anchor.hash];
}
return [false, null];
}