From 1f5bef71ac255eef967b5c6faac77efe92151902 Mon Sep 17 00:00:00 2001 From: Takagi <1103069291@qq.com> Date: Thu, 7 Dec 2023 11:42:07 +0800 Subject: [PATCH] fix: resolve the issue of generated titles missing id (#4997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area console /milestone 2.12.x #### What this PR does / why we need it: 在 #4975 中,使用 `decorations` 重写了 heading,但由于 `decorations` 只会给 nodeview 增加 id,因此会导致 render 后的 html 中不存在 id。 默认编辑器 heading 渲染时,自动增加 id。 #### How to test it? 使用默认编辑器编辑时,增加 Heading。保存后查看主题端对应的 heading 是否存在 id。 #### Which issue(s) this PR fixes: Fixes #4994 #### Does this PR introduce a user-facing change? ```release-note 解决渲染后的 Heading 中不存在 id 的问题。 ``` --- .../editor/src/extensions/heading/index.ts | 44 ++++++++++++++++++- .../{ => packages/editor}/src/utils/anchor.ts | 0 console/packages/editor/src/utils/index.ts | 1 + .../src/components/editor/DefaultEditor.vue | 20 ++------- console/src/utils/__tests__/anchor.spec.ts | 28 ------------ 5 files changed, 48 insertions(+), 45 deletions(-) rename console/{ => packages/editor}/src/utils/anchor.ts (100%) delete mode 100644 console/src/utils/__tests__/anchor.spec.ts diff --git a/console/packages/editor/src/extensions/heading/index.ts b/console/packages/editor/src/extensions/heading/index.ts index 94190c1df..3b0aa003f 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({ + 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({ 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 0e86b1770..531a9ab32 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 e19557b7b..e58d538ff 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 4d9f63492..000000000 --- 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(""); - }); -});