From e7789929ec4ad034b8c57137260bb70b665ef1a4 Mon Sep 17 00:00:00 2001 From: Takagi <1103069291@qq.com> Date: Tue, 26 Dec 2023 18:48:06 +0800 Subject: [PATCH] fix: fix anchor positioning for identical table of contents names (#5101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area editor /milestone 2.12.x #### What this PR does / why we need it: 重写了对默认编辑器标题的 id 生成逻辑。目前将会在对标题进行任意的修改之后,对所有的标题进行 id 计算,用以解决当标题名称具有重复时,生成了相同的 id. 需要注意的是,由于需要对任意标题进行修改之后才会进行生效,因此已经存在重名标题 id 的问题时,需要修改任意的标题使其生效。 #### How to test it? 在文章内新增多个相同内容的标题,查看是否可以正常跳转。 #### Which issue(s) this PR fixes: Fixes #5068 #### Does this PR introduce a user-facing change? ```release-note 解决默认编辑器中具有重名标题时,锚点只会跳转至首个的问题。 ``` --- .../editor/src/extensions/heading/index.ts | 52 +++++++++++-------- console/packages/editor/src/utils/anchor.ts | 15 ++++++ 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/console/packages/editor/src/extensions/heading/index.ts b/console/packages/editor/src/extensions/heading/index.ts index 3b0aa003f4..4452b69fad 100644 --- a/console/packages/editor/src/extensions/heading/index.ts +++ b/console/packages/editor/src/extensions/heading/index.ts @@ -15,17 +15,13 @@ 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"; +import { AttrStep, Plugin, PluginKey } from "@/tiptap"; +import { generateAnchorId } 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), @@ -282,27 +278,39 @@ const Blockquote = TiptapHeading.extend({ return [TiptapParagraph]; }, addProseMirrorPlugins() { + let beforeComposition: boolean | undefined = undefined; 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, - }) - ); - } + appendTransaction: (transactions, oldState, newState) => { + const isChangeHeading = transactions.some((transaction) => { + const composition = this.editor.view.composing; + if (beforeComposition !== undefined && !composition) { + beforeComposition = undefined; + return true; + } + if (transaction.docChanged) { + beforeComposition = composition; + const selection = transaction.selection; + const { $from } = selection; + const node = $from.parent; + return node.type.name === Blockquote.name && !composition; + } + return false; + }); + if (isChangeHeading) { + const tr = newState.tr; + const headingIds: string[] = []; + newState.doc.descendants((node, pos) => { + if (node.type.name === Blockquote.name) { + const id = generateAnchorId(node.textContent, headingIds); + tr.step(new AttrStep(pos, "id", id)); + headingIds.push(id); } }); - return DecorationSet.create(doc, decorations); - }, + return tr; + } + return undefined; }, }), ]; diff --git a/console/packages/editor/src/utils/anchor.ts b/console/packages/editor/src/utils/anchor.ts index 3845a5ac4a..9f22954fbf 100644 --- a/console/packages/editor/src/utils/anchor.ts +++ b/console/packages/editor/src/utils/anchor.ts @@ -3,3 +3,18 @@ export function generateAnchor(text: string) { String(text).trim().toLowerCase().replace(/\s+/g, "-") ); } + +export const generateAnchorId = (text: string, ids: string[]) => { + const originId = generateAnchor(text); + let id = originId; + while (ids.includes(id)) { + const temporarySuffix = id.replace(originId, ""); + const match = temporarySuffix.match(/-(\d+)$/); + if (match) { + id = `${originId}-${Number(match[1]) + 1}`; + } else { + id = `${originId}-1`; + } + } + return id; +};