vuepress-theme-hope
Version:
A light vuepress theme with tons of features
166 lines • 7.3 kB
JavaScript
import { isPlainObject, isSlotContentEmpty, useHeaders } from "@vuepress/helper/client";
import { useToggle, watchImmediate } from "@vueuse/core";
import { computed, defineComponent, h, onMounted, ref, shallowRef } from "vue";
import { ClientOnly, RouteLink, useRoute } from "vuepress/client";
import PrintButton from "@theme-hope/components/base/PrintButton";
import { useMetaLocale } from "@theme-hope/composables/info/useMetaLocale";
import { useData } from "@theme-hope/composables/useData";
import "../../styles/info/toc.scss";
const DEFAULT_TOC_OPTIONS = {
selector: [
...Array.from({ length: 6 }).map((_, i) => `#markdown-content > h${i + 1}`),
"[vp-content] > h2",
].join(", "),
levels: "deep",
ignore: [".vp-badge", ".vp-icon"],
};
export default defineComponent({
name: "TOC",
props: {
/**
* TOC items config
*
* TOC 项目配置
*/
items: Array,
},
slots: Object,
setup(props, { slots }) {
const { frontmatter, themeLocale } = useData();
const tocOptions = computed(() => {
const config = frontmatter.value.toc ?? themeLocale.value.toc;
return isPlainObject(config)
? { ...DEFAULT_TOC_OPTIONS, ...config }
: (config ?? true)
? DEFAULT_TOC_OPTIONS
: // oxlint-disable-next-line no-undefined
undefined;
});
const headers = useHeaders(tocOptions);
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 = (pageHeaders) => pageHeaders.length > 0
? h("ul", { class: "vp-toc-list" }, pageHeaders.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 () => tocOptions.value || props.items?.length
? h(ClientOnly, () => {
const tocHeaders = props.items?.length
? renderChildren(props.items)
: renderChildren(headers.value);
const defaultContent = slots.toc?.(headers.value) ??
(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);
const beforeContent = slots.tocBefore?.();
const afterContent = slots.tocAfter?.();
const isTOCEmpty = isSlotContentEmpty(defaultContent) &&
isSlotContentEmpty(beforeContent) &&
isSlotContentEmpty(afterContent);
return isTOCEmpty
? null
: h("div", { class: "vp-toc-placeholder" }, [
h("aside", { id: "toc", "vp-toc": "" }, [
beforeContent,
defaultContent,
afterContent,
]),
]);
})
: null;
},
});
//# sourceMappingURL=TOC.js.map