diff --git a/console/packages/editor/src/extensions/heading/index.ts b/console/packages/editor/src/extensions/heading/index.ts index 94190c1df7..3b0aa003f4 100644 --- a/console/packages/editor/src/extensions/heading/index.ts +++ b/console/packages/editor/src/extensions/heading/index.ts @@ -1,4 +1,4 @@ -import type { Editor, Range } from "@/tiptap/vue-3"; +import { mergeAttributes, type Editor, type Range } from "@/tiptap/vue-3"; import TiptapParagraph from "@/extensions/paragraph"; import TiptapHeading from "@tiptap/extension-heading"; import type { HeadingOptions } from "@tiptap/extension-heading"; @@ -15,8 +15,24 @@ import MdiFormatHeader6 from "~icons/mdi/format-header-6"; import { markRaw } from "vue"; import { i18n } from "@/locales"; import type { ExtensionOptions } from "@/types"; +import { Decoration, DecorationSet, Plugin, PluginKey } from "@/tiptap"; +import { ExtensionHeading } from ".."; +import { generateAnchor } from "@/utils"; const Blockquote = TiptapHeading.extend<ExtensionOptions & HeadingOptions>({ + renderHTML({ node, HTMLAttributes }) { + const hasLevel = this.options.levels.includes(node.attrs.level); + const level = hasLevel ? node.attrs.level : this.options.levels[0]; + const id = generateAnchor(node.textContent); + HTMLAttributes.id = id; + + return [ + `h${level}`, + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + 0, + ]; + }, + addOptions() { return { ...this.parent?.(), @@ -265,6 +281,32 @@ const Blockquote = TiptapHeading.extend<ExtensionOptions & HeadingOptions>({ addExtensions() { return [TiptapParagraph]; }, + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("generate-heading-id"), + props: { + decorations: (state) => { + const { doc } = state; + const decorations: Decoration[] = []; + doc.descendants((node, pos) => { + if (node.type.name === ExtensionHeading.name) { + const id = generateAnchor(node.textContent); + if (node.attrs.id !== id) { + decorations.push( + Decoration.node(pos, pos + node.nodeSize, { + id, + }) + ); + } + } + }); + return DecorationSet.create(doc, decorations); + }, + }, + }), + ]; + }, }); export default Blockquote; diff --git a/console/src/utils/anchor.ts b/console/packages/editor/src/utils/anchor.ts similarity index 100% rename from console/src/utils/anchor.ts rename to console/packages/editor/src/utils/anchor.ts diff --git a/console/packages/editor/src/utils/index.ts b/console/packages/editor/src/utils/index.ts index 0e86b17702..531a9ab321 100644 --- a/console/packages/editor/src/utils/index.ts +++ b/console/packages/editor/src/utils/index.ts @@ -1 +1,2 @@ export * from "./delete-node"; +export * from "./anchor"; diff --git a/console/src/components/editor/DefaultEditor.vue b/console/src/components/editor/DefaultEditor.vue index e19557b7bd..e58d538ff6 100644 --- a/console/src/components/editor/DefaultEditor.vue +++ b/console/src/components/editor/DefaultEditor.vue @@ -47,7 +47,6 @@ import { ToolbarItem, Plugin, PluginKey, - Decoration, DecorationSet, } from "@halo-dev/richtext-editor"; import { @@ -90,7 +89,6 @@ import type { PluginModule } from "@halo-dev/console-shared"; import { useDebounceFn, useLocalStorage } from "@vueuse/core"; import { onBeforeUnmount } from "vue"; import { usePermission } from "@/utils/permission"; -import { generateAnchor } from "@/utils/anchor"; const { t } = useI18n(); const { currentUserHasPermission } = usePermission(); @@ -295,27 +293,17 @@ onMounted(() => { addProseMirrorPlugins() { return [ new Plugin({ - key: new PluginKey("generate-heading-id"), + key: new PluginKey("get-heading-id"), props: { decorations: (state) => { const headings: HeadingNode[] = []; const { doc } = state; - const decorations: Decoration[] = []; - doc.descendants((node, pos) => { + doc.descendants((node) => { if (node.type.name === ExtensionHeading.name) { - const id = generateAnchor(node.textContent); - if (node.attrs.id !== id) { - decorations.push( - Decoration.node(pos, pos + node.nodeSize, { - id, - }) - ); - } - headings.push({ level: node.attrs.level, text: node.textContent, - id, + id: node.attrs.id, }); } }); @@ -323,7 +311,7 @@ onMounted(() => { if (!selectedHeadingNode.value) { selectedHeadingNode.value = headings[0]; } - return DecorationSet.create(doc, decorations); + return DecorationSet.empty; }, }, }), diff --git a/console/src/utils/__tests__/anchor.spec.ts b/console/src/utils/__tests__/anchor.spec.ts deleted file mode 100644 index 4d9f63492c..0000000000 --- a/console/src/utils/__tests__/anchor.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { generateAnchor } from "../anchor"; - -describe("generateAnchor", () => { - it("should handle basic text", () => { - expect(generateAnchor("Hello World")).toBe("hello-world"); - }); - - it("should trim whitespace", () => { - expect(generateAnchor(" Hello World ")).toBe("hello-world"); - }); - - it("should replace multiple spaces with a single dash", () => { - expect(generateAnchor("Hello World")).toBe("hello-world"); - }); - - it("should handle Chinese characters", () => { - expect(generateAnchor("你好")).toBe("%E4%BD%A0%E5%A5%BD"); - }); - - it("should handle special characters", () => { - expect(generateAnchor("Hello@#World$")).toBe("hello%40%23world%24"); - }); - - it("should handle empty string", () => { - expect(generateAnchor("")).toBe(""); - }); -});