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; +};