editor: fix duplicate block-id on splitting node (#9198)

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2026-01-14 10:56:31 +05:00
committed by GitHub
parent a6032ffd72
commit 22a6271423
4 changed files with 73 additions and 11 deletions

View File

@@ -0,0 +1,46 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "vitest";
import { createEditor, h } from "../../../../test-utils";
import { Heading } from "../../heading";
import { BlockId } from "../block-id";
test("splitting a node with blockId should generate new blockId for the new node", async () => {
const el = h("div", [
h("h1", ["A heading one"], { "data-block-id": "blockid" })
]);
const { editor } = createEditor({
extensions: {
heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
blockId: BlockId
},
initialContent: el.outerHTML
});
editor.commands.setTextSelection(9);
const event = new KeyboardEvent("keydown", { key: "Enter" });
editor.view.dom.dispatchEvent(event);
await new Promise((resolve) => setTimeout(resolve, 100));
const headings = editor.getJSON().content;
expect(headings?.[0].attrs?.blockId).toBe("blockid");
expect(headings?.[1].attrs?.blockId).not.toBeUndefined();
expect(headings?.[1].attrs?.blockId).not.toBe("blockid");
});

View File

@@ -20,6 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Extension } from "@tiptap/core";
import { Plugin } from "@tiptap/pm/state";
import { nanoid } from "nanoid";
import {
AttributeUpdate,
BatchAttributeStep
} from "../../utils/batch-attribute-step.js";
const NESTED_BLOCK_ID_TYPES = ["callout"];
const BLOCK_ID_TYPES = [
@@ -74,24 +78,33 @@ export const BlockId = Extension.create({
const isDocChanged = transactions.some((tr) => tr.docChanged);
if (!isDocChanged) return null;
let updated = false;
const blockIds = new Set<string>();
const updates: AttributeUpdate[] = [];
const { tr } = newState;
tr.doc.forEach(function addBlockId(n, offset) {
if (!n.isBlock || !BLOCK_ID_TYPES.includes(n.type.name)) return;
if (!n.attrs.blockId) {
const id = nanoid(8);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we can't use tr.setNodeMarkup as that creates
// a new transaction for every single node update causing
// significant performance issues
n.attrs.blockId = id;
updated = true;
const currentId = n.attrs.blockId;
const shouldUpdateId = !currentId || blockIds.has(currentId);
const finalId = shouldUpdateId ? nanoid(8) : currentId;
if (shouldUpdateId) {
updates.push({
pos: offset,
attrName: "blockId",
value: finalId
});
}
blockIds.add(finalId);
if (NESTED_BLOCK_ID_TYPES.includes(n.type.name))
n.forEach((n, pos) => addBlockId(n, offset + pos + 1));
});
if (updated) {
if (updates.length > 0) {
tr.step(new BatchAttributeStep(updates));
tr.setMeta("ignoreEdit", true);
return tr;
}

View File

@@ -30,7 +30,10 @@ import {
changedDescendants,
findParentNodeOfTypeClosestToPos
} from "../../utils/prosemirror.js";
import { AttributeUpdate, BatchAttributeStep } from "./batch-attribute-step.js";
import {
AttributeUpdate,
BatchAttributeStep
} from "../../utils/batch-attribute-step.js";
const COLLAPSIBLE_BLOCK_TYPES = [
"paragraph",