perf: improve the table of content in the post editor

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/622/head
Ryan Wang 2 years ago
parent 9d020c073e
commit 831e3fd6ca

@ -16,13 +16,14 @@ import PostSettingModal from "./components/PostSettingModal.vue";
import PostPreviewModal from "./components/PostPreviewModal.vue";
import AttachmentSelectorModal from "../attachments/components/AttachmentSelectorModal.vue";
import type { PostRequest } from "@halo-dev/api-client";
import { computed, onMounted, ref, watch } from "vue";
import { computed, markRaw, onMounted, ref, watch } from "vue";
import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router";
import { v4 as uuid } from "uuid";
import {
allExtensions,
Extension,
RichTextEditor,
useEditor,
} from "@halo-dev/richtext-editor";
@ -30,6 +31,12 @@ import ExtensionCharacterCount from "@tiptap/extension-character-count";
import { formatDatetime } from "@/utils/date";
import { useAttachmentSelect } from "../attachments/composables/use-attachment";
import MdiFileImageBox from "~icons/mdi/file-image-box";
import MdiFormatHeader1 from "~icons/mdi/format-header-1";
import MdiFormatHeader2 from "~icons/mdi/format-header-2";
import MdiFormatHeader3 from "~icons/mdi/format-header-3";
import MdiFormatHeader4 from "~icons/mdi/format-header-4";
import MdiFormatHeader5 from "~icons/mdi/format-header-5";
import MdiFormatHeader6 from "~icons/mdi/format-header-6";
const initialFormState: PostRequest = {
post: {
@ -78,16 +85,43 @@ const isUpdateMode = computed(() => {
return !!formState.value.post.metadata.creationTimestamp;
});
interface TocNode {
interface HeadingNode {
id: string;
level: string;
level: number;
text: string;
}
const toc = ref<TocNode[]>();
const headingIcons = {
1: markRaw(MdiFormatHeader1),
2: markRaw(MdiFormatHeader2),
3: markRaw(MdiFormatHeader3),
4: markRaw(MdiFormatHeader4),
5: markRaw(MdiFormatHeader5),
6: markRaw(MdiFormatHeader6),
};
const headingNodes = ref<HeadingNode[]>();
const selectedHeadingNode = ref<HeadingNode>();
const editor = useEditor({
content: formState.value.content.raw,
extensions: [...allExtensions, ExtensionCharacterCount],
extensions: [
...allExtensions,
ExtensionCharacterCount,
Extension.create({
addGlobalAttributes() {
return [
{
types: ["heading"],
attributes: {
id: {
default: null,
},
},
},
];
},
}),
],
autofocus: "start",
onUpdate: () => {
formState.value.content.raw = editor.value?.getHTML() + "";
@ -115,7 +149,7 @@ const handleGenerateTableOfContent = () => {
return;
}
const headings: TocNode[] = [];
const headings: HeadingNode[] = [];
const transaction = editor.value.state.tr;
editor.value.state.doc.descendants((node, pos) => {
@ -142,7 +176,16 @@ const handleGenerateTableOfContent = () => {
editor.value.view.dispatch(transaction);
toc.value = headings;
headingNodes.value = headings;
if (!selectedHeadingNode.value) {
selectedHeadingNode.value = headings[0];
}
};
const handleSelectHeadingNode = (node: HeadingNode) => {
selectedHeadingNode.value = node;
document.getElementById(node.id)?.scrollIntoView({ behavior: "smooth" });
};
const handleSave = async () => {
@ -274,12 +317,29 @@ onMounted(async () => {
<div class="p-1 pt-0">
<ul class="space-y-1">
<li
v-for="(item, index) in toc"
v-for="(node, index) in headingNodes"
:key="index"
:class="[{ 'bg-gray-100': index === 0 }]"
class="cursor-pointer rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
:class="[
{ 'bg-gray-100': node.id === selectedHeadingNode?.id },
]"
class="group cursor-pointer truncate rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handleSelectHeadingNode(node)"
>
{{ item.text }}
<div
:style="{
paddingLeft: `${(node.level - 1) * 0.8}rem`,
}"
class="flex items-center gap-2"
>
<component
:is="headingIcons[node.level]"
class="h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white"
:class="[
{ '!bg-white': node.id === selectedHeadingNode?.id },
]"
/>
<span class="flex-1 truncate">{{ node.text }}</span>
</div>
</li>
</ul>
</div>

Loading…
Cancel
Save