From 22a62714232bc20c5522453ff59b56fd8a1980ff Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:56:31 +0500 Subject: [PATCH] editor: fix duplicate block-id on splitting node (#9198) Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../block-id/__tests__/block-id.test.ts | 46 +++++++++++++++++++ .../src/extensions/block-id/block-id.ts | 33 +++++++++---- .../editor/src/extensions/heading/heading.ts | 5 +- .../heading => utils}/batch-attribute-step.ts | 0 4 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 packages/editor/src/extensions/block-id/__tests__/block-id.test.ts rename packages/editor/src/{extensions/heading => utils}/batch-attribute-step.ts (100%) diff --git a/packages/editor/src/extensions/block-id/__tests__/block-id.test.ts b/packages/editor/src/extensions/block-id/__tests__/block-id.test.ts new file mode 100644 index 000000000..6d2a0898a --- /dev/null +++ b/packages/editor/src/extensions/block-id/__tests__/block-id.test.ts @@ -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 . +*/ + +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"); +}); diff --git a/packages/editor/src/extensions/block-id/block-id.ts b/packages/editor/src/extensions/block-id/block-id.ts index ca16d7696..e3bc861f5 100644 --- a/packages/editor/src/extensions/block-id/block-id.ts +++ b/packages/editor/src/extensions/block-id/block-id.ts @@ -20,6 +20,10 @@ along with this program. If not, see . 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(); + 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; } diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index a16efc0ae..7ad39e7f0 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -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", diff --git a/packages/editor/src/extensions/heading/batch-attribute-step.ts b/packages/editor/src/utils/batch-attribute-step.ts similarity index 100% rename from packages/editor/src/extensions/heading/batch-attribute-step.ts rename to packages/editor/src/utils/batch-attribute-step.ts