Skip to content

Commit

Permalink
fix: fix anchor positioning for identical table of contents names (#5101
Browse files Browse the repository at this point in the history
)

#### 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
解决默认编辑器中具有重名标题时,锚点只会跳转至首个的问题。
```
  • Loading branch information
LIlGG committed Dec 26, 2023
1 parent a1fe8c3 commit e778992
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 22 deletions.
52 changes: 30 additions & 22 deletions console/packages/editor/src/extensions/heading/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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),
Expand Down Expand Up @@ -282,27 +278,39 @@ const Blockquote = TiptapHeading.extend<ExtensionOptions & HeadingOptions>({
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;
},
}),
];
Expand Down
15 changes: 15 additions & 0 deletions console/packages/editor/src/utils/anchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit e778992

Please sign in to comment.