vuepress-theme-hope
Version:
A light vuepress theme with tons of features
154 lines • 6.65 kB
JavaScript
import { useHeaders } from "@vuepress/helper/client";
import { useToggle, watchImmediate } from "@vueuse/core";
import { defineComponent, h, onMounted, ref, shallowRef, toRef } from "vue";
import { ClientOnly, RouteLink, useRoute } from "vuepress/client";
import PrintButton from "@theme-hope/modules/info/components/PrintButton";
import { useMetaLocale } from "@theme-hope/modules/info/composables/index";
import "../styles/toc.scss";
export default defineComponent({
name: "TOC",
props: {
/**
* TOC items config
*
* TOC 项目配置
*/
items: Array,
/**
* Header options
*/
options: Object,
},
slots: Object,
setup(props, { slots }) {
const headerOptions = toRef(props, "options");
const headers = useHeaders(headerOptions);
const route = useRoute();
const metaLocale = useMetaLocale();
const [isExpanded, toggleExpanded] = useToggle();
const toc = shallowRef();
const tocMarkerTop = ref("-2rem");
const scrollTo = (top) => {
toc.value?.scrollTo({ top, behavior: "smooth" });
};
const updateTocMarker = () => {
if (toc.value) {
const activeTocItem = document.querySelector(".vp-toc-item.active");
if (activeTocItem)
tocMarkerTop.value = `${
// Active toc item top
activeTocItem.getBoundingClientRect().top -
// Toc top
toc.value.getBoundingClientRect().top +
// Toc scroll top
toc.value.scrollTop}px`;
else
tocMarkerTop.value = "-2rem";
}
else {
tocMarkerTop.value = "-2rem";
}
};
onMounted(() => {
// Scroll to active toc item
watchImmediate(() => route.hash, (hash) => {
if (toc.value) {
// Get the active toc item DOM, whose href equals to the current route
const activeTocItem = document.querySelector(`#toc a.vp-toc-link[href$="${hash}"]`);
if (!activeTocItem)
return;
// Get the top and height of the toc
const { top: tocTop, height: tocHeight } = toc.value.getBoundingClientRect();
// Get the top and height of the active toc item
const { top: activeTocItemTop, height: activeTocItemHeight } = activeTocItem.getBoundingClientRect();
// When the active toc item overflows the top edge of toc
if (activeTocItemTop < tocTop)
// Scroll to the top edge of toc
scrollTo(toc.value.scrollTop + activeTocItemTop - tocTop);
// When the active toc item overflows the bottom edge of toc
else if (activeTocItemTop + activeTocItemHeight >
tocTop + tocHeight)
// Scroll to the bottom edge of toc
scrollTo(toc.value.scrollTop +
activeTocItemTop +
activeTocItemHeight -
tocTop -
tocHeight);
}
}, { flush: "post" });
watchImmediate(() => route.fullPath, updateTocMarker, {
flush: "post",
});
});
const renderHeader = ({ title, level, slug }) => h(RouteLink, {
to: `#${slug}`,
class: ["vp-toc-link", `level${level}`],
onClick: () => {
toggleExpanded();
},
}, () => title);
const renderChildren = (headers) => headers.length
? h("ul", { class: "vp-toc-list" }, headers.map((header) => {
const children = renderChildren(header.children);
return [
h("li", {
class: [
"vp-toc-item",
{ active: route.hash === `#${header.slug}` },
],
}, renderHeader(header)),
children ? h("li", children) : null,
];
}))
: null;
return () => {
const tocHeaders = props.items?.length
? renderChildren(props.items)
: renderChildren(headers.value);
const before = slots.before?.();
const after = slots.after?.();
return h(ClientOnly, () =>
// headers can not be accessed during SSR, so we need to wrap it with ClientOnly
tocHeaders || before || after
? h("div", { class: "vp-toc-placeholder" }, [
h("aside", { id: "toc", "vp-toc": "" }, [
before,
tocHeaders
? [
h("div", {
class: "vp-toc-header",
onClick: () => {
toggleExpanded();
},
}, [
metaLocale.value.toc,
h(PrintButton),
h("div", {
class: ["arrow", isExpanded.value ? "down" : "end"],
}),
]),
h("div", {
class: [
"vp-toc-wrapper",
isExpanded.value ? "open" : "",
],
ref: toc,
}, [
tocHeaders,
h("div", {
class: "vp-toc-marker",
style: {
top: tocMarkerTop.value,
},
}),
]),
]
: null,
after,
]),
])
: null);
};
},
});
//# sourceMappingURL=TOC.js.map