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 PostPreviewModal from "./components/PostPreviewModal.vue";
import AttachmentSelectorModal from "../attachments/components/AttachmentSelectorModal.vue"; import AttachmentSelectorModal from "../attachments/components/AttachmentSelectorModal.vue";
import type { PostRequest } from "@halo-dev/api-client"; 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 cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { import {
allExtensions, allExtensions,
Extension,
RichTextEditor, RichTextEditor,
useEditor, useEditor,
} from "@halo-dev/richtext-editor"; } from "@halo-dev/richtext-editor";
@ -30,6 +31,12 @@ import ExtensionCharacterCount from "@tiptap/extension-character-count";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { useAttachmentSelect } from "../attachments/composables/use-attachment"; import { useAttachmentSelect } from "../attachments/composables/use-attachment";
import MdiFileImageBox from "~icons/mdi/file-image-box"; 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 = { const initialFormState: PostRequest = {
post: { post: {
@ -78,16 +85,43 @@ const isUpdateMode = computed(() => {
return !!formState.value.post.metadata.creationTimestamp; return !!formState.value.post.metadata.creationTimestamp;
}); });
interface TocNode { interface HeadingNode {
id: string; id: string;
level: string; level: number;
text: string; 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({ const editor = useEditor({
content: formState.value.content.raw, content: formState.value.content.raw,
extensions: [...allExtensions, ExtensionCharacterCount], extensions: [
...allExtensions,
ExtensionCharacterCount,
Extension.create({
addGlobalAttributes() {
return [
{
types: ["heading"],
attributes: {
id: {
default: null,
},
},
},
];
},
}),
],
autofocus: "start", autofocus: "start",
onUpdate: () => { onUpdate: () => {
formState.value.content.raw = editor.value?.getHTML() + ""; formState.value.content.raw = editor.value?.getHTML() + "";
@ -115,7 +149,7 @@ const handleGenerateTableOfContent = () => {
return; return;
} }
const headings: TocNode[] = []; const headings: HeadingNode[] = [];
const transaction = editor.value.state.tr; const transaction = editor.value.state.tr;
editor.value.state.doc.descendants((node, pos) => { editor.value.state.doc.descendants((node, pos) => {
@ -142,7 +176,16 @@ const handleGenerateTableOfContent = () => {
editor.value.view.dispatch(transaction); 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 () => { const handleSave = async () => {
@ -274,12 +317,29 @@ onMounted(async () => {
<div class="p-1 pt-0"> <div class="p-1 pt-0">
<ul class="space-y-1"> <ul class="space-y-1">
<li <li
v-for="(item, index) in toc" v-for="(node, index) in headingNodes"
:key="index" :key="index"
:class="[{ 'bg-gray-100': index === 0 }]" :class="[
class="cursor-pointer rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900" { '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> </li>
</ul> </ul>
</div> </div>

Loading…
Cancel
Save