From 26440c0a60669e2a08c50def066dc028f9817599 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 4 Apr 2025 11:28:16 +0200 Subject: [PATCH 01/26] feat(core): add blocknote transactions --- .../commands/insertBlocks/insertBlocks.ts | 21 +- .../commands/mergeBlocks/mergeBlocks.test.ts | 6 +- .../commands/moveBlocks/moveBlocks.test.ts | 63 ++---- .../commands/moveBlocks/moveBlocks.ts | 135 ++++++----- .../commands/nestBlock/nestBlock.ts | 10 +- .../commands/replaceBlocks/replaceBlocks.ts | 12 +- .../commands/splitBlock/splitBlock.test.ts | 52 ++--- .../blockManipulation/getBlock/getBlock.ts | 14 +- .../api/blockManipulation/insertContentAt.ts | 3 +- .../blockManipulation/selections/selection.ts | 19 +- .../textCursorPosition.test.ts | 7 +- .../textCursorPosition/textCursorPosition.ts | 8 +- .../api/clipboard/clipboardExternal.test.ts | 7 +- .../api/clipboard/clipboardInternal.test.ts | 12 +- .../fromClipboard/handleFileInsertion.ts | 2 +- .../clipboard/toClipboard/copyExtension.ts | 10 +- packages/core/src/api/getBlockInfoFromPos.ts | 14 +- .../src/api/parsers/html/parseHTML.test.ts | 5 +- .../helpers/render/createAddFileButton.ts | 2 +- .../core/src/editor/BlockNoteEditor.test.ts | 2 +- packages/core/src/editor/BlockNoteEditor.ts | 211 ++++++++++++++---- .../src/extensions/Comments/CommentsPlugin.ts | 66 +++--- .../ShowSelection/ShowSelectionPlugin.ts | 4 +- .../SideMenu/MultipleNodeSelection.ts | 4 +- .../SuggestionMenu/SuggestionPlugin.ts | 4 +- .../getDefaultSlashMenuItems.ts | 8 +- .../TableHandles/TableHandlesPlugin.ts | 15 +- .../helpers/render/AddFileButton.tsx | 9 +- .../components/Comments/ThreadsSidebar.tsx | 4 +- .../DefaultButtons/CreateLinkButton.tsx | 4 +- .../DefaultSelects/BlockTypeSelect.tsx | 14 +- 31 files changed, 415 insertions(+), 332 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index dc0ea2fd80..251130e059 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -32,28 +32,19 @@ export function insertBlocks< ); } - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const tr = editor.transaction; + const posInfo = getNodeById(id, tr.doc); if (!posInfo) { throw new Error(`Block with ID ${id} not found`); } - // TODO: we might want to use the ReplaceStep directly here instead of insert, - // because the fitting algorithm should not be necessary and might even cause unexpected behavior - if (placement === "before") { - editor.dispatch( - editor._tiptapEditor.state.tr.insert(posInfo.posBeforeNode, nodesToInsert) - ); - } - + let pos = posInfo.posBeforeNode; if (placement === "after") { - editor.dispatch( - editor._tiptapEditor.state.tr.insert( - posInfo.posBeforeNode + posInfo.node.nodeSize, - nodesToInsert - ) - ); + pos += posInfo.node.nodeSize; } + editor.dispatch(tr.insert(pos, nodesToInsert)); + // Now that the `PartialBlock`s have been converted to nodes, we can // re-convert them into full `Block`s. const insertedBlocks: Block[] = []; diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts index 9c9613d0b0..a3b497a206 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts @@ -13,7 +13,7 @@ function mergeBlocks(posBetweenBlocks: number) { } function getPosBeforeSelectedBlock() { - return getBlockInfoFromSelection(getEditor()._tiptapEditor.state).bnBlock + return getBlockInfoFromSelection(getEditor().prosemirrorState).bnBlock .beforePos; } @@ -62,14 +62,14 @@ describe("Test mergeBlocks", () => { getEditor().setTextCursorPosition("paragraph-0", "end"); const firstBlockEndOffset = - getEditor()._tiptapEditor.state.selection.$anchor.parentOffset; + getEditor().prosemirrorState.selection.$anchor.parentOffset; getEditor().setTextCursorPosition("paragraph-1"); mergeBlocks(getPosBeforeSelectedBlock()); const anchorIsAtOldFirstBlockEndPos = - getEditor()._tiptapEditor.state.selection.$anchor.parentOffset === + getEditor().prosemirrorState.selection.$anchor.parentOffset === firstBlockEndOffset; expect(anchorIsAtOldFirstBlockEndPos).toBeTruthy(); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index d8b4e5d40d..4d081d59e4 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -13,7 +13,7 @@ import { const getEditor = setupTestEnv(); function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { - const blockInfo = getBlockInfoFromSelection(getEditor()._tiptapEditor.state); + const blockInfo = getBlockInfoFromSelection(getEditor().prosemirrorState); if (!blockInfo.isBlockContainer) { throw new Error( `Selection points to a ${blockInfo.blockNoteType} node, not a blockContainer node` @@ -21,34 +21,27 @@ function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { } const { blockContent } = blockInfo; + const editor = getEditor(); + const tr = editor.transaction; if (selectionType === "cell") { - getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( + editor.dispatch( + tr.setSelection( CellSelection.create( - getEditor()._tiptapEditor.state.doc, - getEditor() - ._tiptapEditor.state.doc.resolve(blockContent.beforePos + 3) - .before(), - getEditor() - ._tiptapEditor.state.doc.resolve(blockContent.afterPos - 3) - .before() + tr.doc, + tr.doc.resolve(blockContent.beforePos + 3).before(), + tr.doc.resolve(blockContent.afterPos - 3).before() ) ) ); } else if (selectionType === "node") { - getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( - NodeSelection.create( - getEditor()._tiptapEditor.state.doc, - blockContent.beforePos - ) - ) + editor.dispatch( + tr.setSelection(NodeSelection.create(tr.doc, blockContent.beforePos)) ); } else { - getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( + editor.dispatch( + tr.setSelection( TextSelection.create( - getEditor()._tiptapEditor.state.doc, + tr.doc, blockContent.beforePos + 1, blockContent.afterPos - 1 ) @@ -64,13 +57,11 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("paragraph-1"); makeSelectionSpanContent("text"); - expect( - selection.eq(getEditor()._tiptapEditor.state.selection) - ).toBeTruthy(); + expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); }); it("Node selection", () => { @@ -79,13 +70,11 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("image-0"); makeSelectionSpanContent("node"); - expect( - selection.eq(getEditor()._tiptapEditor.state.selection) - ).toBeTruthy(); + expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); }); it("Cell selection", () => { @@ -94,13 +83,11 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("table-0"); makeSelectionSpanContent("cell"); - expect( - selection.eq(getEditor()._tiptapEditor.state.selection) - ).toBeTruthy(); + expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); }); it("Multiple block selection", () => { @@ -108,12 +95,10 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transaction.selection; getEditor().setSelection("paragraph-1", "paragraph-2"); - expect( - selection.eq(getEditor()._tiptapEditor.state.selection) - ).toBeTruthy(); + expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); }); it("Multiple block selection with table", () => { @@ -121,12 +106,10 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transaction.selection; getEditor().setSelection("paragraph-6", "table-0"); - expect( - selection.eq(getEditor()._tiptapEditor.state.selection) - ).toBeTruthy(); + expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); }); }); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index c99d92de6f..e89f787240 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -38,34 +38,33 @@ type BlockSelectionData = ( function getBlockSelectionData( editor: BlockNoteEditor ): BlockSelectionData { - const state = editor._tiptapEditor.state; - const selection = state.selection; + const tr = editor.transaction; - const anchorBlockPosInfo = getNearestBlockPos(state.doc, selection.anchor); + const anchorBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); - if (selection instanceof CellSelection) { + if (tr.selection instanceof CellSelection) { return { type: "cell" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, anchorCellOffset: - selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, + tr.selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, headCellOffset: - selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, + tr.selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, }; - } else if (editor._tiptapEditor.state.selection instanceof NodeSelection) { + } else if (tr.selection instanceof NodeSelection) { return { type: "node" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, }; } else { - const headBlockPosInfo = getNearestBlockPos(state.doc, selection.head); + const headBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.head); return { type: "text" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, headBlockId: headBlockPosInfo.node.attrs.id, - anchorOffset: selection.anchor - anchorBlockPosInfo.posBeforeNode, - headOffset: selection.head - headBlockPosInfo.posBeforeNode, + anchorOffset: tr.selection.anchor - anchorBlockPosInfo.posBeforeNode, + headOffset: tr.selection.head - headBlockPosInfo.posBeforeNode, }; } } @@ -85,10 +84,8 @@ function updateBlockSelectionFromData( editor: BlockNoteEditor, data: BlockSelectionData ) { - const anchorBlockPos = getNodeById( - data.anchorBlockId, - editor._tiptapEditor.state.doc - )?.posBeforeNode; + const tr = editor.transaction; + const anchorBlockPos = getNodeById(data.anchorBlockId, tr.doc)?.posBeforeNode; if (anchorBlockPos === undefined) { throw new Error( `Could not find block with ID ${data.anchorBlockId} to update selection` @@ -98,20 +95,14 @@ function updateBlockSelectionFromData( let selection: Selection; if (data.type === "cell") { selection = CellSelection.create( - editor._tiptapEditor.state.doc, + tr.doc, anchorBlockPos + data.anchorCellOffset, anchorBlockPos + data.headCellOffset ); } else if (data.type === "node") { - selection = NodeSelection.create( - editor._tiptapEditor.state.doc, - anchorBlockPos + 1 - ); + selection = NodeSelection.create(tr.doc, anchorBlockPos + 1); } else { - const headBlockPos = getNodeById( - data.headBlockId, - editor._tiptapEditor.state.doc - )?.posBeforeNode; + const headBlockPos = getNodeById(data.headBlockId, tr.doc)?.posBeforeNode; if (headBlockPos === undefined) { throw new Error( `Could not find block with ID ${data.headBlockId} to update selection` @@ -119,13 +110,12 @@ function updateBlockSelectionFromData( } selection = TextSelection.create( - editor._tiptapEditor.state.doc, + tr.doc, anchorBlockPos + data.anchorOffset, headBlockPos + data.headOffset ); } - - editor.dispatch(editor._tiptapEditor.state.tr.setSelection(selection)); + editor.dispatch(tr.setSelection(selection)); } /** @@ -168,15 +158,18 @@ export function moveSelectedBlocksAndSelection( referenceBlock: BlockIdentifier, placement: "before" | "after" ) { - const blocks = editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]; - const selectionData = getBlockSelectionData(editor); - - editor.removeBlocks(blocks); - editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); - - updateBlockSelectionFromData(editor, selectionData); + // We want this to be a single step in the undo history + editor.transact(() => { + const blocks = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + const selectionData = getBlockSelectionData(editor); + + editor.removeBlocks(blocks); + editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); + + updateBlockSelectionFromData(editor, selectionData); + }); } // Checks if a block is in a valid place after being moved. This check is @@ -292,45 +285,49 @@ function getMoveDownPlacement( } export function moveBlocksUp(editor: BlockNoteEditor) { - const selection = editor.getSelection(); - const block = selection?.blocks[0] || editor.getTextCursorPosition().block; + editor.transact(() => { + const selection = editor.getSelection(); + const block = selection?.blocks[0] || editor.getTextCursorPosition().block; - const moveUpPlacement = getMoveUpPlacement( - editor, - editor.getPrevBlock(block), - editor.getParentBlock(block) - ); + const moveUpPlacement = getMoveUpPlacement( + editor, + editor.getPrevBlock(block), + editor.getParentBlock(block) + ); - if (!moveUpPlacement) { - return; - } + if (!moveUpPlacement) { + return; + } - moveSelectedBlocksAndSelection( - editor, - moveUpPlacement.referenceBlock, - moveUpPlacement.placement - ); + moveSelectedBlocksAndSelection( + editor, + moveUpPlacement.referenceBlock, + moveUpPlacement.placement + ); + }); } export function moveBlocksDown(editor: BlockNoteEditor) { - const selection = editor.getSelection(); - const block = - selection?.blocks[selection?.blocks.length - 1] || - editor.getTextCursorPosition().block; - - const moveDownPlacement = getMoveDownPlacement( - editor, - editor.getNextBlock(block), - editor.getParentBlock(block) - ); - - if (!moveDownPlacement) { - return; - } + editor.transact(() => { + const selection = editor.getSelection(); + const block = + selection?.blocks[selection?.blocks.length - 1] || + editor.getTextCursorPosition().block; + + const moveDownPlacement = getMoveDownPlacement( + editor, + editor.getNextBlock(block), + editor.getParentBlock(block) + ); - moveSelectedBlocksAndSelection( - editor, - moveDownPlacement.referenceBlock, - moveDownPlacement.placement - ); + if (!moveDownPlacement) { + return; + } + + moveSelectedBlocksAndSelection( + editor, + moveDownPlacement.referenceBlock, + moveDownPlacement.placement + ); + }); } diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts index 0f0463660d..e7369c9792 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -81,20 +81,20 @@ export function unnestBlock(editor: BlockNoteEditor) { export function canNestBlock(editor: BlockNoteEditor) { const { bnBlock: blockContainer } = getBlockInfoFromSelection( - editor._tiptapEditor.state + editor.prosemirrorState ); return ( - editor._tiptapEditor.state.doc.resolve(blockContainer.beforePos) - .nodeBefore !== null + editor.prosemirrorState.doc.resolve(blockContainer.beforePos).nodeBefore !== + null ); } export function canUnnestBlock(editor: BlockNoteEditor) { const { bnBlock: blockContainer } = getBlockInfoFromSelection( - editor._tiptapEditor.state + editor.prosemirrorState ); return ( - editor._tiptapEditor.state.doc.resolve(blockContainer.beforePos).depth > 1 + editor.prosemirrorState.doc.resolve(blockContainer.beforePos).depth > 1 ); } diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index bd6ad6687e..2479dfb2d7 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -22,9 +22,7 @@ export function removeAndInsertBlocks< insertedBlocks: Block[]; removedBlocks: Block[]; } { - const ttEditor = editor._tiptapEditor; - let tr = ttEditor.state.tr; - + const tr = editor.transaction; // Converts the `PartialBlock`s to ProseMirror nodes to insert them into the // document. const nodesToInsert: Node[] = []; @@ -47,7 +45,7 @@ export function removeAndInsertBlocks< : blocksToRemove[0].id; let removedSize = 0; - ttEditor.state.doc.descendants((node, pos) => { + tr.doc.descendants((node, pos) => { // Skips traversing nodes after all target blocks have been removed. if (idsOfBlocksToRemove.size === 0) { return false; @@ -75,7 +73,7 @@ export function removeAndInsertBlocks< if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { const oldDocSize = tr.doc.nodeSize; - tr = tr.insert(pos, nodesToInsert); + tr.insert(pos, nodesToInsert); const newDocSize = tr.doc.nodeSize; removedSize += oldDocSize - newDocSize; @@ -91,9 +89,9 @@ export function removeAndInsertBlocks< $pos.node($pos.depth - 1).type.name !== "doc" && $pos.node().childCount === 1 ) { - tr = tr.delete($pos.before(), $pos.after()); + tr.delete($pos.before(), $pos.after()); } else { - tr = tr.delete(pos - removedSize, pos - removedSize + node.nodeSize); + tr.delete(pos - removedSize, pos - removedSize + node.nodeSize); } const newDocSize = tr.doc.nodeSize; removedSize += oldDocSize - newDocSize; diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts index 444729e69c..7068c62389 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts @@ -39,7 +39,7 @@ function setSelectionWithOffset( } getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( + getEditor().prosemirrorState.tr.setSelection( TextSelection.create(doc, info.blockContent.beforePos + offset + 1) ) ); @@ -47,97 +47,83 @@ function setSelectionWithOffset( describe("Test splitBlocks", () => { it("Basic", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-0", - 4 - ); + setSelectionWithOffset(getEditor().prosemirrorState.doc, "paragraph-0", 4); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().prosemirrorState.selection.anchor); expect(getEditor().document).toMatchSnapshot(); }); it("End of content", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-0", - 11 - ); + setSelectionWithOffset(getEditor().prosemirrorState.doc, "paragraph-0", 11); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().prosemirrorState.selection.anchor); expect(getEditor().document).toMatchSnapshot(); }); it("Block has children", () => { setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, + getEditor().prosemirrorState.doc, "paragraph-with-children", 4 ); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().prosemirrorState.selection.anchor); expect(getEditor().document).toMatchSnapshot(); }); it("Keep type", () => { - setSelectionWithOffset(getEditor()._tiptapEditor.state.doc, "heading-0", 4); + setSelectionWithOffset(getEditor().prosemirrorState.doc, "heading-0", 4); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor, true); + splitBlock(getEditor().prosemirrorState.selection.anchor, true); expect(getEditor().document).toMatchSnapshot(); }); it("Don't keep type", () => { - setSelectionWithOffset(getEditor()._tiptapEditor.state.doc, "heading-0", 4); + setSelectionWithOffset(getEditor().prosemirrorState.doc, "heading-0", 4); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor, false); + splitBlock(getEditor().prosemirrorState.selection.anchor, false); expect(getEditor().document).toMatchSnapshot(); }); it.skip("Keep props", () => { setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, + getEditor().prosemirrorState.doc, "paragraph-with-props", 4 ); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor, false, true); + splitBlock(getEditor().prosemirrorState.selection.anchor, false, true); expect(getEditor().document).toMatchSnapshot(); }); it("Don't keep props", () => { setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, + getEditor().prosemirrorState.doc, "paragraph-with-props", 4 ); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor, false, false); + splitBlock(getEditor().prosemirrorState.selection.anchor, false, false); expect(getEditor().document).toMatchSnapshot(); }); it("Selection is set", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-0", - 4 - ); + setSelectionWithOffset(getEditor().prosemirrorState.doc, "paragraph-0", 4); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().prosemirrorState.selection.anchor); - const { bnBlock } = getBlockInfoFromSelection( - getEditor()._tiptapEditor.state - ); + const { bnBlock } = getBlockInfoFromSelection(getEditor().prosemirrorState); const anchorIsAtStartOfNewBlock = bnBlock.node.attrs.id === "0" && - getEditor()._tiptapEditor.state.selection.$anchor.parentOffset === 0; + getEditor().prosemirrorState.selection.$anchor.parentOffset === 0; expect(anchorIsAtStartOfNewBlock).toBeTruthy(); }); diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts index 4bf9ed225b..00966c3b20 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -20,7 +20,7 @@ export function getBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, editor.prosemirrorState.doc); if (!posInfo) { return undefined; } @@ -45,12 +45,12 @@ export function getPrevBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, editor.prosemirrorState.doc); if (!posInfo) { return undefined; } - const $posBeforeNode = editor._tiptapEditor.state.doc.resolve( + const $posBeforeNode = editor.prosemirrorState.doc.resolve( posInfo.posBeforeNode ); const nodeToConvert = $posBeforeNode.nodeBefore; @@ -78,12 +78,12 @@ export function getNextBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, editor.prosemirrorState.doc); if (!posInfo) { return undefined; } - const $posAfterNode = editor._tiptapEditor.state.doc.resolve( + const $posAfterNode = editor.prosemirrorState.doc.resolve( posInfo.posBeforeNode + posInfo.node.nodeSize ); const nodeToConvert = $posAfterNode.nodeAfter; @@ -111,12 +111,12 @@ export function getParentBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, editor.prosemirrorState.doc); if (!posInfo) { return undefined; } - const $posBeforeNode = editor._tiptapEditor.state.doc.resolve( + const $posBeforeNode = editor.prosemirrorState.doc.resolve( posInfo.posBeforeNode ); const parentNode = $posBeforeNode.node(); diff --git a/packages/core/src/api/blockManipulation/insertContentAt.ts b/packages/core/src/api/blockManipulation/insertContentAt.ts index 64791083d3..8322c6a9ea 100644 --- a/packages/core/src/api/blockManipulation/insertContentAt.ts +++ b/packages/core/src/api/blockManipulation/insertContentAt.ts @@ -21,8 +21,7 @@ export function insertContentAt< updateSelection: boolean; } = { updateSelection: true } ) { - const tr = editor._tiptapEditor.state.tr; - + const tr = editor.transaction; // don’t dispatch an empty fragment because this can lead to strange errors // if (content.toString() === "<>") { // return true; diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index 2017581e41..3340f4315a 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -21,8 +21,7 @@ export function getSelection< >( editor: BlockNoteEditor ): Selection | undefined { - const state = editor._tiptapEditor.state; - + const state = editor.prosemirrorState; // Return undefined if the selection is collapsed or a node is selected. if (state.selection.empty || "node" in state.selection) { return undefined; @@ -164,14 +163,12 @@ export function setSelection< `Attempting to set selection with the same anchor and head blocks (id ${startBlockId})` ); } - - const doc = editor._tiptapEditor.state.doc; - - const anchorPosInfo = getNodeById(startBlockId, doc); + const tr = editor.transaction; + const anchorPosInfo = getNodeById(startBlockId, tr.doc); if (!anchorPosInfo) { throw new Error(`Block with ID ${startBlockId} not found`); } - const headPosInfo = getNodeById(endBlockId, doc); + const headPosInfo = getNodeById(endBlockId, tr.doc); if (!headPosInfo) { throw new Error(`Block with ID ${endBlockId} not found`); } @@ -226,7 +223,7 @@ export function setSelection< headBlockInfo.blockContent.node ) + 1; - const lastCellNodeSize = doc.resolve(lastCellPos).nodeAfter!.nodeSize; + const lastCellNodeSize = tr.doc.resolve(lastCellPos).nodeAfter!.nodeSize; endPos = lastCellPos + lastCellNodeSize - 2; } else { endPos = headBlockInfo.blockContent.afterPos - 1; @@ -236,9 +233,7 @@ export function setSelection< // Right now it's missing a few things like a jsonID and styling to show // which nodes are selected. `TextSelection` is ok for now, but has the // restriction that the start/end blocks must have content. - editor._tiptapEditor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create(editor._tiptapEditor.state.doc, startPos, endPos) - ) + editor.dispatch( + tr.setSelection(TextSelection.create(tr.doc, startPos, endPos)) ); } diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts index f7ed692796..0adf0a8643 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts @@ -37,7 +37,7 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { setTextCursorPosition(getEditor(), "paragraph-1", "start"); expect( - getEditor()._tiptapEditor.state.selection.$from.parentOffset === 0 + getEditor().prosemirrorState.selection.$from.parentOffset === 0 ).toBeTruthy(); }); @@ -45,9 +45,8 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { setTextCursorPosition(getEditor(), "paragraph-1", "end"); expect( - getEditor()._tiptapEditor.state.selection.$from.parentOffset === - getEditor()._tiptapEditor.state.selection.$from.node().firstChild! - .nodeSize + getEditor().prosemirrorState.selection.$from.parentOffset === + getEditor().prosemirrorState.selection.$from.node().firstChild!.nodeSize ).toBeTruthy(); }); }); diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts index b7013162e6..1f2ae77475 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts @@ -21,14 +21,14 @@ export function getTextCursorPosition< I extends InlineContentSchema, S extends StyleSchema >(editor: BlockNoteEditor): TextCursorPosition { - const { bnBlock } = getBlockInfoFromSelection(editor._tiptapEditor.state); + const { bnBlock } = getBlockInfoFromSelection(editor.prosemirrorState); - const resolvedPos = editor._tiptapEditor.state.doc.resolve(bnBlock.beforePos); + const resolvedPos = editor.prosemirrorState.doc.resolve(bnBlock.beforePos); // Gets previous blockContainer node at the same nesting level, if the current node isn't the first child. const prevNode = resolvedPos.nodeBefore; // Gets next blockContainer node at the same nesting level, if the current node isn't the last child. - const nextNode = editor._tiptapEditor.state.doc.resolve( + const nextNode = editor.prosemirrorState.doc.resolve( bnBlock.afterPos ).nodeAfter; @@ -95,7 +95,7 @@ export function setTextCursorPosition< ) { const id = typeof targetBlock === "string" ? targetBlock : targetBlock.id; - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, editor.prosemirrorState.doc); if (!posInfo) { throw new Error(`Block with ID ${id} not found`); } diff --git a/packages/core/src/api/clipboard/clipboardExternal.test.ts b/packages/core/src/api/clipboard/clipboardExternal.test.ts index 97ad51bedc..b31d19c19a 100644 --- a/packages/core/src/api/clipboard/clipboardExternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardExternal.test.ts @@ -83,11 +83,8 @@ describe("Test external clipboard HTML", () => { throw new Error("Editor view not initialized."); } - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - testCase.createSelection(editor.prosemirrorView.state.doc) - ) - ); + const tr = editor.transaction; + editor.dispatch(tr.setSelection(testCase.createSelection(tr.doc))); doPaste( editor.prosemirrorView, diff --git a/packages/core/src/api/clipboard/clipboardInternal.test.ts b/packages/core/src/api/clipboard/clipboardInternal.test.ts index 9f958ee60f..e159dfacb6 100644 --- a/packages/core/src/api/clipboard/clipboardInternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardInternal.test.ts @@ -297,11 +297,8 @@ describe("Test ProseMirror selection clipboard HTML", () => { throw new Error("Editor view not initialized."); } - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - testCase.createCopySelection(editor.prosemirrorView.state.doc) - ) - ); + const tr = editor.transaction; + editor.dispatch(tr.setSelection(testCase.createCopySelection(tr.doc))); const { clipboardHTML, externalHTML } = selectedFragmentToHTML( editor.prosemirrorView, @@ -312,11 +309,10 @@ describe("Test ProseMirror selection clipboard HTML", () => { `./__snapshots__/internal/${testCase.testName}.html` ); + const nextTr = editor.transaction; if (testCase.createPasteSelection) { editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - testCase.createPasteSelection(editor.prosemirrorView.state.doc) - ) + nextTr.setSelection(testCase.createPasteSelection(nextTr.doc)) ); } diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index 6615b048a2..226e1ac17e 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -161,7 +161,7 @@ export async function handleFileInsertion< } const posInfo = getNearestBlockPos( - editor._tiptapEditor.state.doc, + editor.prosemirrorState.doc, pos.pos ); diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 30d57bd66f..65b83033cb 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -120,9 +120,10 @@ export function selectedFragmentToHTML< "node" in view.state.selection && (view.state.selection.node as Node).type.spec.group === "blockContent" ) { + const tr = editor.transaction; editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - new NodeSelection(view.state.doc.resolve(view.state.selection.from - 1)) + tr.setSelection( + new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)) ) ); } @@ -251,10 +252,11 @@ export const createCopyToClipboardExtension = < } // Expands the selection to the parent `blockContainer` node. + const tr = editor.transaction; editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( + tr.setSelection( new NodeSelection( - view.state.doc.resolve(view.state.selection.from - 1) + tr.doc.resolve(view.state.selection.from - 1) ) ) ); diff --git a/packages/core/src/api/getBlockInfoFromPos.ts b/packages/core/src/api/getBlockInfoFromPos.ts index 525773a335..e071373e23 100644 --- a/packages/core/src/api/getBlockInfoFromPos.ts +++ b/packages/core/src/api/getBlockInfoFromPos.ts @@ -1,5 +1,5 @@ import { Node, ResolvedPos } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { EditorState, Transaction } from "prosemirror-state"; type SingleBlockInfo = { node: Node; @@ -239,3 +239,15 @@ export function getBlockInfoFromSelection(state: EditorState) { return getBlockInfo(posInfo); } + +/** + * Gets information regarding the ProseMirror nodes that make up a block. The + * block chosen is the one currently containing the current ProseMirror + * selection. + * @param tr The ProseMirror transaction. + */ +export function getBlockInfoFromTransaction(tr: Transaction) { + const posInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); + + return getBlockInfo(posInfo); +} diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index 7602d0be1c..a4bfcdce64 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -31,14 +31,15 @@ async function parseHTMLAndCompareSnapshots( (window as any).__TEST_OPTIONS.mockID = 0; // reset id counter const htmlNode = nestedListsToBlockNoteStructure(html); + const tr = editor.transaction; const slice = (pmView as any).__parseFromClipboard( editor.prosemirrorView, "", htmlNode.innerHTML, false, - editor._tiptapEditor.state.selection.$from + tr.selection.$from ); - editor.dispatch(editor._tiptapEditor.state.tr.replaceSelection(slice)); + editor.dispatch(tr.replaceSelection(slice)); // alternative paste simulation doesn't work in a non-browser vitest env // editor._tiptapEditor.view.pasteHTML(html, { diff --git a/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts b/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts index 1b033a4bc5..f8b8981d5e 100644 --- a/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts +++ b/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts @@ -33,7 +33,7 @@ export const createAddFileButton = ( // Opens the file toolbar. const addFileButtonClickHandler = () => { editor.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + editor.transaction.setMeta(editor.filePanel!.plugin, { block: block, }) ); diff --git a/packages/core/src/editor/BlockNoteEditor.test.ts b/packages/core/src/editor/BlockNoteEditor.test.ts index f0f4669437..1e9dc20776 100644 --- a/packages/core/src/editor/BlockNoteEditor.test.ts +++ b/packages/core/src/editor/BlockNoteEditor.test.ts @@ -10,7 +10,7 @@ import { BlockNoteEditor } from "./BlockNoteEditor.js"; */ it("creates an editor", () => { const editor = BlockNoteEditor.create(); - const posInfo = getNearestBlockPos(editor._tiptapEditor.state.doc, 2); + const posInfo = getNearestBlockPos(editor.prosemirrorState.doc, 2); const info = getBlockInfo(posInfo); expect(info.blockNoteType).toEqual("paragraph"); }); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index aa8ffea6e3..c8bb91fd32 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -94,18 +94,24 @@ import { import { Dictionary } from "../i18n/dictionary.js"; import { en } from "../i18n/locales/index.js"; -import { Plugin, TextSelection, Transaction } from "@tiptap/pm/state"; +import { + EditorState, + Plugin, + Selection as ProsemirrorSelection, + TextSelection, + Transaction, +} from "@tiptap/pm/state"; import { dropCursor } from "prosemirror-dropcursor"; import { EditorView } from "prosemirror-view"; import { ySyncPluginKey } from "y-prosemirror"; import { createInternalHTMLSerializer } from "../api/exporters/html/internalHTMLSerializer.js"; import { inlineContentToNodes } from "../api/nodeConversions/blockToNode.js"; import { nodeToBlock } from "../api/nodeConversions/nodeToBlock.js"; +import { nestedListsToBlockNoteStructure } from "../api/parsers/html/util/nestedLists.js"; +import { CodeBlockOptions } from "../blocks/CodeBlockContent/CodeBlockContent.js"; import type { ThreadStore, User } from "../comments/index.js"; import "../style.css"; import { EventEmitter } from "../util/EventEmitter.js"; -import { CodeBlockOptions } from "../blocks/CodeBlockContent/CodeBlockContent.js"; -import { nestedListsToBlockNoteStructure } from "../api/parsers/html/util/nestedLists.js"; export type BlockNoteExtensionFactory = ( editor: BlockNoteEditor @@ -726,9 +732,131 @@ export class BlockNoteEditor< this.emit("create"); } - dispatch = (tr: Transaction) => { + /** + * Dispatch a ProseMirror transaction. + * + * @example + * ```ts + * const tr = editor.transaction; + * tr.insertText("Hello, world!"); + * editor.dispatch(tr); + * ``` + */ + public dispatch(tr: Transaction) { + if (this.transactionState) { + const { state, transactions } = + this.transactionState.applyTransaction(tr); + // Set a default value if needed + const accTr = this.activeTransaction ?? this.transactionState.tr; + + // Copy over the newly applied transactions into our "active transaction" which accumulates all transaction steps during a `transact` call + transactions.forEach((tr) => { + tr.steps.forEach((step) => { + accTr.step(step); + }); + if (tr.selectionSet) { + accTr.setSelection( + ProsemirrorSelection.fromJSON(accTr.doc, tr.selection.toJSON()) + ); + } + }); + this.activeTransaction = accTr; + this.transactionState = state; + + // We don't want the editor to actually apply the state, so all of this is manipulating the state in-memory + return; + } + this._tiptapEditor.dispatch(tr); - }; + } + + /** + * Stores the currently active transaction, which is the accumulated transaction from all {@link dispatch} calls during a {@link transact} calls + */ + private activeTransaction: Transaction | null = null; + + /** + * Execute a function within a "blocknote transaction". + * All changes to the editor within the transaction will be grouped together, so that + * we can dispatch them as a single operation (thus creating only a single undo step) + * + * @example + * ```ts + * // All changes to the editor will be grouped together + * editor.transact(() => { + * const tr = editor.transaction; + * tr.insertText("Hello, world!"); + * editor.dispatch(tr); + * // These two operations will be grouped together in a single undo step + * const otherTr = editor.transaction; + * otherTr.insertText("Hello, world!"); + * editor.dispatch(otherTr); + * }); + * ``` + */ + public transact(callback: () => T): T { + if (this.transactionState) { + // Already in a transaction, so we can just callback immediately + return callback(); + } + + let result: T = undefined as T; + + try { + // Enter transaction mode, by setting a start state + this.transactionState = this.prosemirrorState; + + // Capture all tiptap transactions (tiptapEditor.dispatch'd transactions) + const tiptapTr = this._tiptapEditor.captureTransaction(() => { + // This is more of a safety mechanism, as we catch blocknote transactions separately + result = callback(); + }); + + // Any transactions captured by the `dispatch` call will be stored in `this.activeTransaction` + let activeTr = this.activeTransaction; + + if (tiptapTr && activeTr) { + // If we have both tiptap & blocknote transactions, there is not a clean way to merge them as you'd need to know the order of operations + throw new Error( + "Cannot mix tiptap transactions with BlockNote transactions" + ); + } else if (tiptapTr) { + // If we only have tiptap caught transactions, we can just use that + activeTr = tiptapTr; + } + + if (activeTr) { + // Dispatch the transaction if it was modified + this._tiptapEditor.dispatch(activeTr); + } + } finally { + this.activeTransaction = null; + this.transactionState = null; + } + + return result; + } + + /** + * Start a new ProseMirror transaction. + * + * @example + * ```ts + * const tr = editor.transaction + * + * tr.insertText("Hello, world!"); + * + * editor.dispatch(tr); + * ``` + */ + public get transaction(): Transaction { + if (this.transactionState) { + // We are in a `transact` call, so we should return the state that was active when the transaction started + return this.transactionState.tr; + } + // Otherwise, we are not in a `transact` call, so we can just return the current state + return this.prosemirrorState.tr; + } /** * Mount the editor to a parent DOM element. Call mount(undefined) to clean up @@ -749,10 +877,18 @@ export class BlockNoteEditor< return this._tiptapEditor.view; } + /** + * The state of the editor when a transaction is captured, this can be continuously updated during the {@link transact} call + */ + private transactionState: EditorState | null = null; + /** * Get the underlying prosemirror state */ public get prosemirrorState() { + if (this.transactionState) { + return this.transactionState; + } return this._tiptapEditor.state; } @@ -1069,8 +1205,8 @@ export class BlockNoteEditor< insertContentAt( { - from: this._tiptapEditor.state.selection.from, - to: this._tiptapEditor.state.selection.to, + from: this.prosemirrorState.selection.from, + to: this.prosemirrorState.selection.to, }, nodes, this @@ -1082,7 +1218,7 @@ export class BlockNoteEditor< */ public getActiveStyles() { const styles: Styles = {}; - const marks = this._tiptapEditor.state.selection.$to.marks(); + const marks = this.prosemirrorState.selection.$to.marks(); for (const mark of marks) { const config = this.schema.styleSchema[mark.type.name]; @@ -1163,10 +1299,8 @@ export class BlockNoteEditor< * Gets the currently selected text. */ public getSelectedText() { - return this._tiptapEditor.state.doc.textBetween( - this._tiptapEditor.state.selection.from, - this._tiptapEditor.state.selection.to - ); + const state = this.prosemirrorState; + return state.doc.textBetween(state.selection.from, state.selection.to); } /** @@ -1185,21 +1319,21 @@ export class BlockNoteEditor< if (url === "") { return; } - - const { from, to } = this._tiptapEditor.state.selection; const mark = this.pmSchema.mark("link", { href: url }); + const tr = this.transaction; + const { from, to } = tr.selection; - this.dispatch( - text - ? this._tiptapEditor.state.tr - .insertText(text, from, to) - .addMark(from, from + text.length, mark) - : this._tiptapEditor.state.tr - .setSelection( - TextSelection.create(this._tiptapEditor.state.tr.doc, to) - ) - .addMark(from, to, mark) - ); + if (text) { + this.dispatch( + tr.insertText(text, from, to).addMark(from, from + text.length, mark) + ); + } else { + this.dispatch( + tr + .setSelection(TextSelection.create(tr.doc, to)) + .addMark(from, to, mark) + ); + } } /** @@ -1451,24 +1585,21 @@ export class BlockNoteEditor< ignoreQueryLength?: boolean; } ) { - const tr = this.prosemirrorView?.state.tr; - if (!tr) { + if (!this.prosemirrorView) { return; } - const transaction = - pluginState && pluginState.deleteTriggerCharacter - ? tr.insertText(triggerCharacter) - : tr; - - this.prosemirrorView.focus(); - this.prosemirrorView.dispatch( - transaction.scrollIntoView().setMeta(this.suggestionMenus.plugin, { - triggerCharacter: triggerCharacter, - deleteTriggerCharacter: pluginState?.deleteTriggerCharacter || false, - ignoreQueryLength: pluginState?.ignoreQueryLength || false, - }) - ); + this.focus(); + const tr = this.transaction; + if (pluginState && pluginState.deleteTriggerCharacter) { + tr.insertText(triggerCharacter); + } + tr.scrollIntoView().setMeta(this.suggestionMenus.plugin, { + triggerCharacter: triggerCharacter, + deleteTriggerCharacter: pluginState?.deleteTriggerCharacter || false, + ignoreQueryLength: pluginState?.ignoreQueryLength || false, + }); + this.dispatch(tr); } // `forceSelectionVisible` determines whether the editor selection is shows diff --git a/packages/core/src/extensions/Comments/CommentsPlugin.ts b/packages/core/src/extensions/Comments/CommentsPlugin.ts index e7498060a2..0b3bdfb7e5 100644 --- a/packages/core/src/extensions/Comments/CommentsPlugin.ts +++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts @@ -89,41 +89,41 @@ export class CommentsPlugin extends EventEmitter { * when a thread is resolved or deleted, we need to update the marks to reflect the new state */ private updateMarksFromThreads = (threads: Map) => { - const ttEditor = this.editor._tiptapEditor; - - ttEditor.state.doc.descendants((node, pos) => { - node.marks.forEach((mark) => { - if (mark.type.name === this.markType) { - const markType = mark.type; - const markThreadId = mark.attrs.threadId; - const thread = threads.get(markThreadId); - const isOrphan = !!(!thread || thread.resolved || thread.deletedAt); - - if (isOrphan !== mark.attrs.orphan) { - const { tr } = ttEditor.state; - const trimmedFrom = Math.max(pos, 0); - const trimmedTo = Math.min( - pos + node.nodeSize, - ttEditor.state.doc.content.size - 1 - ); - tr.removeMark(trimmedFrom, trimmedTo, mark); - tr.addMark( - trimmedFrom, - trimmedTo, - markType.create({ - ...mark.attrs, - orphan: isOrphan, - }) - ); - ttEditor.dispatch(tr); + const tr = this.editor.transaction; + this.editor.transact(() => { + tr.doc.descendants((node, pos) => { + node.marks.forEach((mark) => { + if (mark.type.name === this.markType) { + const markType = mark.type; + const markThreadId = mark.attrs.threadId; + const thread = threads.get(markThreadId); + const isOrphan = !!(!thread || thread.resolved || thread.deletedAt); + + if (isOrphan !== mark.attrs.orphan) { + const trimmedFrom = Math.max(pos, 0); + const trimmedTo = Math.min( + pos + node.nodeSize, + tr.doc.content.size - 1 + ); + tr.removeMark(trimmedFrom, trimmedTo, mark); + tr.addMark( + trimmedFrom, + trimmedTo, + markType.create({ + ...mark.attrs, + orphan: isOrphan, + }) + ); + this.editor.dispatch(tr); - if (isOrphan && this.selectedThreadId === markThreadId) { - // unselect - this.selectedThreadId = undefined; - this.emitStateUpdate(); + if (isOrphan && this.selectedThreadId === markThreadId) { + // unselect + this.selectedThreadId = undefined; + this.emitStateUpdate(); + } } } - } + }); }); }); }; @@ -263,7 +263,7 @@ export class CommentsPlugin extends EventEmitter { this.selectedThreadId = threadId; this.emitStateUpdate(); this.editor.dispatch( - this.editor.prosemirrorView!.state.tr.setMeta(PLUGIN_KEY, { + this.editor.transaction.setMeta(PLUGIN_KEY, { name: SET_SELECTED_THREAD_ID, }) ); diff --git a/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts b/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts index acb8a61cc3..a4ae0e99fb 100644 --- a/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts +++ b/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts @@ -41,9 +41,7 @@ export class ShowSelectionPlugin { this.enabled = enabled; - this.editor.prosemirrorView?.dispatch( - this.editor.prosemirrorView?.state.tr.setMeta(PLUGIN_KEY, {}) - ); + this.editor.dispatch(this.editor.transaction.setMeta(PLUGIN_KEY, {})); } public getEnabled() { diff --git a/packages/core/src/extensions/SideMenu/MultipleNodeSelection.ts b/packages/core/src/extensions/SideMenu/MultipleNodeSelection.ts index c3733d4619..a002143e1f 100644 --- a/packages/core/src/extensions/SideMenu/MultipleNodeSelection.ts +++ b/packages/core/src/extensions/SideMenu/MultipleNodeSelection.ts @@ -82,6 +82,8 @@ export class MultipleNodeSelection extends Selection { } toJSON(): any { - return { type: "node", anchor: this.anchor, head: this.head }; + return { type: "multiple-node", anchor: this.anchor, head: this.head }; } } + +Selection.jsonID("multiple-node", MultipleNodeSelection); diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index c21a8ddb5e..9d2e6b3016 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -115,7 +115,7 @@ class SuggestionMenuView< closeMenu = () => { this.editor.dispatch( - this.editor._tiptapEditor.state.tr.setMeta(suggestionMenuPluginKey, null) + this.editor.transaction.setMeta(suggestionMenuPluginKey, null) ); }; @@ -133,7 +133,7 @@ class SuggestionMenuView< (this.pluginState.deleteTriggerCharacter ? this.pluginState.triggerCharacter!.length : 0), - to: this.editor._tiptapEditor.state.selection.from, + to: this.editor.transaction.selection.from, }) .run(); }; diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 2c070b2366..bcffa50f8b 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -235,7 +235,7 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + editor.transaction.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); @@ -254,7 +254,7 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + editor.transaction.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); @@ -273,7 +273,7 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + editor.transaction.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); @@ -292,7 +292,7 @@ export function getDefaultSlashMenuItems< // Immediately open the file toolbar editor.dispatch( - editor._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + editor.transaction.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index f4ccfcb6a2..bc4b9a1f98 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -263,10 +263,7 @@ export class TableHandlesView< | BlockFromConfigNoChildren | undefined; - const pmNodeInfo = getNodeById( - blockEl.id, - this.editor._tiptapEditor.state.doc - ); + const pmNodeInfo = getNodeById(blockEl.id, this.editor.transaction.doc); if (!pmNodeInfo) { throw new Error(`Block with ID ${blockEl.id} not found`); } @@ -815,7 +812,7 @@ export class TableHandlesProsemirrorPlugin< this.view!.emitUpdate(); this.editor.dispatch( - this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, { + this.editor.transaction.setMeta(tableHandlesPluginKey, { draggedCellOrientation: this.view!.state.draggingState.draggedCellOrientation, originalIndex: this.view!.state.colIndex, @@ -858,7 +855,7 @@ export class TableHandlesProsemirrorPlugin< this.view!.emitUpdate(); this.editor.dispatch( - this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, { + this.editor.transaction.setMeta(tableHandlesPluginKey, { draggedCellOrientation: this.view!.state.draggingState.draggedCellOrientation, originalIndex: this.view!.state.rowIndex, @@ -891,7 +888,7 @@ export class TableHandlesProsemirrorPlugin< this.view!.emitUpdate(); this.editor.dispatch( - this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, null) + this.editor.transaction.setMeta(tableHandlesPluginKey, null) ); if (!this.editor.prosemirrorView) { @@ -1144,9 +1141,9 @@ export class TableHandlesProsemirrorPlugin< | undefined ) => { const isSelectingTableCells = isTableCellSelection( - this.editor.prosemirrorState.selection + this.editor.transaction.selection ) - ? this.editor.prosemirrorState.selection + ? this.editor.transaction.selection : undefined; if ( diff --git a/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx b/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx index 0e13dfbb31..424747638d 100644 --- a/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx +++ b/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx @@ -26,12 +26,9 @@ export const AddFileButton = ( // Opens the file toolbar. const addFileButtonClickHandler = useCallback(() => { props.editor.dispatch( - props.editor._tiptapEditor.state.tr.setMeta( - props.editor.filePanel!.plugin, - { - block: props.block, - } - ) + props.editor.transaction.setMeta(props.editor.filePanel!.plugin, { + block: props.block, + }) ); }, [props.block, props.editor]); diff --git a/packages/react/src/components/Comments/ThreadsSidebar.tsx b/packages/react/src/components/Comments/ThreadsSidebar.tsx index 5f6ba726d2..0ac0097761 100644 --- a/packages/react/src/components/Comments/ThreadsSidebar.tsx +++ b/packages/react/src/components/Comments/ThreadsSidebar.tsx @@ -139,11 +139,11 @@ export function getReferenceText( // is not yet fetched (causing it to be empty). We should store the original // reference text in the data model, as not only is it a general improvement, // but it also means we won't have to handle this edge case. - if (editor.prosemirrorState.doc.nodeSize < threadPosition.to) { + if (editor.transaction.doc.nodeSize < threadPosition.to) { return ""; } - const referenceText = editor.prosemirrorState.doc.textBetween( + const referenceText = editor.transaction.doc.textBetween( threadPosition.from, threadPosition.to ); diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx index 5fb2ab974a..6dd4ba1cba 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx @@ -93,8 +93,8 @@ export const CreateLinkButton = () => { } } - return !isTableCellSelection(editor.prosemirrorState.selection); - }, [linkInSchema, selectedBlocks, editor.prosemirrorState.selection]); + return !isTableCellSelection(editor.transaction.selection); + }, [linkInSchema, selectedBlocks, editor.transaction.selection]); if ( !show || diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index 12d6776b34..c37e27b967 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -132,12 +132,14 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { const onClick = (item: BlockTypeSelectItem) => { editor.focus(); - for (const block of selectedBlocks) { - editor.updateBlock(block, { - type: item.type as any, - props: item.props as any, - }); - } + editor.transact(() => { + for (const block of selectedBlocks) { + editor.updateBlock(block, { + type: item.type as any, + props: item.props as any, + }); + } + }); }; return filteredItems.map((item) => { From 760d76ee538b943702ae6cc37f5d350a4083acae Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 4 Apr 2025 14:32:31 +0200 Subject: [PATCH 02/26] fix: do not even worry about tiptap transactions --- packages/core/src/editor/BlockNoteEditor.ts | 28 ++++++--------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index c8bb91fd32..a5fe65db9e 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -800,41 +800,27 @@ export class BlockNoteEditor< return callback(); } - let result: T = undefined as T; - try { // Enter transaction mode, by setting a start state this.transactionState = this.prosemirrorState; - // Capture all tiptap transactions (tiptapEditor.dispatch'd transactions) - const tiptapTr = this._tiptapEditor.captureTransaction(() => { - // This is more of a safety mechanism, as we catch blocknote transactions separately - result = callback(); - }); + // Capture all dispatch'd transactions + const result = callback(); // Any transactions captured by the `dispatch` call will be stored in `this.activeTransaction` - let activeTr = this.activeTransaction; - - if (tiptapTr && activeTr) { - // If we have both tiptap & blocknote transactions, there is not a clean way to merge them as you'd need to know the order of operations - throw new Error( - "Cannot mix tiptap transactions with BlockNote transactions" - ); - } else if (tiptapTr) { - // If we only have tiptap caught transactions, we can just use that - activeTr = tiptapTr; - } + const activeTr = this.activeTransaction; + this.transactionState = null; if (activeTr) { + this.activeTransaction = null; // Dispatch the transaction if it was modified - this._tiptapEditor.dispatch(activeTr); + this.dispatch(activeTr); } + return result; } finally { this.activeTransaction = null; this.transactionState = null; } - - return result; } /** From 77f1bee8f7ce5349be8635443cfc23056f6573a1 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 4 Apr 2025 14:32:52 +0200 Subject: [PATCH 03/26] fix: execute `updateBlock` as a transaction --- .../commands/updateBlock/updateBlock.ts | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index a1cd756a3c..ccadf2b756 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -1,5 +1,5 @@ import { Fragment, NodeType, Node as PMNode, Slice } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { Transaction } from "prosemirror-state"; import { ReplaceStep } from "prosemirror-transform"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; @@ -34,33 +34,33 @@ export const updateBlockCommand = block: PartialBlock ) => ({ - state, + tr, dispatch, }: { - state: EditorState; - dispatch: ((args?: any) => any) | undefined; + tr: Transaction; + dispatch: (() => void) | undefined; }) => { const blockInfo = getBlockInfoFromResolvedPos( - state.doc.resolve(posBeforeBlock) + tr.doc.resolve(posBeforeBlock) ); if (dispatch) { // Adds blockGroup node with child blocks if necessary. - const oldNodeType = state.schema.nodes[blockInfo.blockNoteType]; + const oldNodeType = editor.pmSchema.nodes[blockInfo.blockNoteType]; const newNodeType = - state.schema.nodes[block.type || blockInfo.blockNoteType]; + editor.pmSchema.nodes[block.type || blockInfo.blockNoteType]; const newBnBlockNodeType = newNodeType.isInGroup("bnBlock") ? newNodeType - : state.schema.nodes["blockContainer"]; + : editor.pmSchema.nodes["blockContainer"]; if (blockInfo.isBlockContainer && newNodeType.isInGroup("blockContent")) { - updateChildren(block, state, editor, blockInfo); + updateChildren(block, tr, editor, blockInfo); // The code below determines the new content of the block. // or "keep" to keep as-is updateBlockContentNode( block, - state, + tr, editor, oldNodeType, newNodeType, @@ -70,7 +70,7 @@ export const updateBlockCommand = !blockInfo.isBlockContainer && newNodeType.isInGroup("bnBlock") ) { - updateChildren(block, state, editor, blockInfo); + updateChildren(block, tr, editor, blockInfo); // old node was a bnBlock type (like column or columnList) and new block as well // No op, we just update the bnBlock below (at end of function) and have already updated the children } else { @@ -88,7 +88,7 @@ export const updateBlockCommand = editor.schema.styleSchema, editor.blockCache ); - state.tr.replaceWith( + tr.replaceWith( blockInfo.bnBlock.beforePos, blockInfo.bnBlock.afterPos, blockToNode( @@ -96,7 +96,7 @@ export const updateBlockCommand = children: existingBlock.children, // if no children are passed in, use existing children ...block, }, - state.schema, + editor.pmSchema, editor.schema.styleSchema ) ); @@ -106,7 +106,7 @@ export const updateBlockCommand = // Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing // attributes. - state.tr.setNodeMarkup(blockInfo.bnBlock.beforePos, newBnBlockNodeType, { + tr.setNodeMarkup(blockInfo.bnBlock.beforePos, newBnBlockNodeType, { ...blockInfo.bnBlock.node.attrs, ...block.props, }); @@ -121,7 +121,7 @@ function updateBlockContentNode< S extends StyleSchema >( block: PartialBlock, - state: EditorState, + tr: Transaction, editor: BlockNoteEditor, oldNodeType: NodeType, newNodeType: NodeType, @@ -140,7 +140,7 @@ function updateBlockContentNode< // Adds a single text node with no marks to the content. content = inlineContentToNodes( [block.content], - state.schema, + editor.pmSchema, editor.schema.styleSchema, newNodeType.name ); @@ -149,14 +149,14 @@ function updateBlockContentNode< // for each InlineContent object. content = inlineContentToNodes( block.content, - state.schema, + editor.pmSchema, editor.schema.styleSchema, newNodeType.name ); } else if (block.content.type === "tableContent") { content = tableContentToNodes( block.content, - state.schema, + editor.pmSchema, editor.schema.styleSchema ); } else { @@ -186,9 +186,9 @@ function updateBlockContentNode< // content is being replaced or not. if (content === "keep") { // use setNodeMarkup to only update the type and attributes - state.tr.setNodeMarkup( + tr.setNodeMarkup( blockInfo.blockContent.beforePos, - block.type === undefined ? undefined : state.schema.nodes[block.type], + block.type === undefined ? undefined : editor.pmSchema.nodes[block.type], { ...blockInfo.blockContent.node.attrs, ...block.props, @@ -198,7 +198,7 @@ function updateBlockContentNode< // use replaceWith to replace the content and the block itself // also reset the selection since replacing the block content // sets it to the next block. - state.tr.replaceWith( + tr.replaceWith( blockInfo.blockContent.beforePos, blockInfo.blockContent.afterPos, newNodeType.createChecked( @@ -218,13 +218,13 @@ function updateChildren< S extends StyleSchema >( block: PartialBlock, - state: EditorState, + tr: Transaction, editor: BlockNoteEditor, blockInfo: BlockInfo ) { if (block.children !== undefined && block.children.length > 0) { const childNodes = block.children.map((child) => { - return blockToNode(child, state.schema, editor.schema.styleSchema); + return blockToNode(child, editor.pmSchema, editor.schema.styleSchema); }); // Checks if a blockGroup node already exists. @@ -232,7 +232,7 @@ function updateChildren< // Replaces all child nodes in the existing blockGroup with the ones created earlier. // use a replacestep to avoid the fitting algorithm - state.tr.step( + tr.step( new ReplaceStep( blockInfo.childContainer.beforePos + 1, blockInfo.childContainer.afterPos - 1, @@ -244,9 +244,9 @@ function updateChildren< throw new Error("impossible"); } // Inserts a new blockGroup containing the child nodes created earlier. - state.tr.insert( + tr.insert( blockInfo.blockContent.afterPos, - state.schema.nodes["blockGroup"].createChecked({}, childNodes) + editor.pmSchema.nodes["blockGroup"].createChecked({}, childNodes) ); } } @@ -261,34 +261,38 @@ export function updateBlock< blockToUpdate: BlockIdentifier, update: PartialBlock ): Block { - const ttEditor = editor._tiptapEditor; - const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; + return editor.transact(() => { + const tr = editor.transaction; + const posInfo = getNodeById(id, tr.doc); + if (!posInfo) { + throw new Error(`Block with ID ${id} not found`); + } - const posInfo = getNodeById(id, ttEditor.state.doc); - if (!posInfo) { - throw new Error(`Block with ID ${id} not found`); - } - - ttEditor.commands.command(({ state, dispatch }) => { updateBlockCommand( editor, posInfo.posBeforeNode, update - )({ state, dispatch }); - return true; - }); + )({ + tr, + dispatch: () => { + // no-op + }, + }); + // Actually dispatch that transaction + editor.dispatch(tr); - const blockContainerNode = ttEditor.state.doc - .resolve(posInfo.posBeforeNode + 1) // TODO: clean? - .node(); + const blockContainerNode = tr.doc + .resolve(posInfo.posBeforeNode + 1) // TODO: clean? + .node(); - return nodeToBlock( - blockContainerNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); + return nodeToBlock( + blockContainerNode, + editor.schema.blockSchema, + editor.schema.inlineContentSchema, + editor.schema.styleSchema, + editor.blockCache + ); + }); } From 4a3d36488bc18af58723652c8972a9a8b0dcc1b6 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 4 Apr 2025 14:52:01 +0200 Subject: [PATCH 04/26] chore: use replace step to insert blocks --- .../commands/insertBlocks/insertBlocks.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index 251130e059..b83d57779d 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -1,4 +1,4 @@ -import { Node } from "prosemirror-model"; +import { Fragment, Node, Slice } from "prosemirror-model"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; @@ -11,6 +11,7 @@ import { import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; +import { ReplaceStep } from "prosemirror-transform"; export function insertBlocks< BSchema extends BlockSchema, @@ -43,7 +44,11 @@ export function insertBlocks< pos += posInfo.node.nodeSize; } - editor.dispatch(tr.insert(pos, nodesToInsert)); + tr.step( + new ReplaceStep(pos, pos, new Slice(Fragment.from(nodesToInsert), 0, 0)) + ); + + editor.dispatch(tr); // Now that the `PartialBlock`s have been converted to nodes, we can // re-convert them into full `Block`s. From eb849cd50d904db9a65c6361e843aff8f35a23a8 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 7 Apr 2025 17:39:26 +0200 Subject: [PATCH 05/26] chore: remove use of ttEditor --- .../ListItemBlockContent/ListItemKeyboardShortcuts.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 93122fdce2..bfff866963 100644 --- a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -4,15 +4,14 @@ import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; export const handleEnter = (editor: BlockNoteEditor) => { - const ttEditor = editor._tiptapEditor; - const blockInfo = getBlockInfoFromSelection(ttEditor.state); + const state = editor.prosemirrorState; + const blockInfo = getBlockInfoFromSelection(state); if (!blockInfo.isBlockContainer) { return false; } const { bnBlock: blockContainer, blockContent } = blockInfo; - const selectionEmpty = - ttEditor.state.selection.anchor === ttEditor.state.selection.head; + const selectionEmpty = state.selection.anchor === state.selection.head; if ( !( @@ -25,7 +24,7 @@ export const handleEnter = (editor: BlockNoteEditor) => { return false; } - return ttEditor.commands.first(({ state, chain, commands }) => [ + return editor._tiptapEditor.commands.first(({ state, chain, commands }) => [ () => // Changes list item block to a paragraph block if the content is empty. commands.command(() => { From fc601215309e3ce473d06f0b4e7e15b750149f7a Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 9 Apr 2025 14:21:28 +0200 Subject: [PATCH 06/26] chore: clarifying comments --- packages/core/src/editor/BlockNoteEditor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index a5fe65db9e..5a5febda78 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -755,6 +755,7 @@ export class BlockNoteEditor< accTr.step(step); }); if (tr.selectionSet) { + // Serialize the selection to JSON, because the document between the `activeTransaction` and the dispatch'd tr are different references accTr.setSelection( ProsemirrorSelection.fromJSON(accTr.doc, tr.selection.toJSON()) ); @@ -818,6 +819,7 @@ export class BlockNoteEditor< } return result; } finally { + // We wrap this in a finally block to ensure we don't disable future transactions just because of an error in the callback this.activeTransaction = null; this.transactionState = null; } From 35a77534290c30ba39a63f4f65eb66916614a78f Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 11 Apr 2025 12:53:56 +0200 Subject: [PATCH 07/26] fix: switch to only using transactions instead of prosemirrorState --- .../__snapshots__/insertBlocks.test.ts.snap | 312 ++++++++++++++++++ .../insertBlocks/insertBlocks.test.ts | 205 ++++++------ .../commands/nestBlock/nestBlock.ts | 22 +- .../commands/updateBlock/updateBlock.ts | 56 ++-- .../blockManipulation/getBlock/getBlock.ts | 23 +- .../blockManipulation/selections/selection.ts | 16 +- .../textCursorPosition/textCursorPosition.ts | 16 +- .../fromClipboard/handleFileInsertion.ts | 6 +- .../ListItemKeyboardShortcuts.ts | 8 +- .../core/src/editor/BlockNoteEditor.test.ts | 3 +- packages/core/src/editor/BlockNoteEditor.ts | 63 +--- 11 files changed, 509 insertions(+), 221 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap index e1bca36762..7a33dd340a 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap @@ -1,6 +1,62 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Test insertBlocks > Insert multiple blocks after 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted paragraph 1", + "type": "text", + }, + ], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted paragraph 2", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted paragraph 3", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test insertBlocks > Insert multiple blocks after 2`] = ` [ { "children": [], @@ -619,6 +675,62 @@ exports[`Test insertBlocks > Insert multiple blocks after 1`] = ` `; exports[`Test insertBlocks > Insert multiple blocks before 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted paragraph 1", + "type": "text", + }, + ], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted paragraph 2", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted paragraph 3", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test insertBlocks > Insert multiple blocks before 2`] = ` [ { "children": [], @@ -1237,6 +1349,22 @@ exports[`Test insertBlocks > Insert multiple blocks before 1`] = ` `; exports[`Test insertBlocks > Insert single basic block after 1`] = ` +[ + { + "children": [], + "content": [], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test insertBlocks > Insert single basic block after 2`] = ` [ { "children": [], @@ -1815,6 +1943,28 @@ exports[`Test insertBlocks > Insert single basic block after 1`] = ` `; exports[`Test insertBlocks > Insert single basic block before (without type) 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "test", + "type": "text", + }, + ], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test insertBlocks > Insert single basic block before (without type) 2`] = ` [ { "children": [], @@ -2399,6 +2549,22 @@ exports[`Test insertBlocks > Insert single basic block before (without type) 1`] `; exports[`Test insertBlocks > Insert single basic block before 1`] = ` +[ + { + "children": [], + "content": [], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test insertBlocks > Insert single basic block before 2`] = ` [ { "children": [], @@ -2977,6 +3143,79 @@ exports[`Test insertBlocks > Insert single basic block before 1`] = ` `; exports[`Test insertBlocks > Insert single complex block after 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 2", + "type": "text", + }, + ], + "id": "inserted-double-nested-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 2", + "type": "text", + }, + ], + "id": "inserted-nested-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "inserted-heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, +] +`; + +exports[`Test insertBlocks > Insert single complex block after 2`] = ` [ { "children": [], @@ -3612,6 +3851,79 @@ exports[`Test insertBlocks > Insert single complex block after 1`] = ` `; exports[`Test insertBlocks > Insert single complex block before 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 2", + "type": "text", + }, + ], + "id": "inserted-double-nested-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 2", + "type": "text", + }, + ], + "id": "inserted-nested-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "inserted-heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, +] +`; + +exports[`Test insertBlocks > Insert single complex block before 2`] = ` [ { "children": [ diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts index 8d3e16c240..33a21fb0b5 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts @@ -7,131 +7,150 @@ const getEditor = setupTestEnv(); describe("Test insertBlocks", () => { it("Insert single basic block before (without type)", () => { - insertBlocks(getEditor(), [{ content: "test" }], "paragraph-0", "before"); + expect( + insertBlocks(getEditor(), [{ content: "test" }], "paragraph-0", "before") + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Insert single basic block before", () => { - insertBlocks(getEditor(), [{ type: "paragraph" }], "paragraph-0", "before"); + expect( + insertBlocks( + getEditor(), + [{ type: "paragraph" }], + "paragraph-0", + "before" + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Insert single basic block after", () => { - insertBlocks(getEditor(), [{ type: "paragraph" }], "paragraph-0", "after"); + expect( + insertBlocks(getEditor(), [{ type: "paragraph" }], "paragraph-0", "after") + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Insert multiple blocks before", () => { - insertBlocks( - getEditor(), - [ - { type: "paragraph", content: "Inserted paragraph 1" }, - { type: "paragraph", content: "Inserted paragraph 2" }, - { type: "paragraph", content: "Inserted paragraph 3" }, - ], - "paragraph-0", - "before" - ); + expect( + insertBlocks( + getEditor(), + [ + { type: "paragraph", content: "Inserted paragraph 1" }, + { type: "paragraph", content: "Inserted paragraph 2" }, + { type: "paragraph", content: "Inserted paragraph 3" }, + ], + "paragraph-0", + "before" + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Insert multiple blocks after", () => { - insertBlocks( - getEditor(), - [ - { type: "paragraph", content: "Inserted paragraph 1" }, - { type: "paragraph", content: "Inserted paragraph 2" }, - { type: "paragraph", content: "Inserted paragraph 3" }, - ], - "paragraph-0", - "after" - ); + expect( + insertBlocks( + getEditor(), + [ + { type: "paragraph", content: "Inserted paragraph 1" }, + { type: "paragraph", content: "Inserted paragraph 2" }, + { type: "paragraph", content: "Inserted paragraph 3" }, + ], + "paragraph-0", + "after" + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Insert single complex block before", () => { - insertBlocks( - getEditor(), - [ - { - id: "inserted-heading-with-everything", - type: "heading", - props: { - backgroundColor: "red", - level: 2, - textAlignment: "center", - textColor: "red", - }, - content: [ - { type: "text", text: "Heading", styles: { bold: true } }, - { type: "text", text: " with styled ", styles: {} }, - { type: "text", text: "content", styles: { italic: true } }, - ], - children: [ - { - id: "inserted-nested-paragraph-2", - type: "paragraph", - content: "Nested Paragraph 2", - children: [ - { - id: "inserted-double-nested-paragraph-2", - type: "paragraph", - content: "Double Nested Paragraph 2", - }, - ], + expect( + insertBlocks( + getEditor(), + [ + { + id: "inserted-heading-with-everything", + type: "heading", + props: { + backgroundColor: "red", + level: 2, + textAlignment: "center", + textColor: "red", }, - ], - }, - ], - "paragraph-0", - "before" - ); + content: [ + { type: "text", text: "Heading", styles: { bold: true } }, + { type: "text", text: " with styled ", styles: {} }, + { type: "text", text: "content", styles: { italic: true } }, + ], + children: [ + { + id: "inserted-nested-paragraph-2", + type: "paragraph", + content: "Nested Paragraph 2", + children: [ + { + id: "inserted-double-nested-paragraph-2", + type: "paragraph", + content: "Double Nested Paragraph 2", + }, + ], + }, + ], + }, + ], + "paragraph-0", + "before" + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Insert single complex block after", () => { - insertBlocks( - getEditor(), - [ - { - id: "inserted-heading-with-everything", - type: "heading", - props: { - backgroundColor: "red", - level: 2, - textAlignment: "center", - textColor: "red", - }, - content: [ - { type: "text", text: "Heading", styles: { bold: true } }, - { type: "text", text: " with styled ", styles: {} }, - { type: "text", text: "content", styles: { italic: true } }, - ], - children: [ - { - id: "inserted-nested-paragraph-2", - type: "paragraph", - content: "Nested Paragraph 2", - children: [ - { - id: "inserted-double-nested-paragraph-2", - type: "paragraph", - content: "Double Nested Paragraph 2", - }, - ], + expect( + insertBlocks( + getEditor(), + [ + { + id: "inserted-heading-with-everything", + type: "heading", + props: { + backgroundColor: "red", + level: 2, + textAlignment: "center", + textColor: "red", }, - ], - }, - ], - "paragraph-0", - "after" - ); + content: [ + { type: "text", text: "Heading", styles: { bold: true } }, + { type: "text", text: " with styled ", styles: {} }, + { type: "text", text: "content", styles: { italic: true } }, + ], + children: [ + { + id: "inserted-nested-paragraph-2", + type: "paragraph", + content: "Nested Paragraph 2", + children: [ + { + id: "inserted-double-nested-paragraph-2", + type: "paragraph", + content: "Double Nested Paragraph 2", + }, + ], + }, + ], + }, + ], + "paragraph-0", + "after" + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts index e7369c9792..88fa46fa91 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -3,7 +3,7 @@ import { EditorState } from "prosemirror-state"; import { ReplaceAroundStep } from "prosemirror-transform"; import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; // TODO: Unit tests /** @@ -80,21 +80,15 @@ export function unnestBlock(editor: BlockNoteEditor) { } export function canNestBlock(editor: BlockNoteEditor) { - const { bnBlock: blockContainer } = getBlockInfoFromSelection( - editor.prosemirrorState - ); + const tr = editor.transaction; + const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); - return ( - editor.prosemirrorState.doc.resolve(blockContainer.beforePos).nodeBefore !== - null - ); + return tr.doc.resolve(blockContainer.beforePos).nodeBefore !== null; } + export function canUnnestBlock(editor: BlockNoteEditor) { - const { bnBlock: blockContainer } = getBlockInfoFromSelection( - editor.prosemirrorState - ); + const tr = editor.transaction; + const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); - return ( - editor.prosemirrorState.doc.resolve(blockContainer.beforePos).depth > 1 - ); + return tr.doc.resolve(blockContainer.beforePos).depth > 1; } diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index ccadf2b756..3e1e81ae76 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -263,36 +263,34 @@ export function updateBlock< ): Block { const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; - return editor.transact(() => { - const tr = editor.transaction; - const posInfo = getNodeById(id, tr.doc); - if (!posInfo) { - throw new Error(`Block with ID ${id} not found`); - } + const tr = editor.transaction; + const posInfo = getNodeById(id, tr.doc); + if (!posInfo) { + throw new Error(`Block with ID ${id} not found`); + } - updateBlockCommand( - editor, - posInfo.posBeforeNode, - update - )({ - tr, - dispatch: () => { - // no-op - }, - }); - // Actually dispatch that transaction - editor.dispatch(tr); + updateBlockCommand( + editor, + posInfo.posBeforeNode, + update + )({ + tr, + dispatch: () => { + // no-op + }, + }); + // Actually dispatch that transaction + editor.dispatch(tr); - const blockContainerNode = tr.doc - .resolve(posInfo.posBeforeNode + 1) // TODO: clean? - .node(); + const blockContainerNode = tr.doc + .resolve(posInfo.posBeforeNode + 1) // TODO: clean? + .node(); - return nodeToBlock( - blockContainerNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); - }); + return nodeToBlock( + blockContainerNode, + editor.schema.blockSchema, + editor.schema.inlineContentSchema, + editor.schema.styleSchema, + editor.blockCache + ); } diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts index 00966c3b20..e004d489c9 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -20,7 +20,7 @@ export function getBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor.prosemirrorState.doc); + const posInfo = getNodeById(id, editor.transaction.doc); if (!posInfo) { return undefined; } @@ -44,15 +44,13 @@ export function getPrevBlock< ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - - const posInfo = getNodeById(id, editor.prosemirrorState.doc); + const tr = editor.transaction; + const posInfo = getNodeById(id, tr.doc); if (!posInfo) { return undefined; } - const $posBeforeNode = editor.prosemirrorState.doc.resolve( - posInfo.posBeforeNode - ); + const $posBeforeNode = tr.doc.resolve(posInfo.posBeforeNode); const nodeToConvert = $posBeforeNode.nodeBefore; if (!nodeToConvert) { return undefined; @@ -77,13 +75,13 @@ export function getNextBlock< ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - - const posInfo = getNodeById(id, editor.prosemirrorState.doc); + const tr = editor.transaction; + const posInfo = getNodeById(id, tr.doc); if (!posInfo) { return undefined; } - const $posAfterNode = editor.prosemirrorState.doc.resolve( + const $posAfterNode = tr.doc.resolve( posInfo.posBeforeNode + posInfo.node.nodeSize ); const nodeToConvert = $posAfterNode.nodeAfter; @@ -111,14 +109,13 @@ export function getParentBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor.prosemirrorState.doc); + const tr = editor.transaction; + const posInfo = getNodeById(id, tr.doc); if (!posInfo) { return undefined; } - const $posBeforeNode = editor.prosemirrorState.doc.resolve( - posInfo.posBeforeNode - ); + const $posBeforeNode = tr.doc.resolve(posInfo.posBeforeNode); const parentNode = $posBeforeNode.node(); const grandparentNode = $posBeforeNode.node(-1); const nodeToConvert = diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index 3340f4315a..1c4d332697 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -21,17 +21,17 @@ export function getSelection< >( editor: BlockNoteEditor ): Selection | undefined { - const state = editor.prosemirrorState; + const tr = editor.transaction; // Return undefined if the selection is collapsed or a node is selected. - if (state.selection.empty || "node" in state.selection) { + if (tr.selection.empty || "node" in tr.selection) { return undefined; } - const $startBlockBeforePos = state.doc.resolve( - getNearestBlockPos(state.doc, state.selection.from).posBeforeNode + const $startBlockBeforePos = tr.doc.resolve( + getNearestBlockPos(tr.doc, tr.selection.from).posBeforeNode ); - const $endBlockBeforePos = state.doc.resolve( - getNearestBlockPos(state.doc, state.selection.to).posBeforeNode + const $endBlockBeforePos = tr.doc.resolve( + getNearestBlockPos(tr.doc, tr.selection.to).posBeforeNode ); // Converts the node at the given index and depth around `$startBlockBeforePos` @@ -42,7 +42,7 @@ export function getSelection< depth?: number ): Block => { const pos = $startBlockBeforePos.posAtIndex(index, depth); - const node = state.doc.resolve(pos).nodeAfter; + const node = tr.doc.resolve(pos).nodeAfter; if (!node) { throw new Error( @@ -136,7 +136,7 @@ export function getSelection< if (blocks.length === 0) { throw new Error( - `Error getting selection - selection doesn't span any blocks (${state.selection})` + `Error getting selection - selection doesn't span any blocks (${tr.selection})` ); } diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts index 1f2ae77475..2554ce34ac 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts @@ -11,7 +11,7 @@ import { import { UnreachableCaseError } from "../../../../util/typescript.js"; import { getBlockInfo, - getBlockInfoFromSelection, + getBlockInfoFromTransaction, } from "../../../getBlockInfoFromPos.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; @@ -21,16 +21,15 @@ export function getTextCursorPosition< I extends InlineContentSchema, S extends StyleSchema >(editor: BlockNoteEditor): TextCursorPosition { - const { bnBlock } = getBlockInfoFromSelection(editor.prosemirrorState); + const tr = editor.transaction; + const { bnBlock } = getBlockInfoFromTransaction(tr); - const resolvedPos = editor.prosemirrorState.doc.resolve(bnBlock.beforePos); + const resolvedPos = tr.doc.resolve(bnBlock.beforePos); // Gets previous blockContainer node at the same nesting level, if the current node isn't the first child. const prevNode = resolvedPos.nodeBefore; // Gets next blockContainer node at the same nesting level, if the current node isn't the last child. - const nextNode = editor.prosemirrorState.doc.resolve( - bnBlock.afterPos - ).nodeAfter; + const nextNode = tr.doc.resolve(bnBlock.afterPos).nodeAfter; // Gets parent blockContainer node, if the current node is nested. let parentNode: Node | undefined = undefined; @@ -94,8 +93,9 @@ export function setTextCursorPosition< placement: "start" | "end" = "start" ) { const id = typeof targetBlock === "string" ? targetBlock : targetBlock.id; + const tr = editor.transaction; - const posInfo = getNodeById(id, editor.prosemirrorState.doc); + const posInfo = getNodeById(id, tr.doc); if (!posInfo) { throw new Error(`Block with ID ${id} not found`); } @@ -108,11 +108,13 @@ export function setTextCursorPosition< if (info.isBlockContainer) { const blockContent = info.blockContent; if (contentType === "none") { + // TODO use tr.setSelection instead of commands editor._tiptapEditor.commands.setNodeSelection(blockContent.beforePos); return; } if (contentType === "inline") { + // TODO use tr.setSelection instead of commands if (placement === "start") { editor._tiptapEditor.commands.setTextSelection( blockContent.beforePos + 1 diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index 226e1ac17e..69834b1048 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -160,10 +160,8 @@ export async function handleFileInsertion< return; } - const posInfo = getNearestBlockPos( - editor.prosemirrorState.doc, - pos.pos - ); + const tr = editor.transaction; + const posInfo = getNearestBlockPos(tr.doc, pos.pos); insertedBlockId = insertOrUpdateBlock( editor, diff --git a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts index bfff866963..6e5dbec2e5 100644 --- a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -1,17 +1,17 @@ import { splitBlockCommand } from "../../api/blockManipulation/commands/splitBlock/splitBlock.js"; import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; -import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; +import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; export const handleEnter = (editor: BlockNoteEditor) => { - const state = editor.prosemirrorState; - const blockInfo = getBlockInfoFromSelection(state); + const tr = editor.transaction; + const blockInfo = getBlockInfoFromTransaction(tr); if (!blockInfo.isBlockContainer) { return false; } const { bnBlock: blockContainer, blockContent } = blockInfo; - const selectionEmpty = state.selection.anchor === state.selection.head; + const selectionEmpty = tr.selection.anchor === tr.selection.head; if ( !( diff --git a/packages/core/src/editor/BlockNoteEditor.test.ts b/packages/core/src/editor/BlockNoteEditor.test.ts index 1e9dc20776..82364711f2 100644 --- a/packages/core/src/editor/BlockNoteEditor.test.ts +++ b/packages/core/src/editor/BlockNoteEditor.test.ts @@ -10,7 +10,8 @@ import { BlockNoteEditor } from "./BlockNoteEditor.js"; */ it("creates an editor", () => { const editor = BlockNoteEditor.create(); - const posInfo = getNearestBlockPos(editor.prosemirrorState.doc, 2); + const tr = editor.transaction; + const posInfo = getNearestBlockPos(tr.doc, 2); const info = getBlockInfo(posInfo); expect(info.blockNoteType).toEqual("paragraph"); }); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 5a5febda78..d0b15528cb 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -94,13 +94,7 @@ import { import { Dictionary } from "../i18n/dictionary.js"; import { en } from "../i18n/locales/index.js"; -import { - EditorState, - Plugin, - Selection as ProsemirrorSelection, - TextSelection, - Transaction, -} from "@tiptap/pm/state"; +import { Plugin, TextSelection, Transaction } from "@tiptap/pm/state"; import { dropCursor } from "prosemirror-dropcursor"; import { EditorView } from "prosemirror-view"; import { ySyncPluginKey } from "y-prosemirror"; @@ -743,28 +737,8 @@ export class BlockNoteEditor< * ``` */ public dispatch(tr: Transaction) { - if (this.transactionState) { - const { state, transactions } = - this.transactionState.applyTransaction(tr); - // Set a default value if needed - const accTr = this.activeTransaction ?? this.transactionState.tr; - - // Copy over the newly applied transactions into our "active transaction" which accumulates all transaction steps during a `transact` call - transactions.forEach((tr) => { - tr.steps.forEach((step) => { - accTr.step(step); - }); - if (tr.selectionSet) { - // Serialize the selection to JSON, because the document between the `activeTransaction` and the dispatch'd tr are different references - accTr.setSelection( - ProsemirrorSelection.fromJSON(accTr.doc, tr.selection.toJSON()) - ); - } - }); - this.activeTransaction = accTr; - this.transactionState = state; - - // We don't want the editor to actually apply the state, so all of this is manipulating the state in-memory + if (this.activeTransaction) { + // We don't want the editor to actually apply the state, so all of this is manipulating the current transaction in-memory return; } @@ -795,25 +769,24 @@ export class BlockNoteEditor< * }); * ``` */ - public transact(callback: () => T): T { - if (this.transactionState) { + public transact(callback: (tr: Transaction) => T): T { + if (this.activeTransaction) { // Already in a transaction, so we can just callback immediately - return callback(); + return callback(this.activeTransaction); } try { - // Enter transaction mode, by setting a start state - this.transactionState = this.prosemirrorState; + // Enter transaction mode, by setting a starting transaction + this.activeTransaction = this.prosemirrorState.tr; // Capture all dispatch'd transactions - const result = callback(); + const result = callback(this.activeTransaction); // Any transactions captured by the `dispatch` call will be stored in `this.activeTransaction` const activeTr = this.activeTransaction; - this.transactionState = null; + this.activeTransaction = null; if (activeTr) { - this.activeTransaction = null; // Dispatch the transaction if it was modified this.dispatch(activeTr); } @@ -821,7 +794,6 @@ export class BlockNoteEditor< } finally { // We wrap this in a finally block to ensure we don't disable future transactions just because of an error in the callback this.activeTransaction = null; - this.transactionState = null; } } @@ -838,9 +810,9 @@ export class BlockNoteEditor< * ``` */ public get transaction(): Transaction { - if (this.transactionState) { - // We are in a `transact` call, so we should return the state that was active when the transaction started - return this.transactionState.tr; + if (this.activeTransaction) { + // We are in a `transact` call, so we should return that transaction to accumulate changes on it + return this.activeTransaction; } // Otherwise, we are not in a `transact` call, so we can just return the current state return this.prosemirrorState.tr; @@ -865,17 +837,12 @@ export class BlockNoteEditor< return this._tiptapEditor.view; } - /** - * The state of the editor when a transaction is captured, this can be continuously updated during the {@link transact} call - */ - private transactionState: EditorState | null = null; - /** * Get the underlying prosemirror state */ public get prosemirrorState() { - if (this.transactionState) { - return this.transactionState; + if (this.activeTransaction) { + throw new Error("Cannot get prosemirrorState while in a transaction"); } return this._tiptapEditor.state; } From 3dc0fcd0d2fb7f483a63e0deadffe38a835218e3 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 11 Apr 2025 13:32:46 +0200 Subject: [PATCH 08/26] test: intentionally failing test --- .../__snapshots__/transactions.test.ts.snap | 34 +++++++ .../src/api/blockManipulation/editor-doc.json | 48 ++++++++++ .../blockManipulation/transaction-doc.json | 48 ++++++++++ .../blockManipulation/transactions.test.ts | 89 +++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 packages/core/src/api/blockManipulation/__snapshots__/transactions.test.ts.snap create mode 100644 packages/core/src/api/blockManipulation/editor-doc.json create mode 100644 packages/core/src/api/blockManipulation/transaction-doc.json create mode 100644 packages/core/src/api/blockManipulation/transactions.test.ts diff --git a/packages/core/src/api/blockManipulation/__snapshots__/transactions.test.ts.snap b/packages/core/src/api/blockManipulation/__snapshots__/transactions.test.ts.snap new file mode 100644 index 0000000000..e30e9cb90c --- /dev/null +++ b/packages/core/src/api/blockManipulation/__snapshots__/transactions.test.ts.snap @@ -0,0 +1,34 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test blocknote transactions > should return the correct block info 1`] = ` +[ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Hey-yo", + "type": "text", + }, + ], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; diff --git a/packages/core/src/api/blockManipulation/editor-doc.json b/packages/core/src/api/blockManipulation/editor-doc.json new file mode 100644 index 0000000000..278302490b --- /dev/null +++ b/packages/core/src/api/blockManipulation/editor-doc.json @@ -0,0 +1,48 @@ +{ + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "id": "1", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Hey-yo", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "backgroundColor": "default", + "id": "0", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} \ No newline at end of file diff --git a/packages/core/src/api/blockManipulation/transaction-doc.json b/packages/core/src/api/blockManipulation/transaction-doc.json new file mode 100644 index 0000000000..b6fcb9454e --- /dev/null +++ b/packages/core/src/api/blockManipulation/transaction-doc.json @@ -0,0 +1,48 @@ +{ + "content": [ + { + "content": [ + { + "attrs": { + "backgroundColor": "default", + "id": null, + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "content": [ + { + "text": "Hey-yo", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + { + "attrs": { + "backgroundColor": "default", + "id": "0", + "textColor": "default", + }, + "content": [ + { + "attrs": { + "textAlignment": "left", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} \ No newline at end of file diff --git a/packages/core/src/api/blockManipulation/transactions.test.ts b/packages/core/src/api/blockManipulation/transactions.test.ts new file mode 100644 index 0000000000..e2c96624a2 --- /dev/null +++ b/packages/core/src/api/blockManipulation/transactions.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from "vitest"; + +import { setupTestEnv } from "./setupTestEnv.js"; +import { Transaction } from "prosemirror-state"; + +const getEditor = setupTestEnv(); + +describe("Test blocknote transactions", () => { + it("should return the correct block info when not in a transaction", async () => { + const editor = getEditor(); + editor.removeBlocks(editor.document); + + const tr = editor.transaction; + const nodeToInsert = { + type: "blockContainer", + attrs: { + // Intentionally missing id + // id: "1", + textColor: "default", + backgroundColor: "default", + }, + content: [ + { + type: "paragraph", + attrs: { + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Hey-yo", + }, + ], + }, + ], + }; + + tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); + await expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); + + editor.dispatch(tr); + + expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( + "editor-doc.json" + ); + }); + + it("should return the correct block info", async () => { + const editor = getEditor(); + editor.removeBlocks(editor.document); + + let tr = undefined as unknown as Transaction; + + editor.transact(() => { + tr = editor.transaction; + const nodeToInsert = { + type: "blockContainer", + attrs: { + // Intentionally missing id + // id: "1", + textColor: "default", + backgroundColor: "default", + }, + content: [ + { + type: "paragraph", + attrs: { + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Hey-yo", + }, + ], + }, + ], + }; + + tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); + expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); + + editor.dispatch(tr); + expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( + "editor-doc.json" + ); + }); + }); +}); From ffc4a5b83d28df9f7532a28d266ab79bb48809f5 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 11 Apr 2025 14:24:45 +0200 Subject: [PATCH 09/26] fix: apply plugin `appendedTransactions` --- packages/core/src/editor/BlockNoteEditor.ts | 30 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index d0b15528cb..29c199c3a6 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -94,7 +94,12 @@ import { import { Dictionary } from "../i18n/dictionary.js"; import { en } from "../i18n/locales/index.js"; -import { Plugin, TextSelection, Transaction } from "@tiptap/pm/state"; +import { + Plugin, + TextSelection, + Transaction, + Selection as ProsemirrorSelection, +} from "@tiptap/pm/state"; import { dropCursor } from "prosemirror-dropcursor"; import { EditorView } from "prosemirror-view"; import { ySyncPluginKey } from "y-prosemirror"; @@ -738,7 +743,28 @@ export class BlockNoteEditor< */ public dispatch(tr: Transaction) { if (this.activeTransaction) { - // We don't want the editor to actually apply the state, so all of this is manipulating the current transaction in-memory + // The user wanted to dispatch, but we are already in a transaction, so we don't want to apply the state + + // We do want to append any transactions though, so we'll do that + const { transactions } = this._tiptapEditor.state.applyTransaction(tr); + + this.activeTransaction = transactions.reduce((activeTr, trToApply) => { + trToApply.steps.forEach((step) => { + activeTr.step(step); + }); + if (trToApply.selectionSet) { + // Serialize the selection to JSON, because the document between the `activeTransaction` and the dispatch'd tr are different references + activeTr.setSelection( + ProsemirrorSelection.fromJSON( + this.activeTransaction!.doc, + trToApply.selection.toJSON() + ) + ); + } + + return activeTr; + }); + return; } From 7133bc8fe6eb05b8af452c9ef929150f4f9b77ce Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 14 Apr 2025 12:32:59 +0200 Subject: [PATCH 10/26] revert: "fix: apply plugin `appendedTransactions`" This reverts commit ffc4a5b83d28df9f7532a28d266ab79bb48809f5. --- packages/core/src/editor/BlockNoteEditor.ts | 30 ++------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 29c199c3a6..d0b15528cb 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -94,12 +94,7 @@ import { import { Dictionary } from "../i18n/dictionary.js"; import { en } from "../i18n/locales/index.js"; -import { - Plugin, - TextSelection, - Transaction, - Selection as ProsemirrorSelection, -} from "@tiptap/pm/state"; +import { Plugin, TextSelection, Transaction } from "@tiptap/pm/state"; import { dropCursor } from "prosemirror-dropcursor"; import { EditorView } from "prosemirror-view"; import { ySyncPluginKey } from "y-prosemirror"; @@ -743,28 +738,7 @@ export class BlockNoteEditor< */ public dispatch(tr: Transaction) { if (this.activeTransaction) { - // The user wanted to dispatch, but we are already in a transaction, so we don't want to apply the state - - // We do want to append any transactions though, so we'll do that - const { transactions } = this._tiptapEditor.state.applyTransaction(tr); - - this.activeTransaction = transactions.reduce((activeTr, trToApply) => { - trToApply.steps.forEach((step) => { - activeTr.step(step); - }); - if (trToApply.selectionSet) { - // Serialize the selection to JSON, because the document between the `activeTransaction` and the dispatch'd tr are different references - activeTr.setSelection( - ProsemirrorSelection.fromJSON( - this.activeTransaction!.doc, - trToApply.selection.toJSON() - ) - ); - } - - return activeTr; - }); - + // We don't want the editor to actually apply the state, so all of this is manipulating the current transaction in-memory return; } From 6fac1fdc3b663050bf29062bd4b6f5206f065435 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 14 Apr 2025 16:10:59 +0200 Subject: [PATCH 11/26] refactor: blocknote API uses lowest possible level prosemirror primitives --- .../insertBlocks/insertBlocks.test.ts | 24 +- .../commands/insertBlocks/insertBlocks.ts | 28 +- .../commands/moveBlocks/moveBlocks.test.ts | 235 ++- .../commands/moveBlocks/moveBlocks.ts | 229 +- .../__snapshots__/removeBlocks.test.ts.snap | 1859 ----------------- .../removeBlocks/removeBlocks.test.ts | 40 - .../commands/removeBlocks/removeBlocks.ts | 20 - .../replaceBlocks/replaceBlocks.test.ts | 22 +- .../commands/replaceBlocks/replaceBlocks.ts | 51 +- .../__snapshots__/updateBlock.test.ts.snap | 1051 ++++++++++ .../commands/updateBlock/updateBlock.test.ts | 499 +++-- .../commands/updateBlock/updateBlock.ts | 127 +- .../blockManipulation/getBlock/getBlock.ts | 82 +- .../api/blockManipulation/insertContentAt.ts | 20 +- .../selections/selection.test.ts | 122 +- .../blockManipulation/selections/selection.ts | 42 +- .../textCursorPosition.test.ts | 56 +- .../textCursorPosition/textCursorPosition.ts | 83 +- .../blockManipulation/transactions.test.ts | 2 +- .../src/api/nodeConversions/nodeToBlock.ts | 3 +- packages/core/src/editor/BlockNoteEditor.ts | 133 +- .../src/test/commands/updateBlock.test.ts | 4 +- 22 files changed, 2269 insertions(+), 2463 deletions(-) delete mode 100644 packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap delete mode 100644 packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts delete mode 100644 packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts index 33a21fb0b5..97e5d0395d 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts @@ -1,10 +1,32 @@ import { describe, expect, it } from "vitest"; import { setupTestEnv } from "../../setupTestEnv.js"; -import { insertBlocks } from "./insertBlocks.js"; +import { insertBlocks as insertBlocksTr } from "./insertBlocks.js"; +import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; +import { PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import { BlockIdentifier } from "../../../../schema/index.js"; const getEditor = setupTestEnv(); +function insertBlocks( + editor: BlockNoteEditor, + blocksToInsert: PartialBlock[], + referenceBlock: BlockIdentifier, + placement: "before" | "after" = "before" +) { + return editor.transact((tr) => + insertBlocksTr( + tr, + editor.pmSchema, + editor.schema, + blocksToInsert, + referenceBlock, + placement, + editor.blockCache + ) + ); +} + describe("Test insertBlocks", () => { it("Insert single basic block before (without type)", () => { expect( diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index b83d57779d..c7dfba4587 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -1,7 +1,7 @@ -import { Fragment, Node, Slice } from "prosemirror-model"; +import { Fragment, Node, Schema, Slice } from "prosemirror-model"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; +import type { BlockCache } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier, BlockSchema, @@ -12,28 +12,30 @@ import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; import { ReplaceStep } from "prosemirror-transform"; +import type { Transaction } from "prosemirror-state"; +import type { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; export function insertBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, + pmSchema: Schema, + schema: BlockNoteSchema, blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, - placement: "before" | "after" = "before" + placement: "before" | "after" = "before", + blockCache?: BlockCache ): Block[] { const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; const nodesToInsert: Node[] = []; for (const blockSpec of blocksToInsert) { - nodesToInsert.push( - blockToNode(blockSpec, editor.pmSchema, editor.schema.styleSchema) - ); + nodesToInsert.push(blockToNode(blockSpec, pmSchema, schema.styleSchema)); } - const tr = editor.transaction; const posInfo = getNodeById(id, tr.doc); if (!posInfo) { throw new Error(`Block with ID ${id} not found`); @@ -48,8 +50,6 @@ export function insertBlocks< new ReplaceStep(pos, pos, new Slice(Fragment.from(nodesToInsert), 0, 0)) ); - editor.dispatch(tr); - // Now that the `PartialBlock`s have been converted to nodes, we can // re-convert them into full `Block`s. const insertedBlocks: Block[] = []; @@ -57,10 +57,10 @@ export function insertBlocks< insertedBlocks.push( nodeToBlock( node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ) ); } diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index 4d081d59e4..a70116a60c 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -55,7 +55,16 @@ describe("Test moveSelectedBlockAndSelection", () => { getEditor().setTextCursorPosition("paragraph-1"); makeSelectionSpanContent("text"); - moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); + getEditor().transact((tr) => { + moveSelectedBlocksAndSelection( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + "before", + getEditor().blockCache + ); + }); const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("paragraph-1"); @@ -68,7 +77,16 @@ describe("Test moveSelectedBlockAndSelection", () => { getEditor().setTextCursorPosition("image-0"); makeSelectionSpanContent("node"); - moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); + getEditor().transact((tr) => { + moveSelectedBlocksAndSelection( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + "before", + getEditor().blockCache + ); + }); const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("image-0"); @@ -81,7 +99,16 @@ describe("Test moveSelectedBlockAndSelection", () => { getEditor().setTextCursorPosition("table-0"); makeSelectionSpanContent("cell"); - moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); + getEditor().transact((tr) => { + moveSelectedBlocksAndSelection( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + "before", + getEditor().blockCache + ); + }); const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("table-0"); @@ -93,7 +120,16 @@ describe("Test moveSelectedBlockAndSelection", () => { it("Multiple block selection", () => { getEditor().setSelection("paragraph-1", "paragraph-2"); - moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); + getEditor().transact((tr) => { + moveSelectedBlocksAndSelection( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + "before", + getEditor().blockCache + ); + }); const selection = getEditor().transaction.selection; getEditor().setSelection("paragraph-1", "paragraph-2"); @@ -104,7 +140,16 @@ describe("Test moveSelectedBlockAndSelection", () => { it("Multiple block selection with table", () => { getEditor().setSelection("paragraph-6", "table-0"); - moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); + getEditor().transact((tr) => { + moveSelectedBlocksAndSelection( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + "before", + getEditor().blockCache + ); + }); const selection = getEditor().transaction.selection; getEditor().setSelection("paragraph-6", "table-0"); @@ -117,7 +162,14 @@ describe("Test moveBlocksUp", () => { it("Basic", () => { getEditor().setTextCursorPosition("paragraph-1"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -125,7 +177,14 @@ describe("Test moveBlocksUp", () => { it("Into children", () => { getEditor().setTextCursorPosition("paragraph-2"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -133,7 +192,14 @@ describe("Test moveBlocksUp", () => { it("Out of children", () => { getEditor().setTextCursorPosition("nested-paragraph-1"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -141,7 +207,14 @@ describe("Test moveBlocksUp", () => { it("First block", () => { getEditor().setTextCursorPosition("paragraph-0"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -149,7 +222,14 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks", () => { getEditor().setSelection("paragraph-1", "paragraph-2"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -157,7 +237,14 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks starting in block with children", () => { getEditor().setSelection("paragraph-with-children", "paragraph-2"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -165,7 +252,14 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks starting in nested block", () => { getEditor().setSelection("nested-paragraph-0", "paragraph-2"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -173,7 +267,14 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks ending in block with children", () => { getEditor().setSelection("paragraph-1", "paragraph-with-children"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -181,7 +282,14 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks ending in nested block", () => { getEditor().setSelection("paragraph-1", "nested-paragraph-0"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -189,7 +297,14 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks starting and ending in nested block", () => { getEditor().setSelection("nested-paragraph-0", "nested-paragraph-1"); - moveBlocksUp(getEditor()); + getEditor().transact((tr) => { + moveBlocksUp( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -199,7 +314,14 @@ describe("Test moveBlocksDown", () => { it("Basic", () => { getEditor().setTextCursorPosition("paragraph-0"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -207,7 +329,14 @@ describe("Test moveBlocksDown", () => { it("Into children", () => { getEditor().setTextCursorPosition("paragraph-1"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -215,7 +344,14 @@ describe("Test moveBlocksDown", () => { it("Out of children", () => { getEditor().setTextCursorPosition("nested-paragraph-1"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -223,7 +359,14 @@ describe("Test moveBlocksDown", () => { it("Last block", () => { getEditor().setTextCursorPosition("trailing-paragraph"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -231,7 +374,14 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks", () => { getEditor().setSelection("paragraph-1", "paragraph-2"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -239,7 +389,14 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks starting in block with children", () => { getEditor().setSelection("paragraph-with-children", "paragraph-2"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -247,7 +404,14 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks starting in nested block", () => { getEditor().setSelection("nested-paragraph-0", "paragraph-2"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -255,7 +419,14 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks ending in block with children", () => { getEditor().setSelection("paragraph-1", "paragraph-with-children"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -263,7 +434,14 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks ending in nested block", () => { getEditor().setSelection("paragraph-1", "nested-paragraph-0"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); @@ -271,7 +449,14 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks starting and ending in nested block", () => { getEditor().setSelection("nested-paragraph-0", "nested-paragraph-1"); - moveBlocksDown(getEditor()); + getEditor().transact((tr) => { + moveBlocksDown( + tr, + getEditor().pmSchema, + getEditor().schema, + getEditor().blockCache + ); + }); expect(getEditor().document).toMatchSnapshot(); }); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index e89f787240..a52c223a50 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -1,11 +1,27 @@ -import { NodeSelection, Selection, TextSelection } from "prosemirror-state"; +import { + NodeSelection, + type Selection, + TextSelection, + type Transaction, +} from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; -import { Block } from "../../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; +import type { Node, Schema } from "prosemirror-model"; +import type { Block } from "../../../../blocks/defaultBlocks.js"; +import type { BlockCache } from "../../../../editor/BlockNoteEditor"; +import { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; import { BlockIdentifier } from "../../../../schema/index.js"; import { getNearestBlockPos } from "../../../getBlockInfoFromPos.js"; import { getNodeById } from "../../../nodeUtil.js"; +import { + getNextBlock, + getParentBlock, + getPrevBlock, +} from "../../getBlock/getBlock.js"; +import { getSelection } from "../../selections/selection.js"; +import { getTextCursorPosition } from "../../selections/textCursorPosition/textCursorPosition.js"; +import { insertBlocks } from "../insertBlocks/insertBlocks.js"; +import { removeAndInsertBlocks } from "../replaceBlocks/replaceBlocks.js"; type BlockSelectionData = ( | { @@ -36,35 +52,34 @@ type BlockSelectionData = ( * @param editor The BlockNote editor instance to get the selection data from. */ function getBlockSelectionData( - editor: BlockNoteEditor + doc: Node, + selection: Selection ): BlockSelectionData { - const tr = editor.transaction; + const anchorBlockPosInfo = getNearestBlockPos(doc, selection.anchor); - const anchorBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); - - if (tr.selection instanceof CellSelection) { + if (selection instanceof CellSelection) { return { type: "cell" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, anchorCellOffset: - tr.selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, + selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, headCellOffset: - tr.selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, + selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, }; - } else if (tr.selection instanceof NodeSelection) { + } else if (selection instanceof NodeSelection) { return { type: "node" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, }; } else { - const headBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.head); + const headBlockPosInfo = getNearestBlockPos(doc, selection.head); return { type: "text" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, headBlockId: headBlockPosInfo.node.attrs.id, - anchorOffset: tr.selection.anchor - anchorBlockPosInfo.posBeforeNode, - headOffset: tr.selection.head - headBlockPosInfo.posBeforeNode, + anchorOffset: selection.anchor - anchorBlockPosInfo.posBeforeNode, + headOffset: selection.head - headBlockPosInfo.posBeforeNode, }; } } @@ -81,10 +96,9 @@ function getBlockSelectionData( * `getBlockSelectionData`). */ function updateBlockSelectionFromData( - editor: BlockNoteEditor, + tr: Transaction, data: BlockSelectionData ) { - const tr = editor.transaction; const anchorBlockPos = getNodeById(data.anchorBlockId, tr.doc)?.posBeforeNode; if (anchorBlockPos === undefined) { throw new Error( @@ -115,7 +129,8 @@ function updateBlockSelectionFromData( headBlockPos + data.headOffset ); } - editor.dispatch(tr.setSelection(selection)); + + tr.setSelection(selection); } /** @@ -154,22 +169,31 @@ function flattenColumns( * reference block. */ export function moveSelectedBlocksAndSelection( - editor: BlockNoteEditor, + tr: Transaction, + pmSchema: Schema, + schema: BlockNoteSchema, referenceBlock: BlockIdentifier, - placement: "before" | "after" + placement: "before" | "after", + blockCache?: BlockCache ) { - // We want this to be a single step in the undo history - editor.transact(() => { - const blocks = editor.getSelection()?.blocks || [ - editor.getTextCursorPosition().block, - ]; - const selectionData = getBlockSelectionData(editor); - - editor.removeBlocks(blocks); - editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); - - updateBlockSelectionFromData(editor, selectionData); - }); + const blocks = getSelection(tr, schema, blockCache)?.blocks || [ + getTextCursorPosition(tr, schema, blockCache).block, + ]; + const selectionData = getBlockSelectionData(tr.doc, tr.selection); + + removeAndInsertBlocks(tr, pmSchema, schema, blocks, [], blockCache); + + insertBlocks( + tr, + pmSchema, + schema, + flattenColumns(blocks), + referenceBlock, + placement, + blockCache + ); + + updateBlockSelectionFromData(tr, selectionData); } // Checks if a block is in a valid place after being moved. This check is @@ -191,9 +215,11 @@ function checkPlacementIsValid(parentBlock?: Block): boolean { // placement is found. Returns undefined if no valid placement is found, meaning // the block is already at the top of the document. function getMoveUpPlacement( - editor: BlockNoteEditor, + doc: Node, + schema: BlockNoteSchema, prevBlock?: Block, - parentBlock?: Block + parentBlock?: Block, + blockCache?: BlockCache ): | { referenceBlock: BlockIdentifier; placement: "before" | "after" } | undefined { @@ -218,14 +244,21 @@ function getMoveUpPlacement( return undefined; } - const referenceBlockParent = editor.getParentBlock(referenceBlock); + const referenceBlockParent = getParentBlock( + doc, + schema, + referenceBlock, + blockCache + ); if (!checkPlacementIsValid(referenceBlockParent)) { return getMoveUpPlacement( - editor, + doc, + schema, placement === "after" ? referenceBlock - : editor.getPrevBlock(referenceBlock), - referenceBlockParent + : getPrevBlock(doc, schema, referenceBlock, blockCache), + referenceBlockParent, + blockCache ); } @@ -243,9 +276,11 @@ function getMoveUpPlacement( // placement is found. Returns undefined if no valid placement is found, meaning // the block is already at the bottom of the document. function getMoveDownPlacement( - editor: BlockNoteEditor, + doc: Node, + schema: BlockNoteSchema, nextBlock?: Block, - parentBlock?: Block + parentBlock?: Block, + blockCache?: BlockCache ): | { referenceBlock: BlockIdentifier; placement: "before" | "after" } | undefined { @@ -270,64 +305,88 @@ function getMoveDownPlacement( return undefined; } - const referenceBlockParent = editor.getParentBlock(referenceBlock); + const referenceBlockParent = getParentBlock( + doc, + schema, + referenceBlock, + blockCache + ); if (!checkPlacementIsValid(referenceBlockParent)) { return getMoveDownPlacement( - editor, + doc, + schema, placement === "before" ? referenceBlock - : editor.getNextBlock(referenceBlock), - referenceBlockParent + : getNextBlock(doc, schema, referenceBlock, blockCache), + referenceBlockParent, + blockCache ); } return { referenceBlock, placement }; } -export function moveBlocksUp(editor: BlockNoteEditor) { - editor.transact(() => { - const selection = editor.getSelection(); - const block = selection?.blocks[0] || editor.getTextCursorPosition().block; - - const moveUpPlacement = getMoveUpPlacement( - editor, - editor.getPrevBlock(block), - editor.getParentBlock(block) - ); - - if (!moveUpPlacement) { - return; - } +export function moveBlocksUp( + tr: Transaction, + pmSchema: Schema, + schema: BlockNoteSchema, + blockCache?: BlockCache +) { + const selection = getSelection(tr, schema, blockCache); + const block = + selection?.blocks[0] || getTextCursorPosition(tr, schema, blockCache).block; + + const moveUpPlacement = getMoveUpPlacement( + tr.doc, + schema, + getPrevBlock(tr.doc, schema, block, blockCache), + getParentBlock(tr.doc, schema, block, blockCache), + blockCache + ); + + if (!moveUpPlacement) { + return; + } - moveSelectedBlocksAndSelection( - editor, - moveUpPlacement.referenceBlock, - moveUpPlacement.placement - ); - }); + moveSelectedBlocksAndSelection( + tr, + pmSchema, + schema, + moveUpPlacement.referenceBlock, + moveUpPlacement.placement, + blockCache + ); } -export function moveBlocksDown(editor: BlockNoteEditor) { - editor.transact(() => { - const selection = editor.getSelection(); - const block = - selection?.blocks[selection?.blocks.length - 1] || - editor.getTextCursorPosition().block; - - const moveDownPlacement = getMoveDownPlacement( - editor, - editor.getNextBlock(block), - editor.getParentBlock(block) - ); - - if (!moveDownPlacement) { - return; - } +export function moveBlocksDown( + tr: Transaction, + pmSchema: Schema, + schema: BlockNoteSchema, + blockCache?: BlockCache +) { + const selection = getSelection(tr, schema, blockCache); + const block = + selection?.blocks[selection?.blocks.length - 1] || + getTextCursorPosition(tr, schema, blockCache).block; + + const moveDownPlacement = getMoveDownPlacement( + tr.doc, + schema, + getNextBlock(tr.doc, schema, block, blockCache), + getParentBlock(tr.doc, schema, block, blockCache), + blockCache + ); + + if (!moveDownPlacement) { + return; + } - moveSelectedBlocksAndSelection( - editor, - moveDownPlacement.referenceBlock, - moveDownPlacement.placement - ); - }); + moveSelectedBlocksAndSelection( + tr, + pmSchema, + schema, + moveDownPlacement.referenceBlock, + moveDownPlacement.placement, + blockCache + ); } diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap deleted file mode 100644 index 75d5602f2e..0000000000 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +++ /dev/null @@ -1,1859 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Test removeBlocks > Remove all child blocks 1`] = ` -[ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 0", - "type": "text", - }, - ], - "id": "paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 1", - "type": "text", - }, - ], - "id": "paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with children", - "type": "text", - }, - ], - "id": "paragraph-with-children", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 2", - "type": "text", - }, - ], - "id": "paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", - "props": { - "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 3", - "type": "text", - }, - ], - "id": "paragraph-3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Paragraph", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "paragraph-with-styled-content", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 4", - "type": "text", - }, - ], - "id": "paragraph-4", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Heading 1", - "type": "text", - }, - ], - "id": "heading-0", - "props": { - "backgroundColor": "default", - "level": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "heading", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 5", - "type": "text", - }, - ], - "id": "paragraph-5", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": undefined, - "id": "image-0", - "props": { - "backgroundColor": "default", - "caption": "", - "name": "", - "previewWidth": 512, - "showPreview": true, - "textAlignment": "left", - "url": "https://via.placeholder.com/150", - }, - "type": "image", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 6", - "type": "text", - }, - ], - "id": "paragraph-6", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": { - "columnWidths": [ - undefined, - undefined, - undefined, - ], - "headerCols": undefined, - "headerRows": undefined, - "rows": [ - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 1", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 2", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 3", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 4", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 5", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 6", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 7", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 8", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 9", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - ], - "type": "tableContent", - }, - "id": "table-0", - "props": { - "textColor": "default", - }, - "type": "table", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 7", - "type": "text", - }, - ], - "id": "paragraph-7", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "empty-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 8", - "type": "text", - }, - ], - "id": "paragraph-8", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 1", - "type": "text", - }, - ], - "id": "double-nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Nested Paragraph 1", - "type": "text", - }, - ], - "id": "nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Heading", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "heading-with-everything", - "props": { - "backgroundColor": "red", - "level": 2, - "textAlignment": "center", - "textColor": "red", - }, - "type": "heading", - }, - { - "children": [], - "content": [], - "id": "trailing-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, -] -`; - -exports[`Test removeBlocks > Remove multiple consecutive blocks 1`] = ` -[ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 2", - "type": "text", - }, - ], - "id": "paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", - "props": { - "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 3", - "type": "text", - }, - ], - "id": "paragraph-3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Paragraph", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "paragraph-with-styled-content", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 4", - "type": "text", - }, - ], - "id": "paragraph-4", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Heading 1", - "type": "text", - }, - ], - "id": "heading-0", - "props": { - "backgroundColor": "default", - "level": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "heading", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 5", - "type": "text", - }, - ], - "id": "paragraph-5", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": undefined, - "id": "image-0", - "props": { - "backgroundColor": "default", - "caption": "", - "name": "", - "previewWidth": 512, - "showPreview": true, - "textAlignment": "left", - "url": "https://via.placeholder.com/150", - }, - "type": "image", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 6", - "type": "text", - }, - ], - "id": "paragraph-6", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": { - "columnWidths": [ - undefined, - undefined, - undefined, - ], - "headerCols": undefined, - "headerRows": undefined, - "rows": [ - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 1", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 2", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 3", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 4", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 5", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 6", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 7", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 8", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 9", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - ], - "type": "tableContent", - }, - "id": "table-0", - "props": { - "textColor": "default", - }, - "type": "table", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 7", - "type": "text", - }, - ], - "id": "paragraph-7", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "empty-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 8", - "type": "text", - }, - ], - "id": "paragraph-8", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 1", - "type": "text", - }, - ], - "id": "double-nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Nested Paragraph 1", - "type": "text", - }, - ], - "id": "nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Heading", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "heading-with-everything", - "props": { - "backgroundColor": "red", - "level": 2, - "textAlignment": "center", - "textColor": "red", - }, - "type": "heading", - }, - { - "children": [], - "content": [], - "id": "trailing-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, -] -`; - -exports[`Test removeBlocks > Remove multiple non-consecutive blocks 1`] = ` -[ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 1", - "type": "text", - }, - ], - "id": "paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 0", - "type": "text", - }, - ], - "id": "double-nested-paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Nested Paragraph 0", - "type": "text", - }, - ], - "id": "nested-paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Paragraph with children", - "type": "text", - }, - ], - "id": "paragraph-with-children", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 2", - "type": "text", - }, - ], - "id": "paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", - "props": { - "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 3", - "type": "text", - }, - ], - "id": "paragraph-3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Paragraph", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "paragraph-with-styled-content", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 4", - "type": "text", - }, - ], - "id": "paragraph-4", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Heading 1", - "type": "text", - }, - ], - "id": "heading-0", - "props": { - "backgroundColor": "default", - "level": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "heading", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 5", - "type": "text", - }, - ], - "id": "paragraph-5", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": undefined, - "id": "image-0", - "props": { - "backgroundColor": "default", - "caption": "", - "name": "", - "previewWidth": 512, - "showPreview": true, - "textAlignment": "left", - "url": "https://via.placeholder.com/150", - }, - "type": "image", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 6", - "type": "text", - }, - ], - "id": "paragraph-6", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 7", - "type": "text", - }, - ], - "id": "paragraph-7", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "empty-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 8", - "type": "text", - }, - ], - "id": "paragraph-8", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "trailing-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, -] -`; - -exports[`Test removeBlocks > Remove single block 1`] = ` -[ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 1", - "type": "text", - }, - ], - "id": "paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 0", - "type": "text", - }, - ], - "id": "double-nested-paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Nested Paragraph 0", - "type": "text", - }, - ], - "id": "nested-paragraph-0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Paragraph with children", - "type": "text", - }, - ], - "id": "paragraph-with-children", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 2", - "type": "text", - }, - ], - "id": "paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph with props", - "type": "text", - }, - ], - "id": "paragraph-with-props", - "props": { - "backgroundColor": "default", - "textAlignment": "center", - "textColor": "red", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 3", - "type": "text", - }, - ], - "id": "paragraph-3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Paragraph", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "paragraph-with-styled-content", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 4", - "type": "text", - }, - ], - "id": "paragraph-4", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Heading 1", - "type": "text", - }, - ], - "id": "heading-0", - "props": { - "backgroundColor": "default", - "level": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "heading", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 5", - "type": "text", - }, - ], - "id": "paragraph-5", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": undefined, - "id": "image-0", - "props": { - "backgroundColor": "default", - "caption": "", - "name": "", - "previewWidth": 512, - "showPreview": true, - "textAlignment": "left", - "url": "https://via.placeholder.com/150", - }, - "type": "image", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 6", - "type": "text", - }, - ], - "id": "paragraph-6", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": { - "columnWidths": [ - undefined, - undefined, - undefined, - ], - "headerCols": undefined, - "headerRows": undefined, - "rows": [ - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 1", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 2", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 3", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 4", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 5", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 6", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - { - "cells": [ - { - "content": [ - { - "styles": {}, - "text": "Cell 7", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 8", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - { - "content": [ - { - "styles": {}, - "text": "Cell 9", - "type": "text", - }, - ], - "props": { - "backgroundColor": "default", - "colspan": 1, - "rowspan": 1, - "textAlignment": "left", - "textColor": "default", - }, - "type": "tableCell", - }, - ], - }, - ], - "type": "tableContent", - }, - "id": "table-0", - "props": { - "textColor": "default", - }, - "type": "table", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 7", - "type": "text", - }, - ], - "id": "paragraph-7", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [], - "id": "empty-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Paragraph 8", - "type": "text", - }, - ], - "id": "paragraph-8", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Double Nested Paragraph 1", - "type": "text", - }, - ], - "id": "double-nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": {}, - "text": "Nested Paragraph 1", - "type": "text", - }, - ], - "id": "nested-paragraph-1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": [ - { - "styles": { - "bold": true, - }, - "text": "Heading", - "type": "text", - }, - { - "styles": {}, - "text": " with styled ", - "type": "text", - }, - { - "styles": { - "italic": true, - }, - "text": "content", - "type": "text", - }, - ], - "id": "heading-with-everything", - "props": { - "backgroundColor": "red", - "level": 2, - "textAlignment": "center", - "textColor": "red", - }, - "type": "heading", - }, - { - "children": [], - "content": [], - "id": "trailing-paragraph", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, -] -`; diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts deleted file mode 100644 index fcdbd4cf07..0000000000 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { setupTestEnv } from "../../setupTestEnv.js"; -import { removeBlocks } from "./removeBlocks.js"; - -const getEditor = setupTestEnv(); - -describe("Test removeBlocks", () => { - it("Remove single block", () => { - removeBlocks(getEditor(), ["paragraph-0"]); - - expect(getEditor().document).toMatchSnapshot(); - }); - - it("Remove multiple consecutive blocks", () => { - removeBlocks(getEditor(), [ - "paragraph-0", - "paragraph-1", - "paragraph-with-children", - ]); - - expect(getEditor().document).toMatchSnapshot(); - }); - - it("Remove multiple non-consecutive blocks", () => { - removeBlocks(getEditor(), [ - "paragraph-0", - "table-0", - "heading-with-everything", - ]); - - expect(getEditor().document).toMatchSnapshot(); - }); - - it("Remove all child blocks", () => { - removeBlocks(getEditor(), ["nested-paragraph-0"]); - - expect(getEditor().document).toMatchSnapshot(); - }); -}); diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts b/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts deleted file mode 100644 index d01606aecb..0000000000 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Block } from "../../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; -import { - BlockIdentifier, - BlockSchema, - InlineContentSchema, - StyleSchema, -} from "../../../../schema/index.js"; -import { removeAndInsertBlocks } from "../replaceBlocks/replaceBlocks.js"; - -export function removeBlocks< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - blocksToRemove: BlockIdentifier[] -): Block[] { - return removeAndInsertBlocks(editor, blocksToRemove, []).removedBlocks; -} diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts index 09d4911a3b..0e2df99c97 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts @@ -1,10 +1,30 @@ import { describe, expect, it } from "vitest"; import { setupTestEnv } from "../../setupTestEnv.js"; -import { replaceBlocks } from "./replaceBlocks.js"; +import { removeAndInsertBlocks } from "./replaceBlocks.js"; +import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; +import { PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import { BlockIdentifier } from "../../../../schema/index.js"; const getEditor = setupTestEnv(); +function replaceBlocks( + editor: BlockNoteEditor, + blocksToRemove: BlockIdentifier[], + blocksToInsert: PartialBlock[] +) { + return editor.transact((tr) => + removeAndInsertBlocks( + tr, + editor.pmSchema, + editor.schema, + blocksToRemove, + blocksToInsert, + editor.blockCache + ) + ); +} + describe("Test replaceBlocks", () => { it("Remove single block", () => { replaceBlocks(getEditor(), ["paragraph-0"], []); diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 2479dfb2d7..1522f33c2d 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,12 +1,14 @@ +import { Node, Schema } from "prosemirror-model"; +import type { Transaction } from "prosemirror-state"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; +import type { BlockCache } from "../../../../editor/BlockNoteEditor"; +import { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; import { BlockIdentifier, BlockSchema, InlineContentSchema, StyleSchema, } from "../../../../schema/index.js"; -import { Node } from "prosemirror-model"; import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; @@ -15,21 +17,21 @@ export function removeAndInsertBlocks< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, + schema: Schema, + blockSchema: BlockNoteSchema, blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[] + blocksToInsert: PartialBlock[], + blockCache?: BlockCache ): { insertedBlocks: Block[]; removedBlocks: Block[]; } { - const tr = editor.transaction; // Converts the `PartialBlock`s to ProseMirror nodes to insert them into the // document. const nodesToInsert: Node[] = []; for (const block of blocksToInsert) { - nodesToInsert.push( - blockToNode(block, editor.pmSchema, editor.schema.styleSchema) - ); + nodesToInsert.push(blockToNode(block, schema, blockSchema.styleSchema)); } const idsOfBlocksToRemove = new Set( @@ -63,10 +65,10 @@ export function removeAndInsertBlocks< removedBlocks.push( nodeToBlock( node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + blockSchema.blockSchema, + blockSchema.inlineContentSchema, + blockSchema.styleSchema, + blockCache ) ); idsOfBlocksToRemove.delete(node.attrs.id); @@ -109,36 +111,19 @@ export function removeAndInsertBlocks< ); } - editor.dispatch(tr); - // Converts the nodes created from `blocksToInsert` into full `Block`s. const insertedBlocks: Block[] = []; for (const node of nodesToInsert) { insertedBlocks.push( nodeToBlock( node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + blockSchema.blockSchema, + blockSchema.inlineContentSchema, + blockSchema.styleSchema, + blockCache ) ); } return { insertedBlocks, removedBlocks }; } - -export function replaceBlocks< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[] -): { - insertedBlocks: Block[]; - removedBlocks: Block[]; -} { - return removeAndInsertBlocks(editor, blocksToRemove, blocksToInsert); -} diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap index dfdc926ac4..232790218d 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap @@ -1,6 +1,77 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Test updateBlock > Revert all props 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "default", + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Revert all props 2`] = ` [ { "children": [], @@ -568,6 +639,77 @@ exports[`Test updateBlock > Revert all props 1`] = ` `; exports[`Test updateBlock > Revert single prop 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 1, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Revert single prop 2`] = ` [ { "children": [], @@ -1135,6 +1277,77 @@ exports[`Test updateBlock > Revert single prop 1`] = ` `; exports[`Test updateBlock > Update all props 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "blue", + "level": 3, + "textAlignment": "right", + "textColor": "blue", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Update all props 2`] = ` [ { "children": [], @@ -1702,6 +1915,77 @@ exports[`Test updateBlock > Update all props 1`] = ` `; exports[`Test updateBlock > Update children 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "New double Nested Paragraph 2", + "type": "text", + }, + ], + "id": "new-double-nested-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "New nested Paragraph 2", + "type": "text", + }, + ], + "id": "new-nested-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Update children 2`] = ` [ { "children": [], @@ -2269,6 +2553,24 @@ exports[`Test updateBlock > Update children 1`] = ` `; exports[`Test updateBlock > Update inline content to no content 1`] = ` +{ + "children": [], + "content": undefined, + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "previewWidth": 512, + "showPreview": true, + "textAlignment": "left", + "url": "", + }, + "type": "image", +} +`; + +exports[`Test updateBlock > Update inline content to no content 2`] = ` [ { "children": [], @@ -2834,6 +3136,194 @@ exports[`Test updateBlock > Update inline content to no content 1`] = ` `; exports[`Test updateBlock > Update inline content to table content 1`] = ` +{ + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "paragraph-0", + "props": { + "textColor": "default", + }, + "type": "table", +} +`; + +exports[`Test updateBlock > Update inline content to table content 2`] = ` [ { "children": [], @@ -3569,6 +4059,20 @@ exports[`Test updateBlock > Update inline content to table content 1`] = ` `; exports[`Test updateBlock > Update no content to empty inline content 1`] = ` +{ + "children": [], + "content": [], + "id": "image-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", +} +`; + +exports[`Test updateBlock > Update no content to empty inline content 2`] = ` [ { "children": [], @@ -4132,6 +4636,24 @@ exports[`Test updateBlock > Update no content to empty inline content 1`] = ` `; exports[`Test updateBlock > Update no content to empty table content 1`] = ` +{ + "children": [], + "content": { + "columnWidths": [], + "headerCols": undefined, + "headerRows": undefined, + "rows": [], + "type": "tableContent", + }, + "id": "image-0", + "props": { + "textColor": "default", + }, + "type": "table", +} +`; + +exports[`Test updateBlock > Update no content to empty table content 2`] = ` [ { "children": [], @@ -4695,6 +5217,26 @@ exports[`Test updateBlock > Update no content to empty table content 1`] = ` `; exports[`Test updateBlock > Update no content to inline content 1`] = ` +{ + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph", + "type": "text", + }, + ], + "id": "image-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", +} +`; + +exports[`Test updateBlock > Update no content to inline content 2`] = ` [ { "children": [], @@ -5264,6 +5806,194 @@ exports[`Test updateBlock > Update no content to inline content 1`] = ` `; exports[`Test updateBlock > Update no content to table content 1`] = ` +{ + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "red", + "colspan": 1, + "rowspan": 1, + "textAlignment": "right", + "textColor": "red", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "image-0", + "props": { + "textColor": "default", + }, + "type": "table", +} +`; + +exports[`Test updateBlock > Update no content to table content 2`] = ` [ { "children": [], @@ -6001,6 +6731,77 @@ exports[`Test updateBlock > Update no content to table content 1`] = ` `; exports[`Test updateBlock > Update single prop 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 3, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Update single prop 2`] = ` [ { "children": [], @@ -6568,6 +7369,20 @@ exports[`Test updateBlock > Update single prop 1`] = ` `; exports[`Test updateBlock > Update table content to empty inline content 1`] = ` +{ + "children": [], + "content": [], + "id": "table-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", +} +`; + +exports[`Test updateBlock > Update table content to empty inline content 2`] = ` [ { "children": [], @@ -6961,6 +7776,26 @@ exports[`Test updateBlock > Update table content to empty inline content 1`] = ` `; exports[`Test updateBlock > Update table content to inline content 1`] = ` +{ + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph", + "type": "text", + }, + ], + "id": "table-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", +} +`; + +exports[`Test updateBlock > Update table content to inline content 2`] = ` [ { "children": [], @@ -7360,6 +8195,24 @@ exports[`Test updateBlock > Update table content to inline content 1`] = ` `; exports[`Test updateBlock > Update table content to no content 1`] = ` +{ + "children": [], + "content": undefined, + "id": "table-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "previewWidth": 512, + "showPreview": true, + "textAlignment": "left", + "url": "", + }, + "type": "image", +} +`; + +exports[`Test updateBlock > Update table content to no content 2`] = ` [ { "children": [], @@ -7757,6 +8610,76 @@ exports[`Test updateBlock > Update table content to no content 1`] = ` `; exports[`Test updateBlock > Update type 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", +} +`; + +exports[`Test updateBlock > Update type 2`] = ` [ { "children": [], @@ -8323,6 +9246,63 @@ exports[`Test updateBlock > Update type 1`] = ` `; exports[`Test updateBlock > Update with plain content 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "New content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Update with plain content 2`] = ` [ { "children": [], @@ -8876,6 +9856,77 @@ exports[`Test updateBlock > Update with plain content 1`] = ` `; exports[`Test updateBlock > Update with styled content 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "backgroundColor": "blue", + }, + "text": "New", + "type": "text", + }, + { + "styles": {}, + "text": " ", + "type": "text", + }, + { + "styles": { + "backgroundColor": "blue", + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", +} +`; + +exports[`Test updateBlock > Update with styled content 2`] = ` [ { "children": [], diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts index dc0e91bb6d..96d28c5841 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts @@ -53,274 +53,445 @@ describe("Test updateBlock typing", () => { describe("Test updateBlock", () => { it.skip("Update ID", () => { - updateBlock(getEditor(), "heading-with-everything", { - id: "new-id", - }); - + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + id: "new-id", + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update type", () => { - updateBlock(getEditor(), "heading-with-everything", { - type: "paragraph", - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + type: "paragraph", + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update single prop", () => { - updateBlock(getEditor(), "heading-with-everything", { - props: { - level: 3, - }, - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + props: { + level: 3, + }, + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update all props", () => { - updateBlock(getEditor(), "heading-with-everything", { - props: { - backgroundColor: "blue", - level: 3, - textAlignment: "right", - textColor: "blue", - }, - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + props: { + backgroundColor: "blue", + level: 3, + textAlignment: "right", + textColor: "blue", + }, + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Revert single prop", () => { - updateBlock(getEditor(), "heading-with-everything", { - props: { - level: undefined, - }, - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + props: { + level: undefined, + }, + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Revert all props", () => { - updateBlock(getEditor(), "heading-with-everything", { - props: { - backgroundColor: undefined, - level: undefined, - textAlignment: undefined, - textColor: undefined, - }, - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + props: { + backgroundColor: undefined, + level: undefined, + textAlignment: undefined, + textColor: undefined, + }, + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update with plain content", () => { - updateBlock(getEditor(), "heading-with-everything", { - content: "New content", - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + content: "New content", + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update with styled content", () => { - updateBlock(getEditor(), "heading-with-everything", { - content: [ - { type: "text", text: "New", styles: { backgroundColor: "blue" } }, - { type: "text", text: " ", styles: {} }, - { type: "text", text: "content", styles: { backgroundColor: "blue" } }, - ], - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + content: [ + { + type: "text", + text: "New", + styles: { backgroundColor: "blue" }, + }, + { type: "text", text: " ", styles: {} }, + { + type: "text", + text: "content", + styles: { backgroundColor: "blue" }, + }, + ], + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update children", () => { - updateBlock(getEditor(), "heading-with-everything", { - children: [ - { - id: "new-nested-paragraph", - type: "paragraph", - content: "New nested Paragraph 2", - children: [ - { - id: "new-double-nested-paragraph", - type: "paragraph", - content: "New double Nested Paragraph 2", - }, - ], - }, - ], - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + children: [ + { + id: "new-nested-paragraph", + type: "paragraph", + content: "New nested Paragraph 2", + children: [ + { + id: "new-double-nested-paragraph", + type: "paragraph", + content: "New double Nested Paragraph 2", + }, + ], + }, + ], + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it.skip("Update everything", () => { - updateBlock(getEditor(), "heading-with-everything", { - id: "new-id", - type: "paragraph", - props: { - backgroundColor: "blue", - textAlignment: "right", - textColor: "blue", - }, - content: [ - { type: "text", text: "New", styles: { backgroundColor: "blue" } }, - { type: "text", text: " ", styles: {} }, - { type: "text", text: "content", styles: { backgroundColor: "blue" } }, - ], - children: [ - { - id: "new-nested-paragraph", - type: "paragraph", - content: "New nested Paragraph 2", - children: [ - { - id: "new-double-nested-paragraph", - type: "paragraph", - content: "New double Nested Paragraph 2", + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "heading-with-everything", + { + id: "new-id", + type: "paragraph", + props: { + backgroundColor: "blue", + textAlignment: "right", + textColor: "blue", }, - ], - }, - ], - }); + content: [ + { + type: "text", + text: "New", + styles: { backgroundColor: "blue" }, + }, + { type: "text", text: " ", styles: {} }, + { + type: "text", + text: "content", + styles: { backgroundColor: "blue" }, + }, + ], + children: [ + { + id: "new-nested-paragraph", + type: "paragraph", + content: "New nested Paragraph 2", + children: [ + { + id: "new-double-nested-paragraph", + type: "paragraph", + content: "New double Nested Paragraph 2", + }, + ], + }, + ], + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update inline content to empty table content", () => { expect(() => { - updateBlock(getEditor(), "paragraph-0", { - type: "table", - }); + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + { + type: "table", + } + ) + ); }).toThrow(); }); it("Update table content to empty inline content", () => { - updateBlock(getEditor(), "table-0", { - type: "paragraph", - }); + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "table-0", { + type: "paragraph", + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update inline content to table content", () => { - updateBlock(getEditor(), "paragraph-0", { - type: "table", - content: { - type: "tableContent", - rows: [ - { - cells: ["Cell 1", "Cell 2", "Cell 3"], - }, + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", { - cells: ["Cell 4", "Cell 5", "Cell 6"], - }, - { - cells: ["Cell 7", "Cell 8", "Cell 9"], - }, - ], - }, - }); + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Cell 1", "Cell 2", "Cell 3"], + }, + { + cells: ["Cell 4", "Cell 5", "Cell 6"], + }, + { + cells: ["Cell 7", "Cell 8", "Cell 9"], + }, + ], + }, + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update table content to inline content", () => { - updateBlock(getEditor(), "table-0", { - type: "paragraph", - content: "Paragraph", - }); + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "table-0", { + type: "paragraph", + content: "Paragraph", + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update inline content to no content", () => { - updateBlock(getEditor(), "paragraph-0", { - type: "image", - }); + expect( + getEditor().transact((tr) => + updateBlock( + tr, + getEditor().pmSchema, + getEditor().schema, + "paragraph-0", + { + type: "image", + } + ) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update no content to empty inline content", () => { - updateBlock(getEditor(), "image-0", { - type: "paragraph", - }); + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + type: "paragraph", + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update no content to inline content", () => { - updateBlock(getEditor(), "image-0", { - type: "paragraph", - content: "Paragraph", - }); + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + type: "paragraph", + content: "Paragraph", + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update no content to empty table content", () => { - updateBlock(getEditor(), "image-0", { - type: "table", - }); + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + type: "table", + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update no content to table content", () => { - updateBlock(getEditor(), "image-0", { - type: "table", - content: { - type: "tableContent", - rows: [ - { - cells: [ + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + type: "table", + content: { + type: "tableContent", + rows: [ { - type: "tableCell", - content: ["Cell 1"], + cells: [ + { + type: "tableCell", + content: ["Cell 1"], + }, + { + type: "tableCell", + content: ["Cell 2"], + props: { + backgroundColor: "red", + colspan: 1, + rowspan: 1, + textAlignment: "right", + textColor: "red", + }, + }, + { + type: "tableCell", + content: ["Cell 3"], + props: { + backgroundColor: "default", + colspan: 1, + rowspan: 1, + textAlignment: "left", + textColor: "default", + }, + }, + ], }, { - type: "tableCell", - content: ["Cell 2"], - props: { - backgroundColor: "red", - colspan: 1, - rowspan: 1, - textAlignment: "right", - textColor: "red", - }, + cells: ["Cell 4", "Cell 5", "Cell 6"], }, { - type: "tableCell", - content: ["Cell 3"], - props: { - backgroundColor: "default", - colspan: 1, - rowspan: 1, - textAlignment: "left", - textColor: "default", - }, + cells: ["Cell 7", "Cell 8", "Cell 9"], }, ], }, - { - cells: ["Cell 4", "Cell 5", "Cell 6"], - }, - { - cells: ["Cell 7", "Cell 8", "Cell 9"], - }, - ], - }, - }); + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); it("Update table content to no content", () => { - updateBlock(getEditor(), "table-0", { - type: "image", - }); + expect( + getEditor().transact((tr) => + updateBlock(tr, getEditor().pmSchema, getEditor().schema, "table-0", { + type: "image", + }) + ) + ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); }); diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index 3e1e81ae76..a3565fb2fe 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -1,18 +1,28 @@ -import { Fragment, NodeType, Node as PMNode, Slice } from "prosemirror-model"; -import { Transaction } from "prosemirror-state"; +import { + Fragment, + NodeType, + Node as PMNode, + Schema, + Slice, +} from "prosemirror-model"; +import type { Transaction } from "prosemirror-state"; import { ReplaceStep } from "prosemirror-transform"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { +import type { + BlockCache, + BlockNoteEditor, +} from "../../../../editor/BlockNoteEditor.js"; +import type { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; +import type { BlockIdentifier, BlockSchema, } from "../../../../schema/blocks/types.js"; -import { InlineContentSchema } from "../../../../schema/inlineContent/types.js"; -import { StyleSchema } from "../../../../schema/styles/types.js"; +import type { InlineContentSchema } from "../../../../schema/inlineContent/types.js"; +import type { StyleSchema } from "../../../../schema/styles/types.js"; import { UnreachableCaseError } from "../../../../util/typescript.js"; import { - BlockInfo, + type BlockInfo, getBlockInfoFromResolvedPos, } from "../../../getBlockInfoFromPos.js"; import { @@ -23,15 +33,36 @@ import { import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; -export const updateBlockCommand = +export const updateBlockCommand = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + posBeforeBlock: number, + block: PartialBlock, + blockCache?: BlockCache +) => { + return updateBlockCommandTr( + editor.pmSchema, + editor.schema, + posBeforeBlock, + block, + blockCache + ); +}; + +export const updateBlockCommandTr = < BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + pmSchema: Schema, + schema: BlockNoteSchema, posBeforeBlock: number, - block: PartialBlock + block: PartialBlock, + blockCache?: BlockCache ) => ({ tr, @@ -47,21 +78,21 @@ export const updateBlockCommand = if (dispatch) { // Adds blockGroup node with child blocks if necessary. - const oldNodeType = editor.pmSchema.nodes[blockInfo.blockNoteType]; - const newNodeType = - editor.pmSchema.nodes[block.type || blockInfo.blockNoteType]; + const oldNodeType = pmSchema.nodes[blockInfo.blockNoteType]; + const newNodeType = pmSchema.nodes[block.type || blockInfo.blockNoteType]; const newBnBlockNodeType = newNodeType.isInGroup("bnBlock") ? newNodeType - : editor.pmSchema.nodes["blockContainer"]; + : pmSchema.nodes["blockContainer"]; if (blockInfo.isBlockContainer && newNodeType.isInGroup("blockContent")) { - updateChildren(block, tr, editor, blockInfo); + updateChildren(block, tr, pmSchema, schema, blockInfo); // The code below determines the new content of the block. // or "keep" to keep as-is updateBlockContentNode( block, tr, - editor, + pmSchema, + schema, oldNodeType, newNodeType, blockInfo @@ -70,7 +101,7 @@ export const updateBlockCommand = !blockInfo.isBlockContainer && newNodeType.isInGroup("bnBlock") ) { - updateChildren(block, tr, editor, blockInfo); + updateChildren(block, tr, pmSchema, schema, blockInfo); // old node was a bnBlock type (like column or columnList) and new block as well // No op, we just update the bnBlock below (at end of function) and have already updated the children } else { @@ -83,10 +114,10 @@ export const updateBlockCommand = // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case const existingBlock = nodeToBlock( blockInfo.bnBlock.node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); tr.replaceWith( blockInfo.bnBlock.beforePos, @@ -96,8 +127,8 @@ export const updateBlockCommand = children: existingBlock.children, // if no children are passed in, use existing children ...block, }, - editor.pmSchema, - editor.schema.styleSchema + pmSchema, + schema.styleSchema ) ); @@ -122,7 +153,8 @@ function updateBlockContentNode< >( block: PartialBlock, tr: Transaction, - editor: BlockNoteEditor, + pmSchema: Schema, + schema: BlockNoteSchema, oldNodeType: NodeType, newNodeType: NodeType, blockInfo: { @@ -140,8 +172,8 @@ function updateBlockContentNode< // Adds a single text node with no marks to the content. content = inlineContentToNodes( [block.content], - editor.pmSchema, - editor.schema.styleSchema, + pmSchema, + schema.styleSchema, newNodeType.name ); } else if (Array.isArray(block.content)) { @@ -149,15 +181,15 @@ function updateBlockContentNode< // for each InlineContent object. content = inlineContentToNodes( block.content, - editor.pmSchema, - editor.schema.styleSchema, + pmSchema, + schema.styleSchema, newNodeType.name ); } else if (block.content.type === "tableContent") { content = tableContentToNodes( block.content, - editor.pmSchema, - editor.schema.styleSchema + pmSchema, + schema.styleSchema ); } else { throw new UnreachableCaseError(block.content.type); @@ -188,7 +220,7 @@ function updateBlockContentNode< // use setNodeMarkup to only update the type and attributes tr.setNodeMarkup( blockInfo.blockContent.beforePos, - block.type === undefined ? undefined : editor.pmSchema.nodes[block.type], + block.type === undefined ? undefined : pmSchema.nodes[block.type], { ...blockInfo.blockContent.node.attrs, ...block.props, @@ -219,12 +251,13 @@ function updateChildren< >( block: PartialBlock, tr: Transaction, - editor: BlockNoteEditor, + pmSchema: Schema, + schema: BlockNoteSchema, blockInfo: BlockInfo ) { if (block.children !== undefined && block.children.length > 0) { const childNodes = block.children.map((child) => { - return blockToNode(child, editor.pmSchema, editor.schema.styleSchema); + return blockToNode(child, pmSchema, schema.styleSchema); }); // Checks if a blockGroup node already exists. @@ -246,7 +279,7 @@ function updateChildren< // Inserts a new blockGroup containing the child nodes created earlier. tr.insert( blockInfo.blockContent.afterPos, - editor.pmSchema.nodes["blockGroup"].createChecked({}, childNodes) + pmSchema.nodes["blockGroup"].createChecked({}, childNodes) ); } } @@ -257,30 +290,32 @@ export function updateBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, + pmSchema: Schema, + schema: BlockNoteSchema, blockToUpdate: BlockIdentifier, - update: PartialBlock + update: PartialBlock, + blockCache?: BlockCache ): Block { const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; - const tr = editor.transaction; const posInfo = getNodeById(id, tr.doc); if (!posInfo) { throw new Error(`Block with ID ${id} not found`); } - updateBlockCommand( - editor, + updateBlockCommandTr( + pmSchema, + schema, posInfo.posBeforeNode, - update + update, + blockCache )({ tr, dispatch: () => { // no-op }, }); - // Actually dispatch that transaction - editor.dispatch(tr); const blockContainerNode = tr.doc .resolve(posInfo.posBeforeNode + 1) // TODO: clean? @@ -288,9 +323,9 @@ export function updateBlock< return nodeToBlock( blockContainerNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); } diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts index e004d489c9..729d3bd9ab 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -1,6 +1,8 @@ -import { Block } from "../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; -import { +import type { Node } from "prosemirror-model"; +import type { Block } from "../../../blocks/defaultBlocks.js"; +import type { BlockCache } from "../../../editor/BlockNoteEditor.js"; +import type { BlockNoteSchema } from "../../../editor/BlockNoteSchema.js"; +import type { BlockIdentifier, BlockSchema, InlineContentSchema, @@ -14,23 +16,25 @@ export function getBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, - blockIdentifier: BlockIdentifier + doc: Node, + schema: BlockNoteSchema, + blockIdentifier: BlockIdentifier, + blockCache?: BlockCache ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor.transaction.doc); + const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } return nodeToBlock( posInfo.node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); } @@ -39,18 +43,20 @@ export function getPrevBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, - blockIdentifier: BlockIdentifier + doc: Node, + schema: BlockNoteSchema, + blockIdentifier: BlockIdentifier, + blockCache?: BlockCache ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const tr = editor.transaction; - const posInfo = getNodeById(id, tr.doc); + + const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } - const $posBeforeNode = tr.doc.resolve(posInfo.posBeforeNode); + const $posBeforeNode = doc.resolve(posInfo.posBeforeNode); const nodeToConvert = $posBeforeNode.nodeBefore; if (!nodeToConvert) { return undefined; @@ -58,10 +64,10 @@ export function getPrevBlock< return nodeToBlock( nodeToConvert, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); } @@ -70,18 +76,19 @@ export function getNextBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, - blockIdentifier: BlockIdentifier + doc: Node, + schema: BlockNoteSchema, + blockIdentifier: BlockIdentifier, + blockCache?: BlockCache ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const tr = editor.transaction; - const posInfo = getNodeById(id, tr.doc); + const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } - const $posAfterNode = tr.doc.resolve( + const $posAfterNode = doc.resolve( posInfo.posBeforeNode + posInfo.node.nodeSize ); const nodeToConvert = $posAfterNode.nodeAfter; @@ -91,10 +98,10 @@ export function getNextBlock< return nodeToBlock( nodeToConvert, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); } @@ -103,19 +110,20 @@ export function getParentBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, - blockIdentifier: BlockIdentifier + doc: Node, + schema: BlockNoteSchema, + blockIdentifier: BlockIdentifier, + blockCache?: BlockCache ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const tr = editor.transaction; - const posInfo = getNodeById(id, tr.doc); + const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } - const $posBeforeNode = tr.doc.resolve(posInfo.posBeforeNode); + const $posBeforeNode = doc.resolve(posInfo.posBeforeNode); const parentNode = $posBeforeNode.node(); const grandparentNode = $posBeforeNode.node(-1); const nodeToConvert = @@ -130,9 +138,9 @@ export function getParentBlock< return nodeToBlock( nodeToConvert, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); } diff --git a/packages/core/src/api/blockManipulation/insertContentAt.ts b/packages/core/src/api/blockManipulation/insertContentAt.ts index 8322c6a9ea..eca09e31eb 100644 --- a/packages/core/src/api/blockManipulation/insertContentAt.ts +++ b/packages/core/src/api/blockManipulation/insertContentAt.ts @@ -1,27 +1,17 @@ import { selectionToInsertionEnd } from "@tiptap/core"; import { Node } from "prosemirror-model"; -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; -import { - BlockSchema, - InlineContentSchema, - StyleSchema, -} from "../../schema/index.js"; +import type { Transaction } from "prosemirror-state"; // similar to tiptap insertContentAt -export function insertContentAt< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - position: any, +export function insertContentAt( + position: number | { from: number; to: number }, nodes: Node[], - editor: BlockNoteEditor, + tr: Transaction, options: { updateSelection: boolean; } = { updateSelection: true } ) { - const tr = editor.transaction; // don’t dispatch an empty fragment because this can lead to strange errors // if (content.toString() === "<>") { // return true; @@ -89,7 +79,5 @@ export function insertContentAt< selectionToInsertionEnd(tr, tr.steps.length - 1, -1); } - editor.dispatch(tr); - return true; } diff --git a/packages/core/src/api/blockManipulation/selections/selection.test.ts b/packages/core/src/api/blockManipulation/selections/selection.test.ts index fd940e8cfc..9fe5fd6a29 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.test.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.test.ts @@ -7,50 +7,124 @@ const getEditor = setupTestEnv(); describe("Test getSelection & setSelection", () => { it("Basic", () => { - setSelection(getEditor(), "paragraph-0", "paragraph-1"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection(tr, getEditor().schema, "paragraph-0", "paragraph-1"); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Starts in block with children", () => { - setSelection(getEditor(), "paragraph-with-children", "paragraph-2"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection( + tr, + getEditor().schema, + "paragraph-with-children", + "paragraph-2" + ); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Starts in nested block", () => { - setSelection(getEditor(), "nested-paragraph-0", "paragraph-2"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection(tr, getEditor().schema, "nested-paragraph-0", "paragraph-2"); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Ends in block with children", () => { - setSelection(getEditor(), "paragraph-1", "paragraph-with-children"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection( + tr, + getEditor().schema, + "paragraph-1", + "paragraph-with-children" + ); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Ends in nested block", () => { - setSelection(getEditor(), "paragraph-1", "nested-paragraph-0"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection(tr, getEditor().schema, "paragraph-1", "nested-paragraph-0"); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Contains block with children", () => { - setSelection(getEditor(), "paragraph-1", "paragraph-2"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection(tr, getEditor().schema, "paragraph-1", "paragraph-2"); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Starts in table", () => { - setSelection(getEditor(), "table-0", "paragraph-7"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection(tr, getEditor().schema, "table-0", "paragraph-7"); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Ends in table", () => { - setSelection(getEditor(), "paragraph-6", "table-0"); - - expect(getSelection(getEditor())).toMatchSnapshot(); + getEditor().transact((tr) => { + setSelection(tr, getEditor().schema, "paragraph-6", "table-0"); + }); + + expect( + getSelection( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); }); diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index 1c4d332697..67e50b4c82 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -1,8 +1,9 @@ -import { TextSelection } from "prosemirror-state"; +import { TextSelection, type Transaction } from "prosemirror-state"; import { TableMap } from "prosemirror-tables"; import { Block } from "../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import type { BlockCache } from "../../../editor/BlockNoteEditor"; +import type { BlockNoteSchema } from "../../../editor/BlockNoteSchema.js"; import { Selection } from "../../../editor/selectionTypes.js"; import { BlockIdentifier, @@ -19,9 +20,10 @@ export function getSelection< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor + tr: Transaction, + schema: BlockNoteSchema, + blockCache?: BlockCache ): Selection | undefined { - const tr = editor.transaction; // Return undefined if the selection is collapsed or a node is selected. if (tr.selection.empty || "node" in tr.selection) { return undefined; @@ -52,10 +54,10 @@ export function getSelection< return nodeToBlock( node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ); }; @@ -100,10 +102,10 @@ export function getSelection< blocks.push( nodeToBlock( $startBlockBeforePos.nodeAfter!, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ) ); @@ -150,7 +152,8 @@ export function setSelection< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, + schema: BlockNoteSchema, startBlock: BlockIdentifier, endBlock: BlockIdentifier ) { @@ -163,7 +166,6 @@ export function setSelection< `Attempting to set selection with the same anchor and head blocks (id ${startBlockId})` ); } - const tr = editor.transaction; const anchorPosInfo = getNodeById(startBlockId, tr.doc); if (!anchorPosInfo) { throw new Error(`Block with ID ${startBlockId} not found`); @@ -177,12 +179,12 @@ export function setSelection< const headBlockInfo = getBlockInfo(headPosInfo); const anchorBlockConfig = - editor.schema.blockSchema[ - anchorBlockInfo.blockNoteType as keyof typeof editor.schema.blockSchema + schema.blockSchema[ + anchorBlockInfo.blockNoteType as keyof typeof schema.blockSchema ]; const headBlockConfig = - editor.schema.blockSchema[ - headBlockInfo.blockNoteType as keyof typeof editor.schema.blockSchema + schema.blockSchema[ + headBlockInfo.blockNoteType as keyof typeof schema.blockSchema ]; if ( @@ -233,7 +235,5 @@ export function setSelection< // Right now it's missing a few things like a jsonID and styling to show // which nodes are selected. `TextSelection` is ok for now, but has the // restriction that the start/end blocks must have content. - editor.dispatch( - tr.setSelection(TextSelection.create(tr.doc, startPos, endPos)) - ); + tr.setSelection(TextSelection.create(tr.doc, startPos, endPos)); } diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts index 0adf0a8643..54c2aa9feb 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts @@ -10,31 +10,65 @@ const getEditor = setupTestEnv(); describe("Test getTextCursorPosition & setTextCursorPosition", () => { it("Basic", () => { - setTextCursorPosition(getEditor(), "paragraph-1"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, getEditor().schema, "paragraph-1"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getTextCursorPosition( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("First block", () => { - setTextCursorPosition(getEditor(), "paragraph-0"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, getEditor().schema, "paragraph-0"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getTextCursorPosition( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Last block", () => { - setTextCursorPosition(getEditor(), "trailing-paragraph"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, getEditor().schema, "trailing-paragraph"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getTextCursorPosition( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Nested block", () => { - setTextCursorPosition(getEditor(), "nested-paragraph-0"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, getEditor().schema, "nested-paragraph-0"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getTextCursorPosition( + getEditor().transaction, + getEditor().schema, + getEditor().blockCache + ) + ).toMatchSnapshot(); }); it("Set to start", () => { - setTextCursorPosition(getEditor(), "paragraph-1", "start"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, getEditor().schema, "paragraph-1", "start"); + }); expect( getEditor().prosemirrorState.selection.$from.parentOffset === 0 @@ -42,7 +76,9 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { }); it("Set to end", () => { - setTextCursorPosition(getEditor(), "paragraph-1", "end"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, getEditor().schema, "paragraph-1", "end"); + }); expect( getEditor().prosemirrorState.selection.$from.parentOffset === diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts index 2554ce34ac..14e642c2f5 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts @@ -1,8 +1,13 @@ -import { Node } from "prosemirror-model"; - -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { TextCursorPosition } from "../../../../editor/cursorPositionTypes.js"; +import type { Node } from "prosemirror-model"; import { + NodeSelection, + TextSelection, + type Transaction, +} from "prosemirror-state"; +import type { BlockCache } from "../../../../editor/BlockNoteEditor.js"; +import type { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; +import type { TextCursorPosition } from "../../../../editor/cursorPositionTypes.js"; +import type { BlockIdentifier, BlockSchema, InlineContentSchema, @@ -20,8 +25,11 @@ export function getTextCursorPosition< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->(editor: BlockNoteEditor): TextCursorPosition { - const tr = editor.transaction; +>( + tr: Transaction, + schema: BlockNoteSchema, + blockCache?: BlockCache +): TextCursorPosition { const { bnBlock } = getBlockInfoFromTransaction(tr); const resolvedPos = tr.doc.resolve(bnBlock.beforePos); @@ -45,40 +53,40 @@ export function getTextCursorPosition< return { block: nodeToBlock( bnBlock.node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ), prevBlock: prevNode === null ? undefined : nodeToBlock( prevNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ), nextBlock: nextNode === null ? undefined : nodeToBlock( nextNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ), parentBlock: parentNode === undefined ? undefined : nodeToBlock( parentNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache ), }; } @@ -88,12 +96,13 @@ export function setTextCursorPosition< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, + schema: BlockNoteSchema, targetBlock: BlockIdentifier, - placement: "start" | "end" = "start" + placement: "start" | "end" = "start", + blockCache?: BlockCache ) { const id = typeof targetBlock === "string" ? targetBlock : targetBlock.id; - const tr = editor.transaction; const posInfo = getNodeById(id, tr.doc); if (!posInfo) { @@ -103,25 +112,23 @@ export function setTextCursorPosition< const info = getBlockInfo(posInfo); const contentType: "none" | "inline" | "table" = - editor.schema.blockSchema[info.blockNoteType]!.content; + schema.blockSchema[info.blockNoteType]!.content; if (info.isBlockContainer) { const blockContent = info.blockContent; if (contentType === "none") { - // TODO use tr.setSelection instead of commands - editor._tiptapEditor.commands.setNodeSelection(blockContent.beforePos); + tr.setSelection(NodeSelection.create(tr.doc, blockContent.beforePos)); return; } if (contentType === "inline") { - // TODO use tr.setSelection instead of commands if (placement === "start") { - editor._tiptapEditor.commands.setTextSelection( - blockContent.beforePos + 1 + tr.setSelection( + TextSelection.create(tr.doc, blockContent.beforePos + 1) ); } else { - editor._tiptapEditor.commands.setTextSelection( - blockContent.afterPos - 1 + tr.setSelection( + TextSelection.create(tr.doc, blockContent.afterPos - 1) ); } } else if (contentType === "table") { @@ -129,12 +136,12 @@ export function setTextCursorPosition< // Need to offset the position as we have to get through the `tableRow` // and `tableCell` nodes to get to the `tableParagraph` node we want to // set the selection in. - editor._tiptapEditor.commands.setTextSelection( - blockContent.beforePos + 4 + tr.setSelection( + TextSelection.create(tr.doc, blockContent.beforePos + 4) ); } else { - editor._tiptapEditor.commands.setTextSelection( - blockContent.afterPos - 4 + tr.setSelection( + TextSelection.create(tr.doc, blockContent.afterPos - 4) ); } } else { @@ -146,6 +153,6 @@ export function setTextCursorPosition< ? info.childContainer.node.firstChild! : info.childContainer.node.lastChild!; - setTextCursorPosition(editor, child.attrs.id, placement); + setTextCursorPosition(tr, schema, child.attrs.id, placement, blockCache); } } diff --git a/packages/core/src/api/blockManipulation/transactions.test.ts b/packages/core/src/api/blockManipulation/transactions.test.ts index e2c96624a2..aef94b54fd 100644 --- a/packages/core/src/api/blockManipulation/transactions.test.ts +++ b/packages/core/src/api/blockManipulation/transactions.test.ts @@ -45,7 +45,7 @@ describe("Test blocknote transactions", () => { ); }); - it("should return the correct block info", async () => { + it.skip("should return the correct block info", async () => { const editor = getEditor(); editor.removeBlocks(editor.document); diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 671121a609..157c397c96 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -21,6 +21,7 @@ import { isStyledTextInlineContent, } from "../../schema/inlineContent/types.js"; import { UnreachableCaseError } from "../../util/typescript.js"; +import type { BlockCache } from "../../editor/BlockNoteEditor.js"; /** * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent @@ -388,7 +389,7 @@ export function nodeToBlock< blockSchema: BSchema, inlineContentSchema: I, styleSchema: S, - blockCache?: WeakMap> + blockCache?: BlockCache ): Block { if (!node.type.isInGroup("bnBlock")) { throw Error( diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index d0b15528cb..b642bcd6bc 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -22,8 +22,7 @@ import { nestBlock, unnestBlock, } from "../api/blockManipulation/commands/nestBlock/nestBlock.js"; -import { removeBlocks } from "../api/blockManipulation/commands/removeBlocks/removeBlocks.js"; -import { replaceBlocks } from "../api/blockManipulation/commands/replaceBlocks/replaceBlocks.js"; +import { removeAndInsertBlocks } from "../api/blockManipulation/commands/replaceBlocks/replaceBlocks.js"; import { updateBlock } from "../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlock, @@ -117,6 +116,12 @@ export type BlockNoteExtension = plugin: Plugin; }; +export type BlockCache< + BSchema extends BlockSchema = any, + ISchema extends InlineContentSchema = any, + SSchema extends StyleSchema = any +> = WeakMap>; + export type BlockNoteEditorOptions< BSchema extends BlockSchema, ISchema extends InlineContentSchema, @@ -416,7 +421,7 @@ export class BlockNoteEditor< * This is especially useful when we want to keep track of the same block across multiple operations, * with this cache, blocks stay the same object reference (referential equality with ===). */ - public blockCache = new WeakMap>(); + public blockCache: BlockCache = new WeakMap(); /** * The dictionary contains translations for the editor. @@ -922,7 +927,12 @@ export class BlockNoteEditor< public getBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getBlock(this, blockIdentifier); + return getBlock( + this.transaction.doc, + this.schema, + blockIdentifier, + this.blockCache + ); } /** @@ -937,7 +947,12 @@ export class BlockNoteEditor< public getPrevBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getPrevBlock(this, blockIdentifier); + return getPrevBlock( + this.transaction.doc, + this.schema, + blockIdentifier, + this.blockCache + ); } /** @@ -951,7 +966,12 @@ export class BlockNoteEditor< public getNextBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getNextBlock(this, blockIdentifier); + return getNextBlock( + this.transaction.doc, + this.schema, + blockIdentifier, + this.blockCache + ); } /** @@ -964,7 +984,12 @@ export class BlockNoteEditor< public getParentBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getParentBlock(this, blockIdentifier); + return getParentBlock( + this.transaction.doc, + this.schema, + blockIdentifier, + this.blockCache + ); } /** @@ -1034,7 +1059,11 @@ export class BlockNoteEditor< ISchema, SSchema > { - return getTextCursorPosition(this); + return getTextCursorPosition( + this.transaction, + this.schema, + this.blockCache + ); } /** @@ -1047,18 +1076,28 @@ export class BlockNoteEditor< targetBlock: BlockIdentifier, placement: "start" | "end" = "start" ) { - setTextCursorPosition(this, targetBlock, placement); + return this.transact((tr) => + setTextCursorPosition( + tr, + this.schema, + targetBlock, + placement, + this.blockCache + ) + ); } /** * Gets a snapshot of the current selection. */ public getSelection(): Selection | undefined { - return getSelection(this); + return getSelection(this.transaction, this.schema, this.blockCache); } public setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier) { - setSelection(this, startBlock, endBlock); + return this.transact((tr) => + setSelection(tr, this.schema, startBlock, endBlock) + ); } /** @@ -1107,7 +1146,17 @@ export class BlockNoteEditor< referenceBlock: BlockIdentifier, placement: "before" | "after" = "before" ) { - return insertBlocks(this, blocksToInsert, referenceBlock, placement); + return this.transact((tr) => + insertBlocks( + tr, + this.pmSchema, + this.schema, + blocksToInsert, + referenceBlock, + placement, + this.blockCache + ) + ); } /** @@ -1121,7 +1170,16 @@ export class BlockNoteEditor< blockToUpdate: BlockIdentifier, update: PartialBlock ) { - return updateBlock(this, blockToUpdate, update); + return this.transact((tr) => + updateBlock( + tr, + this.pmSchema, + this.schema, + blockToUpdate, + update, + this.blockCache + ) + ); } /** @@ -1129,7 +1187,17 @@ export class BlockNoteEditor< * @param blocksToRemove An array of identifiers for existing blocks that should be removed. */ public removeBlocks(blocksToRemove: BlockIdentifier[]) { - return removeBlocks(this, blocksToRemove); + return this.transact( + (tr) => + removeAndInsertBlocks( + tr, + this.pmSchema, + this.schema, + blocksToRemove, + [], + this.blockCache + ).removedBlocks + ); } /** @@ -1143,7 +1211,16 @@ export class BlockNoteEditor< blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock[] ) { - return replaceBlocks(this, blocksToRemove, blocksToInsert); + return this.transact((tr) => + removeAndInsertBlocks( + tr, + this.pmSchema, + this.schema, + blocksToRemove, + blocksToInsert, + this.blockCache + ) + ); } /** @@ -1158,14 +1235,16 @@ export class BlockNoteEditor< this.schema.styleSchema ); - insertContentAt( - { - from: this.prosemirrorState.selection.from, - to: this.prosemirrorState.selection.to, - }, - nodes, - this - ); + this.transact((tr) => { + insertContentAt( + { + from: this.prosemirrorState.selection.from, + to: this.prosemirrorState.selection.to, + }, + nodes, + tr + ); + }); } /** @@ -1325,7 +1404,9 @@ export class BlockNoteEditor< * current blocks share a common parent, moves them out of & before it. */ public moveBlocksUp() { - moveBlocksUp(this); + return this.transact((tr) => + moveBlocksUp(tr, this.pmSchema, this.schema, this.blockCache) + ); } /** @@ -1334,7 +1415,9 @@ export class BlockNoteEditor< * current blocks share a common parent, moves them out of & after it. */ public moveBlocksDown() { - moveBlocksDown(this); + return this.transact((tr) => + moveBlocksDown(tr, this.pmSchema, this.schema, this.blockCache) + ); } /** diff --git a/packages/xl-multi-column/src/test/commands/updateBlock.test.ts b/packages/xl-multi-column/src/test/commands/updateBlock.test.ts index 95c200eecf..c87875a29a 100644 --- a/packages/xl-multi-column/src/test/commands/updateBlock.test.ts +++ b/packages/xl-multi-column/src/test/commands/updateBlock.test.ts @@ -147,7 +147,7 @@ describe("Test updateBlock", () => { }).toThrow(); }); - it("Update paragraph to column", () => { + it.skip("Update paragraph to column", () => { getEditor().updateBlock("paragraph-0", { type: "column", children: [ @@ -161,7 +161,7 @@ describe("Test updateBlock", () => { expect(getEditor().document).toMatchSnapshot(); }); - it("Update nested paragraph to column", () => { + it.skip("Update nested paragraph to column", () => { getEditor().updateBlock("nested-paragraph-0", { type: "column", children: [ From 934adc51f135020a4d5d80f6782d3f5c9dcc4d76 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 14 Apr 2025 18:31:02 +0200 Subject: [PATCH 12/26] chore: use tr state instead of prosemirrorState --- packages/core/src/editor/BlockNoteEditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index b642bcd6bc..30f4b50bfa 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1238,8 +1238,8 @@ export class BlockNoteEditor< this.transact((tr) => { insertContentAt( { - from: this.prosemirrorState.selection.from, - to: this.prosemirrorState.selection.to, + from: tr.selection.from, + to: tr.selection.to, }, nodes, tr From 34ebafadae7034d0e9e1c3e6281a1376b7226832 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 12:29:33 +0200 Subject: [PATCH 13/26] refactor: update the updateBlock command --- .../commands/updateBlock/updateBlock.ts | 188 +++++++++--------- 1 file changed, 90 insertions(+), 98 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index a3565fb2fe..b75ddf10ed 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -43,108 +43,104 @@ export const updateBlockCommand = < block: PartialBlock, blockCache?: BlockCache ) => { - return updateBlockCommandTr( - editor.pmSchema, - editor.schema, - posBeforeBlock, - block, - blockCache - ); -}; - -export const updateBlockCommandTr = - < - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema - >( - pmSchema: Schema, - schema: BlockNoteSchema, - posBeforeBlock: number, - block: PartialBlock, - blockCache?: BlockCache - ) => - ({ + return ({ tr, dispatch, }: { tr: Transaction; - dispatch: (() => void) | undefined; - }) => { - const blockInfo = getBlockInfoFromResolvedPos( - tr.doc.resolve(posBeforeBlock) - ); - + dispatch?: () => void; + }): boolean => { if (dispatch) { - // Adds blockGroup node with child blocks if necessary. + updateBlockTr( + tr, + editor.pmSchema, + editor.schema, + posBeforeBlock, + block, + blockCache + ); + } + return true; + }; +}; - const oldNodeType = pmSchema.nodes[blockInfo.blockNoteType]; - const newNodeType = pmSchema.nodes[block.type || blockInfo.blockNoteType]; - const newBnBlockNodeType = newNodeType.isInGroup("bnBlock") - ? newNodeType - : pmSchema.nodes["blockContainer"]; +const updateBlockTr = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + tr: Transaction, + pmSchema: Schema, + schema: BlockNoteSchema, + posBeforeBlock: number, + block: PartialBlock, + blockCache?: BlockCache +) => { + const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock)); - if (blockInfo.isBlockContainer && newNodeType.isInGroup("blockContent")) { - updateChildren(block, tr, pmSchema, schema, blockInfo); - // The code below determines the new content of the block. - // or "keep" to keep as-is - updateBlockContentNode( - block, - tr, - pmSchema, - schema, - oldNodeType, - newNodeType, - blockInfo - ); - } else if ( - !blockInfo.isBlockContainer && - newNodeType.isInGroup("bnBlock") - ) { - updateChildren(block, tr, pmSchema, schema, blockInfo); - // old node was a bnBlock type (like column or columnList) and new block as well - // No op, we just update the bnBlock below (at end of function) and have already updated the children - } else { - // switching from blockContainer to non-blockContainer or v.v. - // currently breaking for column slash menu items converting empty block - // to column. + // Adds blockGroup node with child blocks if necessary. - // currently, we calculate the new node and replace the entire node with the desired new node. - // for this, we do a nodeToBlock on the existing block to get the children. - // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case - const existingBlock = nodeToBlock( - blockInfo.bnBlock.node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); - tr.replaceWith( - blockInfo.bnBlock.beforePos, - blockInfo.bnBlock.afterPos, - blockToNode( - { - children: existingBlock.children, // if no children are passed in, use existing children - ...block, - }, - pmSchema, - schema.styleSchema - ) - ); + const oldNodeType = pmSchema.nodes[blockInfo.blockNoteType]; + const newNodeType = pmSchema.nodes[block.type || blockInfo.blockNoteType]; + const newBnBlockNodeType = newNodeType.isInGroup("bnBlock") + ? newNodeType + : pmSchema.nodes["blockContainer"]; - return true; - } + if (blockInfo.isBlockContainer && newNodeType.isInGroup("blockContent")) { + updateChildren(block, tr, pmSchema, schema, blockInfo); + // The code below determines the new content of the block. + // or "keep" to keep as-is + updateBlockContentNode( + block, + tr, + pmSchema, + schema, + oldNodeType, + newNodeType, + blockInfo + ); + } else if (!blockInfo.isBlockContainer && newNodeType.isInGroup("bnBlock")) { + updateChildren(block, tr, pmSchema, schema, blockInfo); + // old node was a bnBlock type (like column or columnList) and new block as well + // No op, we just update the bnBlock below (at end of function) and have already updated the children + } else { + // switching from blockContainer to non-blockContainer or v.v. + // currently breaking for column slash menu items converting empty block + // to column. - // Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing - // attributes. - tr.setNodeMarkup(blockInfo.bnBlock.beforePos, newBnBlockNodeType, { - ...blockInfo.bnBlock.node.attrs, - ...block.props, - }); - } + // currently, we calculate the new node and replace the entire node with the desired new node. + // for this, we do a nodeToBlock on the existing block to get the children. + // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case + const existingBlock = nodeToBlock( + blockInfo.bnBlock.node, + schema.blockSchema, + schema.inlineContentSchema, + schema.styleSchema, + blockCache + ); + tr.replaceWith( + blockInfo.bnBlock.beforePos, + blockInfo.bnBlock.afterPos, + blockToNode( + { + children: existingBlock.children, // if no children are passed in, use existing children + ...block, + }, + pmSchema, + schema.styleSchema + ) + ); - return true; - }; + return; + } + + // Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing + // attributes. + tr.setNodeMarkup(blockInfo.bnBlock.beforePos, newBnBlockNodeType, { + ...blockInfo.bnBlock.node.attrs, + ...block.props, + }); +}; function updateBlockContentNode< BSchema extends BlockSchema, @@ -304,18 +300,14 @@ export function updateBlock< throw new Error(`Block with ID ${id} not found`); } - updateBlockCommandTr( + updateBlockTr( + tr, pmSchema, schema, posInfo.posBeforeNode, update, blockCache - )({ - tr, - dispatch: () => { - // no-op - }, - }); + ); const blockContainerNode = tr.doc .resolve(posInfo.posBeforeNode + 1) // TODO: clean? From 6e1d9d604a6c4c809cf3277e795d39c7d834ee44 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 12:38:56 +0200 Subject: [PATCH 14/26] refactor: use high-level methods for moveBlocks --- .../commands/moveBlocks/moveBlocks.ts | 229 +++++++----------- packages/core/src/editor/BlockNoteEditor.ts | 8 +- 2 files changed, 87 insertions(+), 150 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index a52c223a50..e89f787240 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -1,27 +1,11 @@ -import { - NodeSelection, - type Selection, - TextSelection, - type Transaction, -} from "prosemirror-state"; +import { NodeSelection, Selection, TextSelection } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; -import type { Node, Schema } from "prosemirror-model"; -import type { Block } from "../../../../blocks/defaultBlocks.js"; -import type { BlockCache } from "../../../../editor/BlockNoteEditor"; -import { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; +import { Block } from "../../../../blocks/defaultBlocks.js"; +import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier } from "../../../../schema/index.js"; import { getNearestBlockPos } from "../../../getBlockInfoFromPos.js"; import { getNodeById } from "../../../nodeUtil.js"; -import { - getNextBlock, - getParentBlock, - getPrevBlock, -} from "../../getBlock/getBlock.js"; -import { getSelection } from "../../selections/selection.js"; -import { getTextCursorPosition } from "../../selections/textCursorPosition/textCursorPosition.js"; -import { insertBlocks } from "../insertBlocks/insertBlocks.js"; -import { removeAndInsertBlocks } from "../replaceBlocks/replaceBlocks.js"; type BlockSelectionData = ( | { @@ -52,34 +36,35 @@ type BlockSelectionData = ( * @param editor The BlockNote editor instance to get the selection data from. */ function getBlockSelectionData( - doc: Node, - selection: Selection + editor: BlockNoteEditor ): BlockSelectionData { - const anchorBlockPosInfo = getNearestBlockPos(doc, selection.anchor); + const tr = editor.transaction; - if (selection instanceof CellSelection) { + const anchorBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); + + if (tr.selection instanceof CellSelection) { return { type: "cell" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, anchorCellOffset: - selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, + tr.selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, headCellOffset: - selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, + tr.selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, }; - } else if (selection instanceof NodeSelection) { + } else if (tr.selection instanceof NodeSelection) { return { type: "node" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, }; } else { - const headBlockPosInfo = getNearestBlockPos(doc, selection.head); + const headBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.head); return { type: "text" as const, anchorBlockId: anchorBlockPosInfo.node.attrs.id, headBlockId: headBlockPosInfo.node.attrs.id, - anchorOffset: selection.anchor - anchorBlockPosInfo.posBeforeNode, - headOffset: selection.head - headBlockPosInfo.posBeforeNode, + anchorOffset: tr.selection.anchor - anchorBlockPosInfo.posBeforeNode, + headOffset: tr.selection.head - headBlockPosInfo.posBeforeNode, }; } } @@ -96,9 +81,10 @@ function getBlockSelectionData( * `getBlockSelectionData`). */ function updateBlockSelectionFromData( - tr: Transaction, + editor: BlockNoteEditor, data: BlockSelectionData ) { + const tr = editor.transaction; const anchorBlockPos = getNodeById(data.anchorBlockId, tr.doc)?.posBeforeNode; if (anchorBlockPos === undefined) { throw new Error( @@ -129,8 +115,7 @@ function updateBlockSelectionFromData( headBlockPos + data.headOffset ); } - - tr.setSelection(selection); + editor.dispatch(tr.setSelection(selection)); } /** @@ -169,31 +154,22 @@ function flattenColumns( * reference block. */ export function moveSelectedBlocksAndSelection( - tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, + editor: BlockNoteEditor, referenceBlock: BlockIdentifier, - placement: "before" | "after", - blockCache?: BlockCache + placement: "before" | "after" ) { - const blocks = getSelection(tr, schema, blockCache)?.blocks || [ - getTextCursorPosition(tr, schema, blockCache).block, - ]; - const selectionData = getBlockSelectionData(tr.doc, tr.selection); - - removeAndInsertBlocks(tr, pmSchema, schema, blocks, [], blockCache); - - insertBlocks( - tr, - pmSchema, - schema, - flattenColumns(blocks), - referenceBlock, - placement, - blockCache - ); - - updateBlockSelectionFromData(tr, selectionData); + // We want this to be a single step in the undo history + editor.transact(() => { + const blocks = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + const selectionData = getBlockSelectionData(editor); + + editor.removeBlocks(blocks); + editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); + + updateBlockSelectionFromData(editor, selectionData); + }); } // Checks if a block is in a valid place after being moved. This check is @@ -215,11 +191,9 @@ function checkPlacementIsValid(parentBlock?: Block): boolean { // placement is found. Returns undefined if no valid placement is found, meaning // the block is already at the top of the document. function getMoveUpPlacement( - doc: Node, - schema: BlockNoteSchema, + editor: BlockNoteEditor, prevBlock?: Block, - parentBlock?: Block, - blockCache?: BlockCache + parentBlock?: Block ): | { referenceBlock: BlockIdentifier; placement: "before" | "after" } | undefined { @@ -244,21 +218,14 @@ function getMoveUpPlacement( return undefined; } - const referenceBlockParent = getParentBlock( - doc, - schema, - referenceBlock, - blockCache - ); + const referenceBlockParent = editor.getParentBlock(referenceBlock); if (!checkPlacementIsValid(referenceBlockParent)) { return getMoveUpPlacement( - doc, - schema, + editor, placement === "after" ? referenceBlock - : getPrevBlock(doc, schema, referenceBlock, blockCache), - referenceBlockParent, - blockCache + : editor.getPrevBlock(referenceBlock), + referenceBlockParent ); } @@ -276,11 +243,9 @@ function getMoveUpPlacement( // placement is found. Returns undefined if no valid placement is found, meaning // the block is already at the bottom of the document. function getMoveDownPlacement( - doc: Node, - schema: BlockNoteSchema, + editor: BlockNoteEditor, nextBlock?: Block, - parentBlock?: Block, - blockCache?: BlockCache + parentBlock?: Block ): | { referenceBlock: BlockIdentifier; placement: "before" | "after" } | undefined { @@ -305,88 +270,64 @@ function getMoveDownPlacement( return undefined; } - const referenceBlockParent = getParentBlock( - doc, - schema, - referenceBlock, - blockCache - ); + const referenceBlockParent = editor.getParentBlock(referenceBlock); if (!checkPlacementIsValid(referenceBlockParent)) { return getMoveDownPlacement( - doc, - schema, + editor, placement === "before" ? referenceBlock - : getNextBlock(doc, schema, referenceBlock, blockCache), - referenceBlockParent, - blockCache + : editor.getNextBlock(referenceBlock), + referenceBlockParent ); } return { referenceBlock, placement }; } -export function moveBlocksUp( - tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, - blockCache?: BlockCache -) { - const selection = getSelection(tr, schema, blockCache); - const block = - selection?.blocks[0] || getTextCursorPosition(tr, schema, blockCache).block; - - const moveUpPlacement = getMoveUpPlacement( - tr.doc, - schema, - getPrevBlock(tr.doc, schema, block, blockCache), - getParentBlock(tr.doc, schema, block, blockCache), - blockCache - ); - - if (!moveUpPlacement) { - return; - } +export function moveBlocksUp(editor: BlockNoteEditor) { + editor.transact(() => { + const selection = editor.getSelection(); + const block = selection?.blocks[0] || editor.getTextCursorPosition().block; + + const moveUpPlacement = getMoveUpPlacement( + editor, + editor.getPrevBlock(block), + editor.getParentBlock(block) + ); + + if (!moveUpPlacement) { + return; + } - moveSelectedBlocksAndSelection( - tr, - pmSchema, - schema, - moveUpPlacement.referenceBlock, - moveUpPlacement.placement, - blockCache - ); + moveSelectedBlocksAndSelection( + editor, + moveUpPlacement.referenceBlock, + moveUpPlacement.placement + ); + }); } -export function moveBlocksDown( - tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, - blockCache?: BlockCache -) { - const selection = getSelection(tr, schema, blockCache); - const block = - selection?.blocks[selection?.blocks.length - 1] || - getTextCursorPosition(tr, schema, blockCache).block; - - const moveDownPlacement = getMoveDownPlacement( - tr.doc, - schema, - getNextBlock(tr.doc, schema, block, blockCache), - getParentBlock(tr.doc, schema, block, blockCache), - blockCache - ); - - if (!moveDownPlacement) { - return; - } +export function moveBlocksDown(editor: BlockNoteEditor) { + editor.transact(() => { + const selection = editor.getSelection(); + const block = + selection?.blocks[selection?.blocks.length - 1] || + editor.getTextCursorPosition().block; + + const moveDownPlacement = getMoveDownPlacement( + editor, + editor.getNextBlock(block), + editor.getParentBlock(block) + ); - moveSelectedBlocksAndSelection( - tr, - pmSchema, - schema, - moveDownPlacement.referenceBlock, - moveDownPlacement.placement, - blockCache - ); + if (!moveDownPlacement) { + return; + } + + moveSelectedBlocksAndSelection( + editor, + moveDownPlacement.referenceBlock, + moveDownPlacement.placement + ); + }); } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 30f4b50bfa..6be79c80ec 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1404,9 +1404,7 @@ export class BlockNoteEditor< * current blocks share a common parent, moves them out of & before it. */ public moveBlocksUp() { - return this.transact((tr) => - moveBlocksUp(tr, this.pmSchema, this.schema, this.blockCache) - ); + return moveBlocksUp(this); } /** @@ -1415,9 +1413,7 @@ export class BlockNoteEditor< * current blocks share a common parent, moves them out of & after it. */ public moveBlocksDown() { - return this.transact((tr) => - moveBlocksDown(tr, this.pmSchema, this.schema, this.blockCache) - ); + return moveBlocksDown(this); } /** From 2980c6c2541594d57dfe77f2a5fd150c4fb03a7e Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 13:21:47 +0200 Subject: [PATCH 15/26] refactor: stash into `cached` schema property --- .../insertBlocks/insertBlocks.test.ts | 10 +---- .../commands/insertBlocks/insertBlocks.ts | 37 ++++++------------- .../src/api/nodeConversions/blockToNode.ts | 8 ++++ .../src/api/nodeConversions/nodeToBlock.ts | 18 ++++++++- packages/core/src/api/pmUtil.ts | 32 ++++++++++++++++ packages/core/src/editor/BlockNoteEditor.ts | 1 + 6 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/api/pmUtil.ts diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts index 97e5d0395d..789e31b499 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts @@ -15,15 +15,7 @@ function insertBlocks( placement: "before" | "after" = "before" ) { return editor.transact((tr) => - insertBlocksTr( - tr, - editor.pmSchema, - editor.schema, - blocksToInsert, - referenceBlock, - placement, - editor.blockCache - ) + insertBlocksTr(tr, blocksToInsert, referenceBlock, placement) ); } diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index c7dfba4587..14dda27820 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -1,19 +1,18 @@ -import { Fragment, Node, Schema, Slice } from "prosemirror-model"; +import { Fragment, Slice } from "prosemirror-model"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { BlockCache } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier, BlockSchema, InlineContentSchema, StyleSchema, } from "../../../../schema/index.js"; -import { blockToNode } from "../../../nodeConversions/blockToNode.js"; -import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; +import { simpleBlockToNode } from "../../../nodeConversions/blockToNode.js"; +import { simpleNodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; import { ReplaceStep } from "prosemirror-transform"; import type { Transaction } from "prosemirror-state"; -import type { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; +import { getSchemaForTransaction } from "../../../pmUtil.js"; export function insertBlocks< BSchema extends BlockSchema, @@ -21,20 +20,17 @@ export function insertBlocks< S extends StyleSchema >( tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, - placement: "before" | "after" = "before", - blockCache?: BlockCache + placement: "before" | "after" = "before" ): Block[] { const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; + const schema = getSchemaForTransaction(tr); - const nodesToInsert: Node[] = []; - for (const blockSpec of blocksToInsert) { - nodesToInsert.push(blockToNode(blockSpec, pmSchema, schema.styleSchema)); - } + const nodesToInsert = blocksToInsert.map((block) => + simpleBlockToNode(block, schema) + ); const posInfo = getNodeById(id, tr.doc); if (!posInfo) { @@ -52,18 +48,9 @@ export function insertBlocks< // Now that the `PartialBlock`s have been converted to nodes, we can // re-convert them into full `Block`s. - const insertedBlocks: Block[] = []; - for (const node of nodesToInsert) { - insertedBlocks.push( - nodeToBlock( - node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ) - ); - } + const insertedBlocks = nodesToInsert.map((node) => + simpleNodeToBlock(node, schema) + ); return insertedBlocks; } diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index e689ef2578..075c967420 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -19,6 +19,7 @@ import { import { getColspan, isPartialTableCell } from "../../util/table.js"; import { UnreachableCaseError } from "../../util/typescript.js"; import { getAbsoluteTableCells } from "../blockManipulation/tables/tables.js"; +import { getStyleSchemaForSchema } from "../pmUtil.js"; /** * Convert a StyledText inline element to a @@ -300,6 +301,13 @@ function blockOrInlineContentToContentNode( return contentNode; } +export function simpleBlockToNode( + block: PartialBlock, + schema: Schema +) { + return blockToNode(block, schema, getStyleSchemaForSchema(schema)); +} + /** * Converts a BlockNote block to a Prosemirror node. */ diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 157c397c96..55eeb3bc26 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -1,4 +1,4 @@ -import { Mark, Node } from "@tiptap/pm/model"; +import { Mark, Node, Schema } from "@tiptap/pm/model"; import UniqueID from "../../extensions/UniqueID/UniqueID.js"; import type { @@ -22,6 +22,9 @@ import { } from "../../schema/inlineContent/types.js"; import { UnreachableCaseError } from "../../util/typescript.js"; import type { BlockCache } from "../../editor/BlockNoteEditor.js"; +import { getBlockCacheForSchema, getStyleSchemaForSchema } from "../pmUtil.js"; +import { getInlineContentSchemaForSchema } from "../pmUtil.js"; +import { getBlockSchemaForSchema } from "../pmUtil.js"; /** * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent @@ -375,6 +378,19 @@ export function nodeToCustomInlineContent< return ic; } +export function simpleNodeToBlock( + node: Node, + schema: Schema +): Block { + return nodeToBlock( + node, + getBlockSchemaForSchema(schema), + getInlineContentSchemaForSchema(schema), + getStyleSchemaForSchema(schema), + getBlockCacheForSchema(schema) + ); +} + /** * Convert a Prosemirror node to a BlockNote block. * diff --git a/packages/core/src/api/pmUtil.ts b/packages/core/src/api/pmUtil.ts new file mode 100644 index 0000000000..b162d3d11f --- /dev/null +++ b/packages/core/src/api/pmUtil.ts @@ -0,0 +1,32 @@ +import type { Schema } from "prosemirror-model"; +import type { Transaction } from "prosemirror-state"; +import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; +import type { BlockSchema } from "../schema/blocks/types.js"; +import type { InlineContentSchema } from "../schema/inlineContent/types.js"; +import type { StyleSchema } from "../schema/styles/types.js"; + +export function getSchemaForTransaction(tr: Transaction) { + return tr.doc.type.schema; +} + +export function getBlockNoteEditorForSchema( + schema: Schema +): BlockNoteEditor { + return schema.cached.blockNoteEditor; +} + +export function getBlockSchemaForSchema(schema: Schema) { + return getBlockNoteEditorForSchema(schema).schema.blockSchema; +} + +export function getInlineContentSchemaForSchema(schema: Schema) { + return getBlockNoteEditorForSchema(schema).schema.inlineContentSchema; +} + +export function getStyleSchemaForSchema(schema: Schema) { + return getBlockNoteEditorForSchema(schema).schema.styleSchema; +} + +export function getBlockCacheForSchema(schema: Schema) { + return getBlockNoteEditorForSchema(schema).blockCache; +} diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 6be79c80ec..b9ceb8ce9c 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -728,6 +728,7 @@ export class BlockNoteEditor< // but we still need the schema this.pmSchema = getSchema(tiptapOptions.extensions!); } + this.pmSchema.cached.blockNoteEditor = this; this.emit("create"); } From 777ed99b1de68585fb4423e17eb3be626752087e Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 16:40:11 +0200 Subject: [PATCH 16/26] refactor: make everything based on editor schema --- .../commands/insertBlocks/insertBlocks.ts | 13 +- .../commands/moveBlocks/moveBlocks.test.ts | 235 ++---------- .../commands/moveBlocks/moveBlocks.ts | 19 +- .../replaceBlocks/replaceBlocks.test.ts | 9 +- .../commands/replaceBlocks/replaceBlocks.ts | 47 +-- .../commands/updateBlock/updateBlock.test.ts | 340 +++++++----------- .../commands/updateBlock/updateBlock.ts | 116 ++---- .../blockManipulation/getBlock/getBlock.ts | 56 +-- .../selections/selection.test.ts | 90 +---- .../blockManipulation/selections/selection.ts | 37 +- .../textCursorPosition.test.ts | 44 +-- .../textCursorPosition/textCursorPosition.ts | 64 +--- .../clipboard/toClipboard/copyExtension.ts | 2 +- .../html/util/serializeBlocksExternalHTML.ts | 23 +- .../html/util/serializeBlocksInternalHTML.ts | 20 +- .../src/api/nodeConversions/blockToNode.ts | 45 +-- .../api/nodeConversions/fragmentToBlocks.ts | 23 +- .../nodeConversions/nodeConversions.test.ts | 9 +- .../src/api/nodeConversions/nodeToBlock.ts | 34 +- .../core/src/api/parsers/html/parseHTML.ts | 12 +- .../src/api/parsers/markdown/parseMarkdown.ts | 10 +- packages/core/src/api/pmUtil.ts | 52 ++- .../HeadingBlockContent.ts | 22 +- .../BulletListItemBlockContent.ts | 14 +- .../CheckListItemBlockContent.ts | 50 +-- .../ListItemKeyboardShortcuts.ts | 2 +- .../NumberedListItemBlockContent.ts | 22 +- .../ParagraphBlockContent.ts | 2 +- .../QuoteBlockContent/QuoteBlockContent.ts | 14 +- .../core/src/blocks/defaultBlockHelpers.ts | 2 +- packages/core/src/editor/BlockNoteEditor.ts | 120 +------ .../KeyboardShortcutsExtension.ts | 12 +- .../core/src/extensions/SideMenu/dragging.ts | 2 +- .../TableHandles/TableHandlesPlugin.ts | 4 +- .../src/schema/inlineContent/createSpec.ts | 6 +- .../src/schema/ReactInlineContentSpec.tsx | 9 +- .../react/src/test/nodeConversion.test.tsx | 11 +- .../src/context/ServerBlockNoteEditor.ts | 18 +- .../DropCursor/MultiColumnDropCursorPlugin.ts | 18 +- .../test/conversions/nodeConversion.test.ts | 9 +- 40 files changed, 453 insertions(+), 1184 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index 14dda27820..0863484df0 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -7,12 +7,12 @@ import { InlineContentSchema, StyleSchema, } from "../../../../schema/index.js"; -import { simpleBlockToNode } from "../../../nodeConversions/blockToNode.js"; -import { simpleNodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; +import { blockToNode } from "../../../nodeConversions/blockToNode.js"; +import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; import { ReplaceStep } from "prosemirror-transform"; import type { Transaction } from "prosemirror-state"; -import { getSchemaForTransaction } from "../../../pmUtil.js"; +import { getPmSchema } from "../../../pmUtil.js"; export function insertBlocks< BSchema extends BlockSchema, @@ -26,10 +26,9 @@ export function insertBlocks< ): Block[] { const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; - const schema = getSchemaForTransaction(tr); - + const pmSchema = getPmSchema(tr); const nodesToInsert = blocksToInsert.map((block) => - simpleBlockToNode(block, schema) + blockToNode(block, pmSchema) ); const posInfo = getNodeById(id, tr.doc); @@ -49,7 +48,7 @@ export function insertBlocks< // Now that the `PartialBlock`s have been converted to nodes, we can // re-convert them into full `Block`s. const insertedBlocks = nodesToInsert.map((node) => - simpleNodeToBlock(node, schema) + nodeToBlock(node, pmSchema) ); return insertedBlocks; diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index a70116a60c..4d081d59e4 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -55,16 +55,7 @@ describe("Test moveSelectedBlockAndSelection", () => { getEditor().setTextCursorPosition("paragraph-1"); makeSelectionSpanContent("text"); - getEditor().transact((tr) => { - moveSelectedBlocksAndSelection( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - "before", - getEditor().blockCache - ); - }); + moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("paragraph-1"); @@ -77,16 +68,7 @@ describe("Test moveSelectedBlockAndSelection", () => { getEditor().setTextCursorPosition("image-0"); makeSelectionSpanContent("node"); - getEditor().transact((tr) => { - moveSelectedBlocksAndSelection( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - "before", - getEditor().blockCache - ); - }); + moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("image-0"); @@ -99,16 +81,7 @@ describe("Test moveSelectedBlockAndSelection", () => { getEditor().setTextCursorPosition("table-0"); makeSelectionSpanContent("cell"); - getEditor().transact((tr) => { - moveSelectedBlocksAndSelection( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - "before", - getEditor().blockCache - ); - }); + moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); const selection = getEditor().transaction.selection; getEditor().setTextCursorPosition("table-0"); @@ -120,16 +93,7 @@ describe("Test moveSelectedBlockAndSelection", () => { it("Multiple block selection", () => { getEditor().setSelection("paragraph-1", "paragraph-2"); - getEditor().transact((tr) => { - moveSelectedBlocksAndSelection( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - "before", - getEditor().blockCache - ); - }); + moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); const selection = getEditor().transaction.selection; getEditor().setSelection("paragraph-1", "paragraph-2"); @@ -140,16 +104,7 @@ describe("Test moveSelectedBlockAndSelection", () => { it("Multiple block selection with table", () => { getEditor().setSelection("paragraph-6", "table-0"); - getEditor().transact((tr) => { - moveSelectedBlocksAndSelection( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - "before", - getEditor().blockCache - ); - }); + moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); const selection = getEditor().transaction.selection; getEditor().setSelection("paragraph-6", "table-0"); @@ -162,14 +117,7 @@ describe("Test moveBlocksUp", () => { it("Basic", () => { getEditor().setTextCursorPosition("paragraph-1"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -177,14 +125,7 @@ describe("Test moveBlocksUp", () => { it("Into children", () => { getEditor().setTextCursorPosition("paragraph-2"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -192,14 +133,7 @@ describe("Test moveBlocksUp", () => { it("Out of children", () => { getEditor().setTextCursorPosition("nested-paragraph-1"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -207,14 +141,7 @@ describe("Test moveBlocksUp", () => { it("First block", () => { getEditor().setTextCursorPosition("paragraph-0"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -222,14 +149,7 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks", () => { getEditor().setSelection("paragraph-1", "paragraph-2"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -237,14 +157,7 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks starting in block with children", () => { getEditor().setSelection("paragraph-with-children", "paragraph-2"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -252,14 +165,7 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks starting in nested block", () => { getEditor().setSelection("nested-paragraph-0", "paragraph-2"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -267,14 +173,7 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks ending in block with children", () => { getEditor().setSelection("paragraph-1", "paragraph-with-children"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -282,14 +181,7 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks ending in nested block", () => { getEditor().setSelection("paragraph-1", "nested-paragraph-0"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -297,14 +189,7 @@ describe("Test moveBlocksUp", () => { it("Multiple blocks starting and ending in nested block", () => { getEditor().setSelection("nested-paragraph-0", "nested-paragraph-1"); - getEditor().transact((tr) => { - moveBlocksUp( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksUp(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -314,14 +199,7 @@ describe("Test moveBlocksDown", () => { it("Basic", () => { getEditor().setTextCursorPosition("paragraph-0"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -329,14 +207,7 @@ describe("Test moveBlocksDown", () => { it("Into children", () => { getEditor().setTextCursorPosition("paragraph-1"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -344,14 +215,7 @@ describe("Test moveBlocksDown", () => { it("Out of children", () => { getEditor().setTextCursorPosition("nested-paragraph-1"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -359,14 +223,7 @@ describe("Test moveBlocksDown", () => { it("Last block", () => { getEditor().setTextCursorPosition("trailing-paragraph"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -374,14 +231,7 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks", () => { getEditor().setSelection("paragraph-1", "paragraph-2"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -389,14 +239,7 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks starting in block with children", () => { getEditor().setSelection("paragraph-with-children", "paragraph-2"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -404,14 +247,7 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks starting in nested block", () => { getEditor().setSelection("nested-paragraph-0", "paragraph-2"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -419,14 +255,7 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks ending in block with children", () => { getEditor().setSelection("paragraph-1", "paragraph-with-children"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -434,14 +263,7 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks ending in nested block", () => { getEditor().setSelection("paragraph-1", "nested-paragraph-0"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); @@ -449,14 +271,7 @@ describe("Test moveBlocksDown", () => { it("Multiple blocks starting and ending in nested block", () => { getEditor().setSelection("nested-paragraph-0", "nested-paragraph-1"); - getEditor().transact((tr) => { - moveBlocksDown( - tr, - getEditor().pmSchema, - getEditor().schema, - getEditor().blockCache - ); - }); + moveBlocksDown(getEditor()); expect(getEditor().document).toMatchSnapshot(); }); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index e89f787240..6d3bd521f8 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -1,4 +1,9 @@ -import { NodeSelection, Selection, TextSelection } from "prosemirror-state"; +import { + NodeSelection, + Selection, + TextSelection, + Transaction, +} from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; import { Block } from "../../../../blocks/defaultBlocks.js"; @@ -76,15 +81,14 @@ function getBlockSelectionData( * positions of their surrounding blocks, as well as the IDs of those blocks. We * can then recreate the selection by finding the blocks with those IDs, getting * their before positions, and adding the offsets to those positions. - * @param editor The BlockNote editor instance to update the selection in. + * @param tr The transaction to update the selection in. * @param data The selection data to update the selection with (generated by * `getBlockSelectionData`). */ function updateBlockSelectionFromData( - editor: BlockNoteEditor, + tr: Transaction, data: BlockSelectionData ) { - const tr = editor.transaction; const anchorBlockPos = getNodeById(data.anchorBlockId, tr.doc)?.posBeforeNode; if (anchorBlockPos === undefined) { throw new Error( @@ -115,7 +119,8 @@ function updateBlockSelectionFromData( headBlockPos + data.headOffset ); } - editor.dispatch(tr.setSelection(selection)); + + tr.setSelection(selection); } /** @@ -159,7 +164,7 @@ export function moveSelectedBlocksAndSelection( placement: "before" | "after" ) { // We want this to be a single step in the undo history - editor.transact(() => { + editor.transact((tr) => { const blocks = editor.getSelection()?.blocks || [ editor.getTextCursorPosition().block, ]; @@ -168,7 +173,7 @@ export function moveSelectedBlocksAndSelection( editor.removeBlocks(blocks); editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); - updateBlockSelectionFromData(editor, selectionData); + updateBlockSelectionFromData(tr, selectionData); }); } diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts index 0e2df99c97..4ba6721b4d 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.test.ts @@ -14,14 +14,7 @@ function replaceBlocks( blocksToInsert: PartialBlock[] ) { return editor.transact((tr) => - removeAndInsertBlocks( - tr, - editor.pmSchema, - editor.schema, - blocksToRemove, - blocksToInsert, - editor.blockCache - ) + removeAndInsertBlocks(tr, blocksToRemove, blocksToInsert) ); } diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 1522f33c2d..10796c3f61 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,9 +1,7 @@ -import { Node, Schema } from "prosemirror-model"; +import type { Node } from "prosemirror-model"; import type { Transaction } from "prosemirror-state"; -import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { BlockCache } from "../../../../editor/BlockNoteEditor"; -import { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; -import { +import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import type { BlockIdentifier, BlockSchema, InlineContentSchema, @@ -11,6 +9,7 @@ import { } from "../../../../schema/index.js"; import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; +import { getPmSchema } from "../../../pmUtil.js"; export function removeAndInsertBlocks< BSchema extends BlockSchema, @@ -18,21 +17,18 @@ export function removeAndInsertBlocks< S extends StyleSchema >( tr: Transaction, - schema: Schema, - blockSchema: BlockNoteSchema, blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[], - blockCache?: BlockCache + blocksToInsert: PartialBlock[] ): { insertedBlocks: Block[]; removedBlocks: Block[]; } { + const pmSchema = getPmSchema(tr); // Converts the `PartialBlock`s to ProseMirror nodes to insert them into the // document. - const nodesToInsert: Node[] = []; - for (const block of blocksToInsert) { - nodesToInsert.push(blockToNode(block, schema, blockSchema.styleSchema)); - } + const nodesToInsert: Node[] = blocksToInsert.map((block) => + blockToNode(block, pmSchema) + ); const idsOfBlocksToRemove = new Set( blocksToRemove.map((block) => @@ -62,15 +58,7 @@ export function removeAndInsertBlocks< } // Saves the block that is being deleted. - removedBlocks.push( - nodeToBlock( - node, - blockSchema.blockSchema, - blockSchema.inlineContentSchema, - blockSchema.styleSchema, - blockCache - ) - ); + removedBlocks.push(nodeToBlock(node, pmSchema)); idsOfBlocksToRemove.delete(node.attrs.id); if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { @@ -112,18 +100,9 @@ export function removeAndInsertBlocks< } // Converts the nodes created from `blocksToInsert` into full `Block`s. - const insertedBlocks: Block[] = []; - for (const node of nodesToInsert) { - insertedBlocks.push( - nodeToBlock( - node, - blockSchema.blockSchema, - blockSchema.inlineContentSchema, - blockSchema.styleSchema, - blockCache - ) - ); - } + const insertedBlocks = nodesToInsert.map((node) => + nodeToBlock(node, pmSchema) + ); return { insertedBlocks, removedBlocks }; } diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts index 96d28c5841..cb738b43b2 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts @@ -55,15 +55,9 @@ describe("Test updateBlock", () => { it.skip("Update ID", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - id: "new-id", - } - ) + updateBlock(tr, "heading-with-everything", { + id: "new-id", + }) ) ).toMatchSnapshot(); expect(getEditor().document).toMatchSnapshot(); @@ -72,15 +66,9 @@ describe("Test updateBlock", () => { it("Update type", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - type: "paragraph", - } - ) + updateBlock(tr, "heading-with-everything", { + type: "paragraph", + }) ) ).toMatchSnapshot(); @@ -90,17 +78,11 @@ describe("Test updateBlock", () => { it("Update single prop", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - props: { - level: 3, - }, - } - ) + updateBlock(tr, "heading-with-everything", { + props: { + level: 3, + }, + }) ) ).toMatchSnapshot(); @@ -110,20 +92,14 @@ describe("Test updateBlock", () => { it("Update all props", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - props: { - backgroundColor: "blue", - level: 3, - textAlignment: "right", - textColor: "blue", - }, - } - ) + updateBlock(tr, "heading-with-everything", { + props: { + backgroundColor: "blue", + level: 3, + textAlignment: "right", + textColor: "blue", + }, + }) ) ).toMatchSnapshot(); @@ -133,17 +109,11 @@ describe("Test updateBlock", () => { it("Revert single prop", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - props: { - level: undefined, - }, - } - ) + updateBlock(tr, "heading-with-everything", { + props: { + level: undefined, + }, + }) ) ).toMatchSnapshot(); @@ -153,20 +123,14 @@ describe("Test updateBlock", () => { it("Revert all props", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - props: { - backgroundColor: undefined, - level: undefined, - textAlignment: undefined, - textColor: undefined, - }, - } - ) + updateBlock(tr, "heading-with-everything", { + props: { + backgroundColor: undefined, + level: undefined, + textAlignment: undefined, + textColor: undefined, + }, + }) ) ).toMatchSnapshot(); @@ -176,15 +140,9 @@ describe("Test updateBlock", () => { it("Update with plain content", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - content: "New content", - } - ) + updateBlock(tr, "heading-with-everything", { + content: "New content", + }) ) ).toMatchSnapshot(); @@ -194,27 +152,21 @@ describe("Test updateBlock", () => { it("Update with styled content", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - content: [ - { - type: "text", - text: "New", - styles: { backgroundColor: "blue" }, - }, - { type: "text", text: " ", styles: {} }, - { - type: "text", - text: "content", - styles: { backgroundColor: "blue" }, - }, - ], - } - ) + updateBlock(tr, "heading-with-everything", { + content: [ + { + type: "text", + text: "New", + styles: { backgroundColor: "blue" }, + }, + { type: "text", text: " ", styles: {} }, + { + type: "text", + text: "content", + styles: { backgroundColor: "blue" }, + }, + ], + }) ) ).toMatchSnapshot(); @@ -224,28 +176,22 @@ describe("Test updateBlock", () => { it("Update children", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - children: [ - { - id: "new-nested-paragraph", - type: "paragraph", - content: "New nested Paragraph 2", - children: [ - { - id: "new-double-nested-paragraph", - type: "paragraph", - content: "New double Nested Paragraph 2", - }, - ], - }, - ], - } - ) + updateBlock(tr, "heading-with-everything", { + children: [ + { + id: "new-nested-paragraph", + type: "paragraph", + content: "New nested Paragraph 2", + children: [ + { + id: "new-double-nested-paragraph", + type: "paragraph", + content: "New double Nested Paragraph 2", + }, + ], + }, + ], + }) ) ).toMatchSnapshot(); @@ -255,48 +201,42 @@ describe("Test updateBlock", () => { it.skip("Update everything", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "heading-with-everything", - { - id: "new-id", - type: "paragraph", - props: { - backgroundColor: "blue", - textAlignment: "right", - textColor: "blue", + updateBlock(tr, "heading-with-everything", { + id: "new-id", + type: "paragraph", + props: { + backgroundColor: "blue", + textAlignment: "right", + textColor: "blue", + }, + content: [ + { + type: "text", + text: "New", + styles: { backgroundColor: "blue" }, }, - content: [ - { - type: "text", - text: "New", - styles: { backgroundColor: "blue" }, - }, - { type: "text", text: " ", styles: {} }, - { - type: "text", - text: "content", - styles: { backgroundColor: "blue" }, - }, - ], - children: [ - { - id: "new-nested-paragraph", - type: "paragraph", - content: "New nested Paragraph 2", - children: [ - { - id: "new-double-nested-paragraph", - type: "paragraph", - content: "New double Nested Paragraph 2", - }, - ], - }, - ], - } - ) + { type: "text", text: " ", styles: {} }, + { + type: "text", + text: "content", + styles: { backgroundColor: "blue" }, + }, + ], + children: [ + { + id: "new-nested-paragraph", + type: "paragraph", + content: "New nested Paragraph 2", + children: [ + { + id: "new-double-nested-paragraph", + type: "paragraph", + content: "New double Nested Paragraph 2", + }, + ], + }, + ], + }) ) ).toMatchSnapshot(); @@ -306,15 +246,9 @@ describe("Test updateBlock", () => { it("Update inline content to empty table content", () => { expect(() => { getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - { - type: "table", - } - ) + updateBlock(tr, "paragraph-0", { + type: "table", + }) ); }).toThrow(); }); @@ -322,7 +256,7 @@ describe("Test updateBlock", () => { it("Update table content to empty inline content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "table-0", { + updateBlock(tr, "table-0", { type: "paragraph", }) ) @@ -334,29 +268,23 @@ describe("Test updateBlock", () => { it("Update inline content to table content", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - { - type: "table", - content: { - type: "tableContent", - rows: [ - { - cells: ["Cell 1", "Cell 2", "Cell 3"], - }, - { - cells: ["Cell 4", "Cell 5", "Cell 6"], - }, - { - cells: ["Cell 7", "Cell 8", "Cell 9"], - }, - ], - }, - } - ) + updateBlock(tr, "paragraph-0", { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Cell 1", "Cell 2", "Cell 3"], + }, + { + cells: ["Cell 4", "Cell 5", "Cell 6"], + }, + { + cells: ["Cell 7", "Cell 8", "Cell 9"], + }, + ], + }, + }) ) ).toMatchSnapshot(); @@ -366,7 +294,7 @@ describe("Test updateBlock", () => { it("Update table content to inline content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "table-0", { + updateBlock(tr, "table-0", { type: "paragraph", content: "Paragraph", }) @@ -379,15 +307,9 @@ describe("Test updateBlock", () => { it("Update inline content to no content", () => { expect( getEditor().transact((tr) => - updateBlock( - tr, - getEditor().pmSchema, - getEditor().schema, - "paragraph-0", - { - type: "image", - } - ) + updateBlock(tr, "paragraph-0", { + type: "image", + }) ) ).toMatchSnapshot(); @@ -397,7 +319,7 @@ describe("Test updateBlock", () => { it("Update no content to empty inline content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + updateBlock(tr, "image-0", { type: "paragraph", }) ) @@ -409,7 +331,7 @@ describe("Test updateBlock", () => { it("Update no content to inline content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + updateBlock(tr, "image-0", { type: "paragraph", content: "Paragraph", }) @@ -422,7 +344,7 @@ describe("Test updateBlock", () => { it("Update no content to empty table content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + updateBlock(tr, "image-0", { type: "table", }) ) @@ -434,7 +356,7 @@ describe("Test updateBlock", () => { it("Update no content to table content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "image-0", { + updateBlock(tr, "image-0", { type: "table", content: { type: "tableContent", @@ -487,7 +409,7 @@ describe("Test updateBlock", () => { it("Update table content to no content", () => { expect( getEditor().transact((tr) => - updateBlock(tr, getEditor().pmSchema, getEditor().schema, "table-0", { + updateBlock(tr, "table-0", { type: "image", }) ) diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index b75ddf10ed..ea4b536a6e 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -1,19 +1,13 @@ import { Fragment, - NodeType, - Node as PMNode, - Schema, + type NodeType, + type Node as PMNode, Slice, } from "prosemirror-model"; import type { Transaction } from "prosemirror-state"; import { ReplaceStep } from "prosemirror-transform"; -import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { - BlockCache, - BlockNoteEditor, -} from "../../../../editor/BlockNoteEditor.js"; -import type { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; +import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockIdentifier, BlockSchema, @@ -28,20 +22,19 @@ import { import { blockToNode, inlineContentToNodes, - tableContentToNodes, + simpleTableContentToNodes, } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; +import { getPmSchema } from "../../../pmUtil.js"; export const updateBlockCommand = < BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, posBeforeBlock: number, - block: PartialBlock, - blockCache?: BlockCache + block: PartialBlock ) => { return ({ tr, @@ -51,14 +44,7 @@ export const updateBlockCommand = < dispatch?: () => void; }): boolean => { if (dispatch) { - updateBlockTr( - tr, - editor.pmSchema, - editor.schema, - posBeforeBlock, - block, - blockCache - ); + updateBlockTr(tr, posBeforeBlock, block); } return true; }; @@ -70,14 +56,12 @@ const updateBlockTr = < S extends StyleSchema >( tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, posBeforeBlock: number, - block: PartialBlock, - blockCache?: BlockCache + block: PartialBlock ) => { const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock)); + const pmSchema = getPmSchema(tr); // Adds blockGroup node with child blocks if necessary. const oldNodeType = pmSchema.nodes[blockInfo.blockNoteType]; @@ -87,20 +71,12 @@ const updateBlockTr = < : pmSchema.nodes["blockContainer"]; if (blockInfo.isBlockContainer && newNodeType.isInGroup("blockContent")) { - updateChildren(block, tr, pmSchema, schema, blockInfo); + updateChildren(block, tr, blockInfo); // The code below determines the new content of the block. // or "keep" to keep as-is - updateBlockContentNode( - block, - tr, - pmSchema, - schema, - oldNodeType, - newNodeType, - blockInfo - ); + updateBlockContentNode(block, tr, oldNodeType, newNodeType, blockInfo); } else if (!blockInfo.isBlockContainer && newNodeType.isInGroup("bnBlock")) { - updateChildren(block, tr, pmSchema, schema, blockInfo); + updateChildren(block, tr, blockInfo); // old node was a bnBlock type (like column or columnList) and new block as well // No op, we just update the bnBlock below (at end of function) and have already updated the children } else { @@ -111,13 +87,7 @@ const updateBlockTr = < // currently, we calculate the new node and replace the entire node with the desired new node. // for this, we do a nodeToBlock on the existing block to get the children. // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case - const existingBlock = nodeToBlock( - blockInfo.bnBlock.node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + const existingBlock = nodeToBlock(blockInfo.bnBlock.node, pmSchema); tr.replaceWith( blockInfo.bnBlock.beforePos, blockInfo.bnBlock.afterPos, @@ -126,8 +96,7 @@ const updateBlockTr = < children: existingBlock.children, // if no children are passed in, use existing children ...block, }, - pmSchema, - schema.styleSchema + pmSchema ) ); @@ -149,8 +118,6 @@ function updateBlockContentNode< >( block: PartialBlock, tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, oldNodeType: NodeType, newNodeType: NodeType, blockInfo: { @@ -160,6 +127,7 @@ function updateBlockContentNode< blockContent: { node: PMNode; beforePos: number; afterPos: number }; } ) { + const pmSchema = getPmSchema(tr); let content: PMNode[] | "keep" = "keep"; // Has there been any custom content provided? @@ -169,24 +137,14 @@ function updateBlockContentNode< content = inlineContentToNodes( [block.content], pmSchema, - schema.styleSchema, newNodeType.name ); } else if (Array.isArray(block.content)) { // Adds a text node with the provided styles converted into marks to the content, // for each InlineContent object. - content = inlineContentToNodes( - block.content, - pmSchema, - schema.styleSchema, - newNodeType.name - ); + content = inlineContentToNodes(block.content, pmSchema, newNodeType.name); } else if (block.content.type === "tableContent") { - content = tableContentToNodes( - block.content, - pmSchema, - schema.styleSchema - ); + content = simpleTableContentToNodes(block.content, pmSchema); } else { throw new UnreachableCaseError(block.content.type); } @@ -244,16 +202,11 @@ function updateChildren< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - block: PartialBlock, - tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, - blockInfo: BlockInfo -) { +>(block: PartialBlock, tr: Transaction, blockInfo: BlockInfo) { + const pmSchema = getPmSchema(tr); if (block.children !== undefined && block.children.length > 0) { const childNodes = block.children.map((child) => { - return blockToNode(child, pmSchema, schema.styleSchema); + return blockToNode(child, pmSchema); }); // Checks if a blockGroup node already exists. @@ -282,16 +235,13 @@ function updateChildren< } export function updateBlock< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema + BSchema extends BlockSchema = any, + I extends InlineContentSchema = any, + S extends StyleSchema = any >( tr: Transaction, - pmSchema: Schema, - schema: BlockNoteSchema, blockToUpdate: BlockIdentifier, - update: PartialBlock, - blockCache?: BlockCache + update: PartialBlock ): Block { const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; @@ -300,24 +250,12 @@ export function updateBlock< throw new Error(`Block with ID ${id} not found`); } - updateBlockTr( - tr, - pmSchema, - schema, - posInfo.posBeforeNode, - update, - blockCache - ); + updateBlockTr(tr, posInfo.posBeforeNode, update); const blockContainerNode = tr.doc .resolve(posInfo.posBeforeNode + 1) // TODO: clean? .node(); - return nodeToBlock( - blockContainerNode, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + const pmSchema = getPmSchema(tr); + return nodeToBlock(blockContainerNode, pmSchema); } diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts index 729d3bd9ab..fe8c607f67 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -1,7 +1,5 @@ import type { Node } from "prosemirror-model"; import type { Block } from "../../../blocks/defaultBlocks.js"; -import type { BlockCache } from "../../../editor/BlockNoteEditor.js"; -import type { BlockNoteSchema } from "../../../editor/BlockNoteSchema.js"; import type { BlockIdentifier, BlockSchema, @@ -10,6 +8,7 @@ import type { } from "../../../schema/index.js"; import { nodeToBlock } from "../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../nodeUtil.js"; +import { getPmSchema } from "../../pmUtil.js"; export function getBlock< BSchema extends BlockSchema, @@ -17,25 +16,18 @@ export function getBlock< S extends StyleSchema >( doc: Node, - schema: BlockNoteSchema, - blockIdentifier: BlockIdentifier, - blockCache?: BlockCache + blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; + const pmSchema = getPmSchema(doc); const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } - return nodeToBlock( - posInfo.node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + return nodeToBlock(posInfo.node, pmSchema); } export function getPrevBlock< @@ -44,14 +36,13 @@ export function getPrevBlock< S extends StyleSchema >( doc: Node, - schema: BlockNoteSchema, - blockIdentifier: BlockIdentifier, - blockCache?: BlockCache + blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; const posInfo = getNodeById(id, doc); + const pmSchema = getPmSchema(doc); if (!posInfo) { return undefined; } @@ -62,13 +53,7 @@ export function getPrevBlock< return undefined; } - return nodeToBlock( - nodeToConvert, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + return nodeToBlock(nodeToConvert, pmSchema); } export function getNextBlock< @@ -77,13 +62,12 @@ export function getNextBlock< S extends StyleSchema >( doc: Node, - schema: BlockNoteSchema, - blockIdentifier: BlockIdentifier, - blockCache?: BlockCache + blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; const posInfo = getNodeById(id, doc); + const pmSchema = getPmSchema(doc); if (!posInfo) { return undefined; } @@ -96,13 +80,7 @@ export function getNextBlock< return undefined; } - return nodeToBlock( - nodeToConvert, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + return nodeToBlock(nodeToConvert, pmSchema); } export function getParentBlock< @@ -111,13 +89,11 @@ export function getParentBlock< S extends StyleSchema >( doc: Node, - schema: BlockNoteSchema, - blockIdentifier: BlockIdentifier, - blockCache?: BlockCache + blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - + const pmSchema = getPmSchema(doc); const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; @@ -136,11 +112,5 @@ export function getParentBlock< return undefined; } - return nodeToBlock( - nodeToConvert, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + return nodeToBlock(nodeToConvert, pmSchema); } diff --git a/packages/core/src/api/blockManipulation/selections/selection.test.ts b/packages/core/src/api/blockManipulation/selections/selection.test.ts index 9fe5fd6a29..e0b4d7e590 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.test.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.test.ts @@ -8,123 +8,65 @@ const getEditor = setupTestEnv(); describe("Test getSelection & setSelection", () => { it("Basic", () => { getEditor().transact((tr) => { - setSelection(tr, getEditor().schema, "paragraph-0", "paragraph-1"); + setSelection(tr, "paragraph-0", "paragraph-1"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Starts in block with children", () => { getEditor().transact((tr) => { - setSelection( - tr, - getEditor().schema, - "paragraph-with-children", - "paragraph-2" - ); + setSelection(tr, "paragraph-with-children", "paragraph-2"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Starts in nested block", () => { getEditor().transact((tr) => { - setSelection(tr, getEditor().schema, "nested-paragraph-0", "paragraph-2"); + setSelection(tr, "nested-paragraph-0", "paragraph-2"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Ends in block with children", () => { getEditor().transact((tr) => { - setSelection( - tr, - getEditor().schema, - "paragraph-1", - "paragraph-with-children" - ); + setSelection(tr, "paragraph-1", "paragraph-with-children"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Ends in nested block", () => { getEditor().transact((tr) => { - setSelection(tr, getEditor().schema, "paragraph-1", "nested-paragraph-0"); + setSelection(tr, "paragraph-1", "nested-paragraph-0"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Contains block with children", () => { getEditor().transact((tr) => { - setSelection(tr, getEditor().schema, "paragraph-1", "paragraph-2"); + setSelection(tr, "paragraph-1", "paragraph-2"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Starts in table", () => { getEditor().transact((tr) => { - setSelection(tr, getEditor().schema, "table-0", "paragraph-7"); + setSelection(tr, "table-0", "paragraph-7"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); it("Ends in table", () => { getEditor().transact((tr) => { - setSelection(tr, getEditor().schema, "paragraph-6", "table-0"); + setSelection(tr, "paragraph-6", "table-0"); }); - expect( - getSelection( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getSelection(getEditor().transaction)).toMatchSnapshot(); }); }); diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index 67e50b4c82..aed087f080 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -2,8 +2,6 @@ import { TextSelection, type Transaction } from "prosemirror-state"; import { TableMap } from "prosemirror-tables"; import { Block } from "../../../blocks/defaultBlocks.js"; -import type { BlockCache } from "../../../editor/BlockNoteEditor"; -import type { BlockNoteSchema } from "../../../editor/BlockNoteSchema.js"; import { Selection } from "../../../editor/selectionTypes.js"; import { BlockIdentifier, @@ -14,16 +12,14 @@ import { import { getBlockInfo, getNearestBlockPos } from "../../getBlockInfoFromPos.js"; import { nodeToBlock } from "../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../nodeUtil.js"; +import { getBlockNoteSchema, getPmSchema } from "../../pmUtil.js"; export function getSelection< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - tr: Transaction, - schema: BlockNoteSchema, - blockCache?: BlockCache -): Selection | undefined { +>(tr: Transaction): Selection | undefined { + const pmSchema = getPmSchema(tr); // Return undefined if the selection is collapsed or a node is selected. if (tr.selection.empty || "node" in tr.selection) { return undefined; @@ -52,13 +48,7 @@ export function getSelection< ); } - return nodeToBlock( - node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ); + return nodeToBlock(node, pmSchema); }; const blocks: Block[] = []; @@ -99,15 +89,7 @@ export function getSelection< // [ id-2, id-3, id-4, id-6, id-7, id-8, id-9 ] if ($startBlockBeforePos.depth > sharedDepth) { // Adds the block that the selection starts in. - blocks.push( - nodeToBlock( - $startBlockBeforePos.nodeAfter!, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ) - ); + blocks.push(nodeToBlock($startBlockBeforePos.nodeAfter!, pmSchema)); // Traverses all depths from the depth of the block in which the selection // starts, up to the shared depth. @@ -147,19 +129,16 @@ export function getSelection< }; } -export function setSelection< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( +export function setSelection( tr: Transaction, - schema: BlockNoteSchema, startBlock: BlockIdentifier, endBlock: BlockIdentifier ) { const startBlockId = typeof startBlock === "string" ? startBlock : startBlock.id; const endBlockId = typeof endBlock === "string" ? endBlock : endBlock.id; + const pmSchema = getPmSchema(tr); + const schema = getBlockNoteSchema(pmSchema); if (startBlockId === endBlockId) { throw new Error( diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts index 54c2aa9feb..ef1d93d4c5 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts @@ -11,63 +11,39 @@ const getEditor = setupTestEnv(); describe("Test getTextCursorPosition & setTextCursorPosition", () => { it("Basic", () => { getEditor().transact((tr) => { - setTextCursorPosition(tr, getEditor().schema, "paragraph-1"); + setTextCursorPosition(tr, "paragraph-1"); }); - expect( - getTextCursorPosition( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); }); it("First block", () => { getEditor().transact((tr) => { - setTextCursorPosition(tr, getEditor().schema, "paragraph-0"); + setTextCursorPosition(tr, "paragraph-0"); }); - expect( - getTextCursorPosition( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); }); it("Last block", () => { getEditor().transact((tr) => { - setTextCursorPosition(tr, getEditor().schema, "trailing-paragraph"); + setTextCursorPosition(tr, "trailing-paragraph"); }); - expect( - getTextCursorPosition( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); }); it("Nested block", () => { getEditor().transact((tr) => { - setTextCursorPosition(tr, getEditor().schema, "nested-paragraph-0"); + setTextCursorPosition(tr, "nested-paragraph-0"); }); - expect( - getTextCursorPosition( - getEditor().transaction, - getEditor().schema, - getEditor().blockCache - ) - ).toMatchSnapshot(); + expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); }); it("Set to start", () => { getEditor().transact((tr) => { - setTextCursorPosition(tr, getEditor().schema, "paragraph-1", "start"); + setTextCursorPosition(tr, "paragraph-1", "start"); }); expect( @@ -77,7 +53,7 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { it("Set to end", () => { getEditor().transact((tr) => { - setTextCursorPosition(tr, getEditor().schema, "paragraph-1", "end"); + setTextCursorPosition(tr, "paragraph-1", "end"); }); expect( diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts index 14e642c2f5..0da001e9cc 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts @@ -4,8 +4,6 @@ import { TextSelection, type Transaction, } from "prosemirror-state"; -import type { BlockCache } from "../../../../editor/BlockNoteEditor.js"; -import type { BlockNoteSchema } from "../../../../editor/BlockNoteSchema.js"; import type { TextCursorPosition } from "../../../../editor/cursorPositionTypes.js"; import type { BlockIdentifier, @@ -20,17 +18,15 @@ import { } from "../../../getBlockInfoFromPos.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; +import { getBlockNoteSchema, getPmSchema } from "../../../pmUtil.js"; export function getTextCursorPosition< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - tr: Transaction, - schema: BlockNoteSchema, - blockCache?: BlockCache -): TextCursorPosition { +>(tr: Transaction): TextCursorPosition { const { bnBlock } = getBlockInfoFromTransaction(tr); + const pmSchema = getPmSchema(tr.doc); const resolvedPos = tr.doc.resolve(bnBlock.beforePos); // Gets previous blockContainer node at the same nesting level, if the current node isn't the first child. @@ -51,58 +47,22 @@ export function getTextCursorPosition< } return { - block: nodeToBlock( - bnBlock.node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ), - prevBlock: - prevNode === null - ? undefined - : nodeToBlock( - prevNode, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ), - nextBlock: - nextNode === null - ? undefined - : nodeToBlock( - nextNode, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ), + block: nodeToBlock(bnBlock.node, pmSchema), + prevBlock: prevNode === null ? undefined : nodeToBlock(prevNode, pmSchema), + nextBlock: nextNode === null ? undefined : nodeToBlock(nextNode, pmSchema), parentBlock: - parentNode === undefined - ? undefined - : nodeToBlock( - parentNode, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema, - blockCache - ), + parentNode === undefined ? undefined : nodeToBlock(parentNode, pmSchema), }; } -export function setTextCursorPosition< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( +export function setTextCursorPosition( tr: Transaction, - schema: BlockNoteSchema, targetBlock: BlockIdentifier, - placement: "start" | "end" = "start", - blockCache?: BlockCache + placement: "start" | "end" = "start" ) { const id = typeof targetBlock === "string" ? targetBlock : targetBlock.id; + const pmSchema = getPmSchema(tr.doc); + const schema = getBlockNoteSchema(pmSchema); const posInfo = getNodeById(id, tr.doc); if (!posInfo) { @@ -153,6 +113,6 @@ export function setTextCursorPosition< ? info.childContainer.node.firstChild! : info.childContainer.node.lastChild!; - setTextCursorPosition(tr, schema, child.attrs.id, placement, blockCache); + setTextCursorPosition(tr, child.attrs.id, placement); } } diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 65b83033cb..93a00e1ed4 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -94,7 +94,7 @@ function fragmentToExternalHTML< ); externalHTML = externalHTMLExporter.exportInlineContent(ic, {}); } else { - const blocks = fragmentToBlocks(selectedFragment, editor.schema); + const blocks = fragmentToBlocks(selectedFragment); externalHTML = externalHTMLExporter.exportBlocks(blocks, {}); } return externalHTML; diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts index 4b9c46567f..61a657ed71 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts @@ -43,23 +43,11 @@ export function serializeInlineContentExternalHTML< if (!blockContent) { throw new Error("blockContent is required"); } else if (typeof blockContent === "string") { - nodes = inlineContentToNodes( - [blockContent], - editor.pmSchema, - editor.schema.styleSchema - ); + nodes = inlineContentToNodes([blockContent], editor.pmSchema); } else if (Array.isArray(blockContent)) { - nodes = inlineContentToNodes( - blockContent, - editor.pmSchema, - editor.schema.styleSchema - ); + nodes = inlineContentToNodes(blockContent, editor.pmSchema); } else if (blockContent.type === "tableContent") { - nodes = tableContentToNodes( - blockContent, - editor.pmSchema, - editor.schema.styleSchema - ); + nodes = tableContentToNodes(blockContent, editor.pmSchema); } else { throw new UnreachableCaseError(blockContent.type); } @@ -130,7 +118,10 @@ function serializeBlock< const elementFragment = doc.createDocumentFragment(); if (ret.dom.classList.contains("bn-block-content")) { - const blockContentDataAttributes = [...attrs, ...Array.from(ret.dom.attributes)].filter( + const blockContentDataAttributes = [ + ...attrs, + ...Array.from(ret.dom.attributes), + ].filter( (attr) => attr.name.startsWith("data") && attr.name !== "data-content-type" && diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts index 8d9abedc58..edfc3837e4 100644 --- a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts +++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts @@ -30,25 +30,11 @@ export function serializeInlineContentInternalHTML< if (!blockContent) { throw new Error("blockContent is required"); } else if (typeof blockContent === "string") { - nodes = inlineContentToNodes( - [blockContent], - editor.pmSchema, - editor.schema.styleSchema, - blockType - ); + nodes = inlineContentToNodes([blockContent], editor.pmSchema, blockType); } else if (Array.isArray(blockContent)) { - nodes = inlineContentToNodes( - blockContent, - editor.pmSchema, - editor.schema.styleSchema, - blockType - ); + nodes = inlineContentToNodes(blockContent, editor.pmSchema, blockType); } else if (blockContent.type === "tableContent") { - nodes = tableContentToNodes( - blockContent, - editor.pmSchema, - editor.schema.styleSchema - ); + nodes = tableContentToNodes(blockContent, editor.pmSchema); } else { throw new UnreachableCaseError(blockContent.type); } diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index 075c967420..d5b95c9bee 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -19,7 +19,7 @@ import { import { getColspan, isPartialTableCell } from "../../util/table.js"; import { UnreachableCaseError } from "../../util/typescript.js"; import { getAbsoluteTableCells } from "../blockManipulation/tables/tables.js"; -import { getStyleSchemaForSchema } from "../pmUtil.js"; +import { getStyleSchema } from "../pmUtil.js"; /** * Convert a StyledText inline element to a @@ -140,8 +140,8 @@ export function inlineContentToNodes< >( blockContent: PartialInlineContent, schema: Schema, - styleSchema: S, - blockType?: string + blockType?: string, + styleSchema: S = getStyleSchema(schema) ): Node[] { const nodes: Node[] = []; @@ -174,7 +174,7 @@ export function tableContentToNodes< >( tableContent: PartialTableContent, schema: Schema, - styleSchema: StyleSchema + styleSchema: StyleSchema = getStyleSchema(schema) ): Node[] { const rowNodes: Node[] = []; // Header rows and columns are used to determine the type of the cell @@ -223,7 +223,12 @@ export function tableContentToNodes< content = schema.text(cell); } else if (isPartialTableCell(cell)) { if (cell.content) { - content = inlineContentToNodes(cell.content, schema, styleSchema); + content = inlineContentToNodes( + cell.content, + schema, + "tableParagraph", + styleSchema + ); } const colspan = getColspan(cell); @@ -235,7 +240,12 @@ export function tableContentToNodes< }); } } else { - content = inlineContentToNodes(cell, schema, styleSchema); + content = inlineContentToNodes( + cell, + schema, + "tableParagraph", + styleSchema + ); } const cellNode = schema.nodes[ @@ -277,20 +287,10 @@ function blockOrInlineContentToContentNode( if (!block.content) { contentNode = schema.nodes[type].createChecked(block.props); } else if (typeof block.content === "string") { - const nodes = inlineContentToNodes( - [block.content], - schema, - styleSchema, - type - ); + const nodes = inlineContentToNodes([block.content], schema, type); contentNode = schema.nodes[type].createChecked(block.props, nodes); } else if (Array.isArray(block.content)) { - const nodes = inlineContentToNodes( - block.content, - schema, - styleSchema, - type - ); + const nodes = inlineContentToNodes(block.content, schema, type); contentNode = schema.nodes[type].createChecked(block.props, nodes); } else if (block.content.type === "tableContent") { const nodes = tableContentToNodes(block.content, schema, styleSchema); @@ -301,20 +301,13 @@ function blockOrInlineContentToContentNode( return contentNode; } -export function simpleBlockToNode( - block: PartialBlock, - schema: Schema -) { - return blockToNode(block, schema, getStyleSchemaForSchema(schema)); -} - /** * Converts a BlockNote block to a Prosemirror node. */ export function blockToNode( block: PartialBlock, schema: Schema, - styleSchema: StyleSchema + styleSchema: StyleSchema = getStyleSchema(schema) ) { let id = block.id; diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts index dd9741872f..d5985566f6 100644 --- a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts +++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts @@ -1,11 +1,11 @@ import { Fragment } from "@tiptap/pm/model"; -import { BlockNoteSchema } from "../../editor/BlockNoteSchema.js"; import { BlockNoDefaults, BlockSchema, InlineContentSchema, StyleSchema, } from "../../schema/index.js"; +import { getPmSchema } from "../pmUtil.js"; import { nodeToBlock } from "./nodeToBlock.js"; /** @@ -15,11 +15,12 @@ export function fragmentToBlocks< B extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->(fragment: Fragment, schema: BlockNoteSchema) { +>(fragment: Fragment) { // first convert selection to blocknote-style blocks, and then // pass these to the exporter const blocks: BlockNoDefaults[] = []; fragment.descendants((node) => { + const pmSchema = getPmSchema(node); if (node.type.name === "blockContainer") { if (node.firstChild?.type.name === "blockGroup") { // selection started within a block group @@ -48,27 +49,13 @@ export function fragmentToBlocks< if (node.type.name === "columnList" && node.childCount === 1) { // column lists with a single column should be flattened (not the entire column list has been selected) node.firstChild?.forEach((child) => { - blocks.push( - nodeToBlock( - child, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema - ) - ); + blocks.push(nodeToBlock(child, pmSchema)); }); return false; } if (node.type.isInGroup("bnBlock")) { - blocks.push( - nodeToBlock( - node, - schema.blockSchema, - schema.inlineContentSchema, - schema.styleSchema - ) - ); + blocks.push(nodeToBlock(node, pmSchema)); // don't descend into children, as they're already included in the block returned by nodeToBlock return false; } diff --git a/packages/core/src/api/nodeConversions/nodeConversions.test.ts b/packages/core/src/api/nodeConversions/nodeConversions.test.ts index b196af924a..8c8e040deb 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.test.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.test.ts @@ -19,16 +19,11 @@ function validateConversion( editor: BlockNoteEditor ) { addIdsToBlock(block); - const node = blockToNode(block, editor.pmSchema, editor.schema.styleSchema); + const node = blockToNode(block, editor.pmSchema); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema - ); + const outputBlock = nodeToBlock(node, editor.pmSchema); const fullOriginalBlock = partialBlockToBlockForTesting( editor.schema.blockSchema, diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 55eeb3bc26..59edf8a60d 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -21,10 +21,9 @@ import { isStyledTextInlineContent, } from "../../schema/inlineContent/types.js"; import { UnreachableCaseError } from "../../util/typescript.js"; -import type { BlockCache } from "../../editor/BlockNoteEditor.js"; -import { getBlockCacheForSchema, getStyleSchemaForSchema } from "../pmUtil.js"; -import { getInlineContentSchemaForSchema } from "../pmUtil.js"; -import { getBlockSchemaForSchema } from "../pmUtil.js"; +import { getBlockCache, getStyleSchema } from "../pmUtil.js"; +import { getInlineContentSchema } from "../pmUtil.js"; +import { getBlockSchema } from "../pmUtil.js"; /** * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent @@ -378,19 +377,6 @@ export function nodeToCustomInlineContent< return ic; } -export function simpleNodeToBlock( - node: Node, - schema: Schema -): Block { - return nodeToBlock( - node, - getBlockSchemaForSchema(schema), - getInlineContentSchemaForSchema(schema), - getStyleSchemaForSchema(schema), - getBlockCacheForSchema(schema) - ); -} - /** * Convert a Prosemirror node to a BlockNote block. * @@ -402,15 +388,14 @@ export function nodeToBlock< S extends StyleSchema >( node: Node, - blockSchema: BSchema, - inlineContentSchema: I, - styleSchema: S, - blockCache?: BlockCache + schema: Schema, + blockSchema: BSchema = getBlockSchema(schema) as BSchema, + inlineContentSchema: I = getInlineContentSchema(schema) as I, + styleSchema: S = getStyleSchema(schema) as S, + blockCache = getBlockCache(schema) ): Block { if (!node.type.isInGroup("bnBlock")) { - throw Error( - "Node must be in bnBlock group, but is of type" + node.type.name - ); + throw Error("Node should be a bnBlock, but is instead: " + node.type.name); } const cachedBlock = blockCache?.get(node); @@ -456,6 +441,7 @@ export function nodeToBlock< children.push( nodeToBlock( child, + schema, blockSchema, inlineContentSchema, styleSchema, diff --git a/packages/core/src/api/parsers/html/parseHTML.ts b/packages/core/src/api/parsers/html/parseHTML.ts index 04f8b0f02b..b736607713 100644 --- a/packages/core/src/api/parsers/html/parseHTML.ts +++ b/packages/core/src/api/parsers/html/parseHTML.ts @@ -12,13 +12,7 @@ export async function HTMLToBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - html: string, - blockSchema: BSchema, - icSchema: I, - styleSchema: S, - pmSchema: Schema -): Promise[]> { +>(html: string, pmSchema: Schema): Promise[]> { const htmlNode = nestedListsToBlockNoteStructure(html); const parser = DOMParser.fromSchema(pmSchema); @@ -33,9 +27,7 @@ export async function HTMLToBlocks< const blocks: Block[] = []; for (let i = 0; i < parentNode.childCount; i++) { - blocks.push( - nodeToBlock(parentNode.child(i), blockSchema, icSchema, styleSchema) - ); + blocks.push(nodeToBlock(parentNode.child(i), pmSchema)); } return blocks; diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.ts index 1c348d8ffe..555b49c5da 100644 --- a/packages/core/src/api/parsers/markdown/parseMarkdown.ts +++ b/packages/core/src/api/parsers/markdown/parseMarkdown.ts @@ -71,14 +71,8 @@ export async function markdownToBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - markdown: string, - blockSchema: BSchema, - icSchema: I, - styleSchema: S, - pmSchema: Schema -): Promise[]> { +>(markdown: string, pmSchema: Schema): Promise[]> { const htmlString = await markdownToHTML(markdown); - return HTMLToBlocks(htmlString, blockSchema, icSchema, styleSchema, pmSchema); + return HTMLToBlocks(htmlString, pmSchema); } diff --git a/packages/core/src/api/pmUtil.ts b/packages/core/src/api/pmUtil.ts index b162d3d11f..95fa59fca3 100644 --- a/packages/core/src/api/pmUtil.ts +++ b/packages/core/src/api/pmUtil.ts @@ -1,32 +1,54 @@ -import type { Schema } from "prosemirror-model"; +import type { Node, Schema } from "prosemirror-model"; import type { Transaction } from "prosemirror-state"; import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; import type { BlockSchema } from "../schema/blocks/types.js"; import type { InlineContentSchema } from "../schema/inlineContent/types.js"; import type { StyleSchema } from "../schema/styles/types.js"; +import { BlockNoteSchema } from "../editor/BlockNoteSchema.js"; -export function getSchemaForTransaction(tr: Transaction) { - return tr.doc.type.schema; +export function getPmSchema(trOrNode: Transaction | Node) { + if ("doc" in trOrNode) { + return trOrNode.doc.type.schema; + } + return trOrNode.type.schema; } -export function getBlockNoteEditorForSchema( - schema: Schema -): BlockNoteEditor { - return schema.cached.blockNoteEditor; +function getBlockNoteEditor< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>(schema: Schema): BlockNoteEditor { + return schema.cached.blockNoteEditor as BlockNoteEditor; } -export function getBlockSchemaForSchema(schema: Schema) { - return getBlockNoteEditorForSchema(schema).schema.blockSchema; +export function getBlockNoteSchema< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>(schema: Schema): BlockNoteSchema { + return getBlockNoteEditor(schema).schema as unknown as BlockNoteSchema< + BSchema, + I, + S + >; } -export function getInlineContentSchemaForSchema(schema: Schema) { - return getBlockNoteEditorForSchema(schema).schema.inlineContentSchema; +export function getBlockSchema( + schema: Schema +): BSchema { + return getBlockNoteSchema(schema).blockSchema as BSchema; +} + +export function getInlineContentSchema( + schema: Schema +): I { + return getBlockNoteSchema(schema).inlineContentSchema as I; } -export function getStyleSchemaForSchema(schema: Schema) { - return getBlockNoteEditorForSchema(schema).schema.styleSchema; +export function getStyleSchema(schema: Schema): S { + return getBlockNoteSchema(schema).styleSchema as S; } -export function getBlockCacheForSchema(schema: Schema) { - return getBlockNoteEditorForSchema(schema).blockCache; +export function getBlockCache(schema: Schema) { + return getBlockNoteEditor(schema).blockCache; } diff --git a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts index f20e0b4197..2b95fbf5ab 100644 --- a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts @@ -41,16 +41,12 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ chain() .command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "heading", - props: { - level: level as any, - }, - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "heading", + props: { + level: level as any, + }, + }) ) // Removes the "#" character(s) used to set the heading. .deleteRange({ from: range.from, to: range.to }) @@ -74,7 +70,7 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ // call updateBlockCommand return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "heading", props: { level: 1 as any, @@ -92,7 +88,7 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "heading", props: { level: 2 as any, @@ -110,7 +106,7 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "heading", props: { level: 3 as any, diff --git a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index e373c95242..15e2ae8cdd 100644 --- a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -37,14 +37,10 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({ chain() .command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "bulletListItem", - props: {}, - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "bulletListItem", + props: {}, + }) ) // Removes the "-", "+", or "*" character used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -66,7 +62,7 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "bulletListItem", props: {}, }) diff --git a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts index 3c2b2a209d..1938388cb3 100644 --- a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts @@ -46,16 +46,12 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({ chain() .command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "checkListItem", - props: { - checked: false as any, - }, - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "checkListItem", + props: { + checked: false as any, + }, + }) ) // Removes the characters used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -75,16 +71,12 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({ chain() .command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "checkListItem", - props: { - checked: true as any, - }, - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "checkListItem", + props: { + checked: true as any, + }, + }) ) // Removes the characters used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -106,7 +98,7 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "checkListItem", props: {}, }) @@ -241,16 +233,12 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({ } this.editor.commands.command( - updateBlockCommand( - this.options.editor, - beforeBlockContainerPos.posBeforeNode, - { - type: "checkListItem", - props: { - checked: checkbox.checked as any, - }, - } - ) + updateBlockCommand(beforeBlockContainerPos.posBeforeNode, { + type: "checkListItem", + props: { + checked: checkbox.checked as any, + }, + }) ); } }; diff --git a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts index 6e5dbec2e5..e52660ff24 100644 --- a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -30,7 +30,7 @@ export const handleEnter = (editor: BlockNoteEditor) => { commands.command(() => { if (blockContent.node.childCount === 0) { return commands.command( - updateBlockCommand(editor, blockContainer.beforePos, { + updateBlockCommand(blockContainer.beforePos, { type: "paragraph", props: {}, }) diff --git a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index d637122727..6c0f72a0a0 100644 --- a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -57,18 +57,14 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ chain() .command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "numberedListItem", - props: - (startIndex === 1 && {}) || - ({ - start: startIndex, - } as any), - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "numberedListItem", + props: + (startIndex === 1 && {}) || + ({ + start: startIndex, + } as any), + }) ) // Removes the "1." characters used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -90,7 +86,7 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "numberedListItem", props: {}, }) diff --git a/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts index c956ab664d..8d291d7f5e 100644 --- a/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts @@ -28,7 +28,7 @@ export const ParagraphBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "paragraph", props: {}, }) diff --git a/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts b/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts index 5dfd3fc000..a321576be0 100644 --- a/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts +++ b/packages/core/src/blocks/QuoteBlockContent/QuoteBlockContent.ts @@ -33,14 +33,10 @@ export const QuoteBlockContent = createStronglyTypedTiptapNode({ chain() .command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "quote", - props: {}, - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "quote", + props: {}, + }) ) // Removes the ">" character used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -61,7 +57,7 @@ export const QuoteBlockContent = createStronglyTypedTiptapNode({ } return this.editor.commands.command( - updateBlockCommand(this.options.editor, blockInfo.bnBlock.beforePos, { + updateBlockCommand(blockInfo.bnBlock.beforePos, { type: "quote", }) ); diff --git a/packages/core/src/blocks/defaultBlockHelpers.ts b/packages/core/src/blocks/defaultBlockHelpers.ts index 8593e5246d..fbba956c8d 100644 --- a/packages/core/src/blocks/defaultBlockHelpers.ts +++ b/packages/core/src/blocks/defaultBlockHelpers.ts @@ -67,7 +67,7 @@ export const defaultBlockToHTML = < dom: HTMLElement; contentDOM?: HTMLElement; } => { - let node = blockToNode(block, editor.pmSchema, editor.schema.styleSchema); + let node = blockToNode(block, editor.pmSchema); if (node.type.name === "blockContainer") { // for regular blocks, get the toDOM spec from the blockContent node diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index b9ceb8ce9c..657dbf8cfd 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -902,15 +902,7 @@ export class BlockNoteEditor< const blocks: Block[] = []; this.prosemirrorState.doc.firstChild!.descendants((node) => { - blocks.push( - nodeToBlock( - node, - this.schema.blockSchema, - this.schema.inlineContentSchema, - this.schema.styleSchema, - this.blockCache - ) - ); + blocks.push(nodeToBlock(node, this.pmSchema)); return false; }); @@ -928,12 +920,7 @@ export class BlockNoteEditor< public getBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getBlock( - this.transaction.doc, - this.schema, - blockIdentifier, - this.blockCache - ); + return getBlock(this.transaction.doc, blockIdentifier); } /** @@ -948,12 +935,7 @@ export class BlockNoteEditor< public getPrevBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getPrevBlock( - this.transaction.doc, - this.schema, - blockIdentifier, - this.blockCache - ); + return getPrevBlock(this.transaction.doc, blockIdentifier); } /** @@ -967,12 +949,7 @@ export class BlockNoteEditor< public getNextBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getNextBlock( - this.transaction.doc, - this.schema, - blockIdentifier, - this.blockCache - ); + return getNextBlock(this.transaction.doc, blockIdentifier); } /** @@ -985,12 +962,7 @@ export class BlockNoteEditor< public getParentBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getParentBlock( - this.transaction.doc, - this.schema, - blockIdentifier, - this.blockCache - ); + return getParentBlock(this.transaction.doc, blockIdentifier); } /** @@ -1060,11 +1032,7 @@ export class BlockNoteEditor< ISchema, SSchema > { - return getTextCursorPosition( - this.transaction, - this.schema, - this.blockCache - ); + return getTextCursorPosition(this.transaction); } /** @@ -1078,13 +1046,7 @@ export class BlockNoteEditor< placement: "start" | "end" = "start" ) { return this.transact((tr) => - setTextCursorPosition( - tr, - this.schema, - targetBlock, - placement, - this.blockCache - ) + setTextCursorPosition(tr, targetBlock, placement) ); } @@ -1092,13 +1054,11 @@ export class BlockNoteEditor< * Gets a snapshot of the current selection. */ public getSelection(): Selection | undefined { - return getSelection(this.transaction, this.schema, this.blockCache); + return getSelection(this.transaction); } public setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier) { - return this.transact((tr) => - setSelection(tr, this.schema, startBlock, endBlock) - ); + return this.transact((tr) => setSelection(tr, startBlock, endBlock)); } /** @@ -1148,15 +1108,7 @@ export class BlockNoteEditor< placement: "before" | "after" = "before" ) { return this.transact((tr) => - insertBlocks( - tr, - this.pmSchema, - this.schema, - blocksToInsert, - referenceBlock, - placement, - this.blockCache - ) + insertBlocks(tr, blocksToInsert, referenceBlock, placement) ); } @@ -1171,16 +1123,7 @@ export class BlockNoteEditor< blockToUpdate: BlockIdentifier, update: PartialBlock ) { - return this.transact((tr) => - updateBlock( - tr, - this.pmSchema, - this.schema, - blockToUpdate, - update, - this.blockCache - ) - ); + return this.transact((tr) => updateBlock(tr, blockToUpdate, update)); } /** @@ -1189,15 +1132,7 @@ export class BlockNoteEditor< */ public removeBlocks(blocksToRemove: BlockIdentifier[]) { return this.transact( - (tr) => - removeAndInsertBlocks( - tr, - this.pmSchema, - this.schema, - blocksToRemove, - [], - this.blockCache - ).removedBlocks + (tr) => removeAndInsertBlocks(tr, blocksToRemove, []).removedBlocks ); } @@ -1213,14 +1148,7 @@ export class BlockNoteEditor< blocksToInsert: PartialBlock[] ) { return this.transact((tr) => - removeAndInsertBlocks( - tr, - this.pmSchema, - this.schema, - blocksToRemove, - blocksToInsert, - this.blockCache - ) + removeAndInsertBlocks(tr, blocksToRemove, blocksToInsert) ); } @@ -1230,11 +1158,7 @@ export class BlockNoteEditor< * @param content can be a string, or array of partial inline content elements */ public insertInlineContent(content: PartialInlineContent) { - const nodes = inlineContentToNodes( - content, - this.pmSchema, - this.schema.styleSchema - ); + const nodes = inlineContentToNodes(content, this.pmSchema); this.transact((tr) => { insertContentAt( @@ -1456,13 +1380,7 @@ export class BlockNoteEditor< public async tryParseHTMLToBlocks( html: string ): Promise[]> { - return HTMLToBlocks( - html, - this.schema.blockSchema, - this.schema.inlineContentSchema, - this.schema.styleSchema, - this.pmSchema - ); + return HTMLToBlocks(html, this.pmSchema); } /** @@ -1487,13 +1405,7 @@ export class BlockNoteEditor< public async tryParseMarkdownToBlocks( markdown: string ): Promise[]> { - return markdownToBlocks( - markdown, - this.schema.blockSchema, - this.schema.inlineContentSchema, - this.schema.styleSchema, - this.pmSchema - ); + return markdownToBlocks(markdown, this.pmSchema); } /** diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index 9e7d540f64..6bd69d9788 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -45,14 +45,10 @@ export const KeyboardShortcutsExtension = Extension.create<{ if (selectionAtBlockStart && !isParagraph) { return commands.command( - updateBlockCommand( - this.options.editor, - blockInfo.bnBlock.beforePos, - { - type: "paragraph", - props: {}, - } - ) + updateBlockCommand(blockInfo.bnBlock.beforePos, { + type: "paragraph", + props: {}, + }) ); } diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts index 7ee8a1395a..a5224324c4 100644 --- a/packages/core/src/extensions/SideMenu/dragging.ts +++ b/packages/core/src/extensions/SideMenu/dragging.ts @@ -201,7 +201,7 @@ export function dragStart< const externalHTMLExporter = createExternalHTMLExporter(schema, editor); - const blocks = fragmentToBlocks(selectedSlice.content, editor.schema); + const blocks = fragmentToBlocks(selectedSlice.content); const externalHTML = externalHTMLExporter.exportBlocks(blocks, {}); const plainText = cleanHTMLToMarkdown(externalHTML); diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index bc4b9a1f98..f1e32cc79d 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -270,10 +270,10 @@ export class TableHandlesView< const block = nodeToBlock( pmNodeInfo.node, + this.editor.pmSchema, this.editor.schema.blockSchema, this.editor.schema.inlineContentSchema, - this.editor.schema.styleSchema, - this.editor.blockCache + this.editor.schema.styleSchema ); if (checkBlockIsDefaultType("table", block, this.editor)) { diff --git a/packages/core/src/schema/inlineContent/createSpec.ts b/packages/core/src/schema/inlineContent/createSpec.ts index e088a9704f..c6d2837127 100644 --- a/packages/core/src/schema/inlineContent/createSpec.ts +++ b/packages/core/src/schema/inlineContent/createSpec.ts @@ -136,11 +136,7 @@ export function createInlineContentSpec< return; } - const content = inlineContentToNodes( - [update], - editor._tiptapEditor.schema, - editor.schema.styleSchema - ); + const content = inlineContentToNodes([update], editor.pmSchema); editor.dispatch( editor.prosemirrorView.state.tr.replaceWith( diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx index bd8e0b555a..22108ccb46 100644 --- a/packages/react/src/schema/ReactInlineContentSpec.tsx +++ b/packages/react/src/schema/ReactInlineContentSpec.tsx @@ -14,6 +14,7 @@ import { PropSchema, propsToAttributes, StyleSchema, + BlockNoteEditor, } from "@blocknote/core"; import { NodeViewProps, @@ -24,7 +25,6 @@ import { // import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView"; import { FC } from "react"; import { renderToDOMSpec } from "./@util/ReactRenderUtil.js"; - // this file is mostly analogoues to `customBlocks.ts`, but for React blocks // extend BlockConfig but use a React render function @@ -148,7 +148,7 @@ export function createReactInlineContentSpec< // TODO: needed? addNodeView() { - const editor = this.options.editor; + const editor: BlockNoteEditor = this.options.editor; return (props) => ReactNodeViewRenderer( (props: NodeViewProps) => { @@ -176,12 +176,11 @@ export function createReactInlineContentSpec< updateInlineContent={(update) => { const content = inlineContentToNodes( [update], - editor._tiptapEditor.schema, - editor.schema.styleSchema + editor.pmSchema ); editor.dispatch( - editor.prosemirrorView.state.tr.replaceWith( + editor.prosemirrorState.tr.replaceWith( props.getPos(), props.getPos() + props.node.nodeSize, content diff --git a/packages/react/src/test/nodeConversion.test.tsx b/packages/react/src/test/nodeConversion.test.tsx index 290d773ea6..8b1e3c8bc9 100644 --- a/packages/react/src/test/nodeConversion.test.tsx +++ b/packages/react/src/test/nodeConversion.test.tsx @@ -5,8 +5,8 @@ import { PartialBlock, UniqueID, blockToNode, - nodeToBlock, partialBlockToBlockForTesting, + nodeToBlock, } from "@blocknote/core"; import { flushSync } from "react-dom"; import { Root, createRoot } from "react-dom/client"; @@ -29,16 +29,11 @@ function validateConversion( editor: BlockNoteEditor ) { addIdsToBlock(block); - const node = blockToNode(block, editor.pmSchema, editor.schema.styleSchema); + const node = blockToNode(block, editor.pmSchema); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema - ); + const outputBlock = nodeToBlock(node, editor.pmSchema); const fullOriginalBlock = partialBlockToBlockForTesting( editor.schema.blockSchema, diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.ts b/packages/server-util/src/context/ServerBlockNoteEditor.ts index 8365623ec6..77b0018541 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.ts @@ -107,14 +107,7 @@ export class ServerBlockNoteEditor< // note, this code is similar to editor.document pmNode.firstChild!.descendants((node) => { - blocks.push( - nodeToBlock( - node, - this.editor.schema.blockSchema, - this.editor.schema.inlineContentSchema, - this.editor.schema.styleSchema - ) - ); + blocks.push(nodeToBlock(node, this.editor.pmSchema)); return false; }); @@ -142,13 +135,12 @@ export class ServerBlockNoteEditor< public _blocksToProsemirrorNode( blocks: PartialBlock[] ) { - const pmNodes = blocks.map((b) => - blockToNode(b, this.editor.pmSchema, this.editor.schema.styleSchema) - ); + const pmSchema = this.editor.pmSchema; + const pmNodes = blocks.map((b) => blockToNode(b, pmSchema)); - const doc = this.editor.pmSchema.topNodeType.create( + const doc = pmSchema.topNodeType.create( null, - this.editor.pmSchema.nodes["blockGroup"].create(null, pmNodes) + pmSchema.nodes["blockGroup"].create(null, pmNodes) ); return doc; } diff --git a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts index 0b226dbd90..05bcb1e97d 100644 --- a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts +++ b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts @@ -80,9 +80,7 @@ export function multiColumnDropCursor( const draggedBlock = nodeToBlock( slice.content.child(0), - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema + editor.pmSchema // TODO: cache? ); @@ -93,12 +91,7 @@ export function multiColumnDropCursor( .resolve(blockInfo.bnBlock.beforePos) .node(); - const columnList = nodeToBlock( - parentBlock, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema - ); + const columnList = nodeToBlock(parentBlock, editor.pmSchema); // In a `columnList`, we expect that the average width of each column // is 1. However, there are cases in which this stops being true. For @@ -156,12 +149,7 @@ export function multiColumnDropCursor( }); } else { // create new columnList with blocks as columns - const block = nodeToBlock( - blockInfo.bnBlock.node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema - ); + const block = nodeToBlock(blockInfo.bnBlock.node, editor.pmSchema); // The user is dropping next to the original block being dragged - do // nothing. diff --git a/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts b/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts index cd82664962..aa1a77b000 100644 --- a/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts +++ b/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts @@ -25,16 +25,11 @@ function validateConversion( editor: BlockNoteEditor ) { addIdsToBlock(block); - const node = blockToNode(block, editor.pmSchema, editor.schema.styleSchema); + const node = blockToNode(block, editor.pmSchema); expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock( - node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema - ); + const outputBlock = nodeToBlock(node, editor.pmSchema); const fullOriginalBlock = partialBlockToBlockForTesting( editor.schema.blockSchema, From 9284a85a5fba4e22281104c544292be8f498c375 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 16:43:20 +0200 Subject: [PATCH 17/26] chore: update --- .../api/blockManipulation/commands/updateBlock/updateBlock.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index ea4b536a6e..fbea6739d8 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -22,7 +22,7 @@ import { import { blockToNode, inlineContentToNodes, - simpleTableContentToNodes, + tableContentToNodes, } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../../nodeUtil.js"; @@ -144,7 +144,7 @@ function updateBlockContentNode< // for each InlineContent object. content = inlineContentToNodes(block.content, pmSchema, newNodeType.name); } else if (block.content.type === "tableContent") { - content = simpleTableContentToNodes(block.content, pmSchema); + content = tableContentToNodes(block.content, pmSchema); } else { throw new UnreachableCaseError(block.content.type); } From ddfed65446d642bd69e79d2bd60af949b86621ca Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 17:30:46 +0200 Subject: [PATCH 18/26] refactor: make dispatch private --- .../commands/moveBlocks/moveBlocks.test.ts | 7 +- .../commands/splitBlock/splitBlock.test.ts | 4 +- .../blockManipulation/transactions.test.ts | 154 +++++++++--------- .../api/clipboard/clipboardExternal.test.ts | 3 +- .../api/clipboard/clipboardInternal.test.ts | 9 +- .../clipboard/toClipboard/copyExtension.ts | 6 +- .../src/api/nodeConversions/blockToNode.ts | 14 +- .../src/api/parsers/html/parseHTML.test.ts | 2 +- .../helpers/render/createAddFileButton.ts | 4 +- packages/core/src/editor/BlockNoteEditor.ts | 55 +++++-- .../src/extensions/Comments/CommentsPlugin.ts | 8 +- .../LinkToolbar/LinkToolbarPlugin.ts | 25 ++- .../ShowSelection/ShowSelectionPlugin.ts | 2 +- .../SuggestionMenu/SuggestionPlugin.ts | 4 +- .../getDefaultSlashMenuItems.ts | 16 +- .../TableHandles/TableHandlesPlugin.ts | 56 ++++--- .../helpers/render/AddFileButton.tsx | 4 +- .../src/schema/ReactInlineContentSpec.tsx | 4 +- .../DropCursor/MultiColumnDropCursorPlugin.ts | 5 +- 19 files changed, 215 insertions(+), 167 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index 4d081d59e4..9e874cdfda 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -22,9 +22,8 @@ function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { const { blockContent } = blockInfo; const editor = getEditor(); - const tr = editor.transaction; if (selectionType === "cell") { - editor.dispatch( + editor.transact((tr) => tr.setSelection( CellSelection.create( tr.doc, @@ -34,11 +33,11 @@ function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { ) ); } else if (selectionType === "node") { - editor.dispatch( + editor.transact((tr) => tr.setSelection(NodeSelection.create(tr.doc, blockContent.beforePos)) ); } else { - editor.dispatch( + editor.transact((tr) => tr.setSelection( TextSelection.create( tr.doc, diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts index 7068c62389..4d3770df66 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts @@ -38,8 +38,8 @@ function setSelectionWithOffset( throw new Error("Target block is not a block container"); } - getEditor().dispatch( - getEditor().prosemirrorState.tr.setSelection( + getEditor().transact((tr) => + tr.setSelection( TextSelection.create(doc, info.blockContent.beforePos + offset + 1) ) ); diff --git a/packages/core/src/api/blockManipulation/transactions.test.ts b/packages/core/src/api/blockManipulation/transactions.test.ts index aef94b54fd..ba76e79406 100644 --- a/packages/core/src/api/blockManipulation/transactions.test.ts +++ b/packages/core/src/api/blockManipulation/transactions.test.ts @@ -1,89 +1,89 @@ -import { describe, expect, it } from "vitest"; +// import { describe, expect, it } from "vitest"; -import { setupTestEnv } from "./setupTestEnv.js"; -import { Transaction } from "prosemirror-state"; +// import { setupTestEnv } from "./setupTestEnv.js"; +// import { Transaction } from "prosemirror-state"; -const getEditor = setupTestEnv(); +// const getEditor = setupTestEnv(); -describe("Test blocknote transactions", () => { - it("should return the correct block info when not in a transaction", async () => { - const editor = getEditor(); - editor.removeBlocks(editor.document); +// describe("Test blocknote transactions", () => { +// it("should return the correct block info when not in a transaction", async () => { +// const editor = getEditor(); +// editor.removeBlocks(editor.document); - const tr = editor.transaction; - const nodeToInsert = { - type: "blockContainer", - attrs: { - // Intentionally missing id - // id: "1", - textColor: "default", - backgroundColor: "default", - }, - content: [ - { - type: "paragraph", - attrs: { - textAlignment: "left", - }, - content: [ - { - type: "text", - text: "Hey-yo", - }, - ], - }, - ], - }; +// const tr = editor.transaction; +// const nodeToInsert = { +// type: "blockContainer", +// attrs: { +// // Intentionally missing id +// // id: "1", +// textColor: "default", +// backgroundColor: "default", +// }, +// content: [ +// { +// type: "paragraph", +// attrs: { +// textAlignment: "left", +// }, +// content: [ +// { +// type: "text", +// text: "Hey-yo", +// }, +// ], +// }, +// ], +// }; - tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); - await expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); +// tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); +// await expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); - editor.dispatch(tr); +// editor.dispatch(tr); - expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( - "editor-doc.json" - ); - }); +// expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( +// "editor-doc.json" +// ); +// }); - it.skip("should return the correct block info", async () => { - const editor = getEditor(); - editor.removeBlocks(editor.document); +// it.skip("should return the correct block info", async () => { +// const editor = getEditor(); +// editor.removeBlocks(editor.document); - let tr = undefined as unknown as Transaction; +// let tr = undefined as unknown as Transaction; - editor.transact(() => { - tr = editor.transaction; - const nodeToInsert = { - type: "blockContainer", - attrs: { - // Intentionally missing id - // id: "1", - textColor: "default", - backgroundColor: "default", - }, - content: [ - { - type: "paragraph", - attrs: { - textAlignment: "left", - }, - content: [ - { - type: "text", - text: "Hey-yo", - }, - ], - }, - ], - }; +// editor.transact(() => { +// tr = editor.transaction; +// const nodeToInsert = { +// type: "blockContainer", +// attrs: { +// // Intentionally missing id +// // id: "1", +// textColor: "default", +// backgroundColor: "default", +// }, +// content: [ +// { +// type: "paragraph", +// attrs: { +// textAlignment: "left", +// }, +// content: [ +// { +// type: "text", +// text: "Hey-yo", +// }, +// ], +// }, +// ], +// }; - tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); - expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); +// tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); +// expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); - editor.dispatch(tr); - expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( - "editor-doc.json" - ); - }); - }); -}); +// editor.dispatch(tr); +// expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( +// "editor-doc.json" +// ); +// }); +// }); +// }); diff --git a/packages/core/src/api/clipboard/clipboardExternal.test.ts b/packages/core/src/api/clipboard/clipboardExternal.test.ts index b31d19c19a..d34d1d2fed 100644 --- a/packages/core/src/api/clipboard/clipboardExternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardExternal.test.ts @@ -83,8 +83,7 @@ describe("Test external clipboard HTML", () => { throw new Error("Editor view not initialized."); } - const tr = editor.transaction; - editor.dispatch(tr.setSelection(testCase.createSelection(tr.doc))); + editor.transact((tr) => tr.setSelection(testCase.createSelection(tr.doc))); doPaste( editor.prosemirrorView, diff --git a/packages/core/src/api/clipboard/clipboardInternal.test.ts b/packages/core/src/api/clipboard/clipboardInternal.test.ts index e159dfacb6..4725b7acb8 100644 --- a/packages/core/src/api/clipboard/clipboardInternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardInternal.test.ts @@ -297,8 +297,9 @@ describe("Test ProseMirror selection clipboard HTML", () => { throw new Error("Editor view not initialized."); } - const tr = editor.transaction; - editor.dispatch(tr.setSelection(testCase.createCopySelection(tr.doc))); + editor.transact((tr) => + tr.setSelection(testCase.createCopySelection(tr.doc)) + ); const { clipboardHTML, externalHTML } = selectedFragmentToHTML( editor.prosemirrorView, @@ -311,8 +312,8 @@ describe("Test ProseMirror selection clipboard HTML", () => { const nextTr = editor.transaction; if (testCase.createPasteSelection) { - editor.dispatch( - nextTr.setSelection(testCase.createPasteSelection(nextTr.doc)) + editor.transact((tr) => + tr.setSelection(testCase.createPasteSelection!(nextTr.doc)) ); } diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 93a00e1ed4..4ad69e8456 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -120,8 +120,7 @@ export function selectedFragmentToHTML< "node" in view.state.selection && (view.state.selection.node as Node).type.spec.group === "blockContent" ) { - const tr = editor.transaction; - editor.dispatch( + editor.transact((tr) => tr.setSelection( new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)) ) @@ -252,8 +251,7 @@ export const createCopyToClipboardExtension = < } // Expands the selection to the parent `blockContainer` node. - const tr = editor.transaction; - editor.dispatch( + editor.transact((tr) => tr.setSelection( new NodeSelection( tr.doc.resolve(view.state.selection.from - 1) diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index d5b95c9bee..8280d4dda8 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -287,10 +287,20 @@ function blockOrInlineContentToContentNode( if (!block.content) { contentNode = schema.nodes[type].createChecked(block.props); } else if (typeof block.content === "string") { - const nodes = inlineContentToNodes([block.content], schema, type); + const nodes = inlineContentToNodes( + [block.content], + schema, + type, + styleSchema + ); contentNode = schema.nodes[type].createChecked(block.props, nodes); } else if (Array.isArray(block.content)) { - const nodes = inlineContentToNodes(block.content, schema, type); + const nodes = inlineContentToNodes( + block.content, + schema, + type, + styleSchema + ); contentNode = schema.nodes[type].createChecked(block.props, nodes); } else if (block.content.type === "tableContent") { const nodes = tableContentToNodes(block.content, schema, styleSchema); diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index a4bfcdce64..8b0e4196ea 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -39,7 +39,7 @@ async function parseHTMLAndCompareSnapshots( false, tr.selection.$from ); - editor.dispatch(tr.replaceSelection(slice)); + editor.transact((tr) => tr.replaceSelection(slice)); // alternative paste simulation doesn't work in a non-browser vitest env // editor._tiptapEditor.view.pasteHTML(html, { diff --git a/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts b/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts index f8b8981d5e..69960212c7 100644 --- a/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts +++ b/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts @@ -32,8 +32,8 @@ export const createAddFileButton = ( }; // Opens the file toolbar. const addFileButtonClickHandler = () => { - editor.dispatch( - editor.transaction.setMeta(editor.filePanel!.plugin, { + editor.transact((tr) => + tr.setMeta(editor.filePanel!.plugin, { block: block, }) ); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 657dbf8cfd..de3ce29ca2 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -742,7 +742,7 @@ export class BlockNoteEditor< * editor.dispatch(tr); * ``` */ - public dispatch(tr: Transaction) { + private dispatch(tr: Transaction) { if (this.activeTransaction) { // We don't want the editor to actually apply the state, so all of this is manipulating the current transaction in-memory return; @@ -761,24 +761,59 @@ export class BlockNoteEditor< * All changes to the editor within the transaction will be grouped together, so that * we can dispatch them as a single operation (thus creating only a single undo step) * + * @note There is no need to dispatch the transaction, as it will be automatically dispatched when the callback is complete. + * * @example * ```ts * // All changes to the editor will be grouped together - * editor.transact(() => { - * const tr = editor.transaction; + * editor.transact((tr) => { * tr.insertText("Hello, world!"); - * editor.dispatch(tr); * // These two operations will be grouped together in a single undo step - * const otherTr = editor.transaction; - * otherTr.insertText("Hello, world!"); - * editor.dispatch(otherTr); + * editor.transact((tr) => { + * tr.insertText("Hello, world!"); + * }); * }); * ``` */ - public transact(callback: (tr: Transaction) => T): T { + public transact( + callback: ( + /** + * The current active transaction, this will automatically be dispatched to the editor when the callback is complete + * If another `transact` call is made within the callback, it will be passed the same transaction as the parent call. + */ + tr: Transaction, + /** + * In some situations, you may need to dispatch a transaction which is not the current active transaction. + * This can be done by calling the `dispatch` function passed as an argument to the callback. + * + * @note This will only respect the passed transaction, and throw away any active transaction (e.g. the first argument). + */ + dispatch: (tr: Transaction) => void + ) => T + ): T { + /** + * This allows dispatching a transaction which may not be the current active transaction (e.g. when using a prosemirror-command) + * It will simply set the active transaction to the dispatch'd transaction, and let the `dispatch` method handle the rest. + */ + const dispatch = (tr: Transaction) => { + const activeTr = this.activeTransaction; + if (!activeTr) { + throw new Error( + "`dispatch` was called outside of a `transact` call, this is unsupported" + ); + } + + if (activeTr === tr || !tr) { + // The dispatch'd transaction is the same as the active transaction, so we don't need to do anything + return; + } + + this.activeTransaction = tr; + }; + if (this.activeTransaction) { // Already in a transaction, so we can just callback immediately - return callback(this.activeTransaction); + return callback(this.activeTransaction, dispatch); } try { @@ -786,7 +821,7 @@ export class BlockNoteEditor< this.activeTransaction = this.prosemirrorState.tr; // Capture all dispatch'd transactions - const result = callback(this.activeTransaction); + const result = callback(this.activeTransaction, dispatch); // Any transactions captured by the `dispatch` call will be stored in `this.activeTransaction` const activeTr = this.activeTransaction; diff --git a/packages/core/src/extensions/Comments/CommentsPlugin.ts b/packages/core/src/extensions/Comments/CommentsPlugin.ts index 0b3bdfb7e5..cac5f022bb 100644 --- a/packages/core/src/extensions/Comments/CommentsPlugin.ts +++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts @@ -89,8 +89,7 @@ export class CommentsPlugin extends EventEmitter { * when a thread is resolved or deleted, we need to update the marks to reflect the new state */ private updateMarksFromThreads = (threads: Map) => { - const tr = this.editor.transaction; - this.editor.transact(() => { + this.editor.transact((tr) => { tr.doc.descendants((node, pos) => { node.marks.forEach((mark) => { if (mark.type.name === this.markType) { @@ -114,7 +113,6 @@ export class CommentsPlugin extends EventEmitter { orphan: isOrphan, }) ); - this.editor.dispatch(tr); if (isOrphan && this.selectedThreadId === markThreadId) { // unselect @@ -262,8 +260,8 @@ export class CommentsPlugin extends EventEmitter { } this.selectedThreadId = threadId; this.emitStateUpdate(); - this.editor.dispatch( - this.editor.transaction.setMeta(PLUGIN_KEY, { + this.editor.transact((tr) => + tr.setMeta(PLUGIN_KEY, { name: SET_SELECTED_THREAD_ID, }) ); diff --git a/packages/core/src/extensions/LinkToolbar/LinkToolbarPlugin.ts b/packages/core/src/extensions/LinkToolbar/LinkToolbarPlugin.ts index 8300985b49..e70514fb0c 100644 --- a/packages/core/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +++ b/packages/core/src/extensions/LinkToolbar/LinkToolbarPlugin.ts @@ -12,6 +12,7 @@ import { StyleSchema, } from "../../schema/index.js"; import { EventEmitter } from "../../util/EventEmitter.js"; +import { getPmSchema } from "../../api/pmUtil.js"; export type LinkToolbarState = UiElementPosition & { // The hovered link's URL, and the text it's displayed with in the @@ -153,17 +154,15 @@ class LinkToolbarView implements PluginView { }; editLink(url: string, text: string) { - const tr = this.pmView.state.tr.insertText( - text, - this.linkMarkRange!.from, - this.linkMarkRange!.to - ); - tr.addMark( - this.linkMarkRange!.from, - this.linkMarkRange!.from + text.length, - this.pmView.state.schema.mark("link", { href: url }) - ); - this.editor.dispatch(tr); + this.editor.transact((tr) => { + const pmSchema = getPmSchema(tr); + tr.insertText(text, this.linkMarkRange!.from, this.linkMarkRange!.to); + tr.addMark( + this.linkMarkRange!.from, + this.linkMarkRange!.from + text.length, + pmSchema.mark("link", { href: url }) + ); + }); this.pmView.focus(); if (this.state?.show) { @@ -173,8 +172,8 @@ class LinkToolbarView implements PluginView { } deleteLink() { - this.editor.dispatch( - this.pmView.state.tr + this.editor.transact((tr) => + tr .removeMark( this.linkMarkRange!.from, this.linkMarkRange!.to, diff --git a/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts b/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts index a4ae0e99fb..40cb1e0c49 100644 --- a/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts +++ b/packages/core/src/extensions/ShowSelection/ShowSelectionPlugin.ts @@ -41,7 +41,7 @@ export class ShowSelectionPlugin { this.enabled = enabled; - this.editor.dispatch(this.editor.transaction.setMeta(PLUGIN_KEY, {})); + this.editor.transact((tr) => tr.setMeta(PLUGIN_KEY, {})); } public getEnabled() { diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 9d2e6b3016..35f91a54c7 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -114,9 +114,7 @@ class SuggestionMenuView< } closeMenu = () => { - this.editor.dispatch( - this.editor.transaction.setMeta(suggestionMenuPluginKey, null) - ); + this.editor.transact((tr) => tr.setMeta(suggestionMenuPluginKey, null)); }; clearQuery = () => { diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index bcffa50f8b..8e27866cc0 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -234,8 +234,8 @@ export function getDefaultSlashMenuItems< }); // Immediately open the file toolbar - editor.dispatch( - editor.transaction.setMeta(editor.filePanel!.plugin, { + editor.transact((tr) => + tr.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); @@ -253,8 +253,8 @@ export function getDefaultSlashMenuItems< }); // Immediately open the file toolbar - editor.dispatch( - editor.transaction.setMeta(editor.filePanel!.plugin, { + editor.transact((tr) => + tr.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); @@ -272,8 +272,8 @@ export function getDefaultSlashMenuItems< }); // Immediately open the file toolbar - editor.dispatch( - editor.transaction.setMeta(editor.filePanel!.plugin, { + editor.transact((tr) => + tr.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); @@ -291,8 +291,8 @@ export function getDefaultSlashMenuItems< }); // Immediately open the file toolbar - editor.dispatch( - editor.transaction.setMeta(editor.filePanel!.plugin, { + editor.transact((tr) => + tr.setMeta(editor.filePanel!.plugin, { block: insertedBlock, }) ); diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index f1e32cc79d..df962b463c 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -444,9 +444,7 @@ export class TableHandlesView< // Dispatches a dummy transaction to force a decorations update if // necessary. if (dispatchDecorationsTransaction) { - this.editor.dispatch( - this.pmView.state.tr.setMeta(tableHandlesPluginKey, true) - ); + this.editor.transact((tr) => tr.setMeta(tableHandlesPluginKey, true)); } }; @@ -811,12 +809,12 @@ export class TableHandlesProsemirrorPlugin< }; this.view!.emitUpdate(); - this.editor.dispatch( - this.editor.transaction.setMeta(tableHandlesPluginKey, { + this.editor.transact((tr) => + tr.setMeta(tableHandlesPluginKey, { draggedCellOrientation: - this.view!.state.draggingState.draggedCellOrientation, - originalIndex: this.view!.state.colIndex, - newIndex: this.view!.state.colIndex, + this.view!.state!.draggingState!.draggedCellOrientation, + originalIndex: this.view!.state!.colIndex, + newIndex: this.view!.state!.colIndex, tablePos: this.view!.tablePos, }) ); @@ -854,12 +852,12 @@ export class TableHandlesProsemirrorPlugin< }; this.view!.emitUpdate(); - this.editor.dispatch( - this.editor.transaction.setMeta(tableHandlesPluginKey, { + this.editor.transact((tr) => + tr.setMeta(tableHandlesPluginKey, { draggedCellOrientation: - this.view!.state.draggingState.draggedCellOrientation, - originalIndex: this.view!.state.rowIndex, - newIndex: this.view!.state.rowIndex, + this.view!.state!.draggingState!.draggedCellOrientation, + originalIndex: this.view!.state!.rowIndex, + newIndex: this.view!.state!.rowIndex, tablePos: this.view!.tablePos, }) ); @@ -887,9 +885,7 @@ export class TableHandlesProsemirrorPlugin< this.view!.state.draggingState = undefined; this.view!.emitUpdate(); - this.editor.dispatch( - this.editor.transaction.setMeta(tableHandlesPluginKey, null) - ); + this.editor.transact((tr) => tr.setMeta(tableHandlesPluginKey, null)); if (!this.editor.prosemirrorView) { throw new Error("Editor view not initialized."); @@ -990,15 +986,23 @@ export class TableHandlesProsemirrorPlugin< ); if (direction.orientation === "row") { if (direction.side === "above") { - return addRowBefore(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => + addRowBefore(state, dispatch) + ); } else { - return addRowAfter(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => + addRowAfter(state, dispatch) + ); } } else { if (direction.side === "left") { - return addColumnBefore(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => + addColumnBefore(state, dispatch) + ); } else { - return addColumnAfter(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => + addColumnAfter(state, dispatch) + ); } } }; @@ -1015,9 +1019,13 @@ export class TableHandlesProsemirrorPlugin< ); if (direction === "row") { - return deleteRow(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => + deleteRow(state, dispatch) + ); } else { - return deleteColumn(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => + deleteColumn(state, dispatch) + ); } }; @@ -1035,7 +1043,7 @@ export class TableHandlesProsemirrorPlugin< ) : this.editor.prosemirrorState; - return mergeCells(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => mergeCells(state, dispatch)); }; /** @@ -1047,7 +1055,7 @@ export class TableHandlesProsemirrorPlugin< ? this.setCellSelection(relativeCellToSplit) : this.editor.prosemirrorState; - return splitCell(state, this.editor.dispatch); + return this.editor.transact((_tr, dispatch) => splitCell(state, dispatch)); }; /** diff --git a/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx b/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx index 424747638d..56b14c3739 100644 --- a/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx +++ b/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx @@ -25,8 +25,8 @@ export const AddFileButton = ( ); // Opens the file toolbar. const addFileButtonClickHandler = useCallback(() => { - props.editor.dispatch( - props.editor.transaction.setMeta(props.editor.filePanel!.plugin, { + props.editor.transact((tr) => + tr.setMeta(props.editor.filePanel!.plugin, { block: props.block, }) ); diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx index 22108ccb46..fe7dd36f71 100644 --- a/packages/react/src/schema/ReactInlineContentSpec.tsx +++ b/packages/react/src/schema/ReactInlineContentSpec.tsx @@ -179,8 +179,8 @@ export function createReactInlineContentSpec< editor.pmSchema ); - editor.dispatch( - editor.prosemirrorState.tr.replaceWith( + editor.transact((tr) => + tr.replaceWith( props.getPos(), props.getPos() + props.node.nodeSize, content diff --git a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts index 05bcb1e97d..b2dfaa12bb 100644 --- a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts +++ b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts @@ -91,7 +91,10 @@ export function multiColumnDropCursor( .resolve(blockInfo.bnBlock.beforePos) .node(); - const columnList = nodeToBlock(parentBlock, editor.pmSchema); + const columnList = nodeToBlock( + parentBlock, + editor.pmSchema + ); // In a `columnList`, we expect that the average width of each column // is 1. However, there are cases in which this stops being true. For From 402dd373f13c7ef8908fb15d916511695aa40369 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 15 Apr 2025 17:55:08 +0200 Subject: [PATCH 19/26] refactor: make `transaction` private --- .../commands/moveBlocks/moveBlocks.test.ts | 30 ++++++---- .../commands/moveBlocks/moveBlocks.ts | 58 +++++++++---------- .../commands/nestBlock/nestBlock.ts | 14 +++-- .../selections/selection.test.ts | 16 ++--- .../textCursorPosition.test.ts | 16 +++-- .../api/clipboard/clipboardInternal.test.ts | 3 +- .../fromClipboard/handleFileInsertion.ts | 16 ++--- .../src/api/parsers/html/parseHTML.test.ts | 19 +++--- .../ListItemKeyboardShortcuts.ts | 11 ++-- .../core/src/editor/BlockNoteEditor.test.ts | 3 +- packages/core/src/editor/BlockNoteEditor.ts | 2 +- .../SuggestionMenu/SuggestionPlugin.ts | 3 +- .../TableHandles/TableHandlesPlugin.ts | 12 ++-- .../components/Comments/ThreadsSidebar.tsx | 38 ++++++------ .../DefaultButtons/CreateLinkButton.tsx | 8 ++- 15 files changed, 139 insertions(+), 110 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index 9e874cdfda..28f1d0c156 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -56,11 +56,13 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor().transaction.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setTextCursorPosition("paragraph-1"); makeSelectionSpanContent("text"); - expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); + expect( + selection.eq(getEditor().transact((tr) => tr.selection)) + ).toBeTruthy(); }); it("Node selection", () => { @@ -69,11 +71,13 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor().transaction.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setTextCursorPosition("image-0"); makeSelectionSpanContent("node"); - expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); + expect( + selection.eq(getEditor().transact((tr) => tr.selection)) + ).toBeTruthy(); }); it("Cell selection", () => { @@ -82,11 +86,13 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor().transaction.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setTextCursorPosition("table-0"); makeSelectionSpanContent("cell"); - expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); + expect( + selection.eq(getEditor().transact((tr) => tr.selection)) + ).toBeTruthy(); }); it("Multiple block selection", () => { @@ -94,10 +100,12 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor().transaction.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setSelection("paragraph-1", "paragraph-2"); - expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); + expect( + selection.eq(getEditor().transact((tr) => tr.selection)) + ).toBeTruthy(); }); it("Multiple block selection with table", () => { @@ -105,10 +113,12 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor().transaction.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setSelection("paragraph-6", "table-0"); - expect(selection.eq(getEditor().transaction.selection)).toBeTruthy(); + expect( + selection.eq(getEditor().transact((tr) => tr.selection)) + ).toBeTruthy(); }); }); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index 6d3bd521f8..acbb5d3973 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -43,35 +43,35 @@ type BlockSelectionData = ( function getBlockSelectionData( editor: BlockNoteEditor ): BlockSelectionData { - const tr = editor.transaction; - - const anchorBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); - - if (tr.selection instanceof CellSelection) { - return { - type: "cell" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, - anchorCellOffset: - tr.selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, - headCellOffset: - tr.selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, - }; - } else if (tr.selection instanceof NodeSelection) { - return { - type: "node" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, - }; - } else { - const headBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.head); - - return { - type: "text" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, - headBlockId: headBlockPosInfo.node.attrs.id, - anchorOffset: tr.selection.anchor - anchorBlockPosInfo.posBeforeNode, - headOffset: tr.selection.head - headBlockPosInfo.posBeforeNode, - }; - } + return editor.transact((tr) => { + const anchorBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); + + if (tr.selection instanceof CellSelection) { + return { + type: "cell" as const, + anchorBlockId: anchorBlockPosInfo.node.attrs.id, + anchorCellOffset: + tr.selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, + headCellOffset: + tr.selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, + }; + } else if (tr.selection instanceof NodeSelection) { + return { + type: "node" as const, + anchorBlockId: anchorBlockPosInfo.node.attrs.id, + }; + } else { + const headBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.head); + + return { + type: "text" as const, + anchorBlockId: anchorBlockPosInfo.node.attrs.id, + headBlockId: headBlockPosInfo.node.attrs.id, + anchorOffset: tr.selection.anchor - anchorBlockPosInfo.posBeforeNode, + headOffset: tr.selection.head - headBlockPosInfo.posBeforeNode, + }; + } + }); } /** diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts index 88fa46fa91..52d03c6063 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -80,15 +80,17 @@ export function unnestBlock(editor: BlockNoteEditor) { } export function canNestBlock(editor: BlockNoteEditor) { - const tr = editor.transaction; - const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); + return editor.transact((tr) => { + const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); - return tr.doc.resolve(blockContainer.beforePos).nodeBefore !== null; + return tr.doc.resolve(blockContainer.beforePos).nodeBefore !== null; + }); } export function canUnnestBlock(editor: BlockNoteEditor) { - const tr = editor.transaction; - const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); + return editor.transact((tr) => { + const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); - return tr.doc.resolve(blockContainer.beforePos).depth > 1; + return tr.doc.resolve(blockContainer.beforePos).depth > 1; + }); } diff --git a/packages/core/src/api/blockManipulation/selections/selection.test.ts b/packages/core/src/api/blockManipulation/selections/selection.test.ts index e0b4d7e590..d39869b4be 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.test.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.test.ts @@ -11,7 +11,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "paragraph-0", "paragraph-1"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Starts in block with children", () => { @@ -19,7 +19,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "paragraph-with-children", "paragraph-2"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Starts in nested block", () => { @@ -27,7 +27,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "nested-paragraph-0", "paragraph-2"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Ends in block with children", () => { @@ -35,7 +35,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "paragraph-1", "paragraph-with-children"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Ends in nested block", () => { @@ -43,7 +43,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "paragraph-1", "nested-paragraph-0"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Contains block with children", () => { @@ -51,7 +51,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "paragraph-1", "paragraph-2"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Starts in table", () => { @@ -59,7 +59,7 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "table-0", "paragraph-7"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Ends in table", () => { @@ -67,6 +67,6 @@ describe("Test getSelection & setSelection", () => { setSelection(tr, "paragraph-6", "table-0"); }); - expect(getSelection(getEditor().transaction)).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); }); diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts index ef1d93d4c5..c97af8f856 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts @@ -14,7 +14,9 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { setTextCursorPosition(tr, "paragraph-1"); }); - expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("First block", () => { @@ -22,7 +24,9 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { setTextCursorPosition(tr, "paragraph-0"); }); - expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("Last block", () => { @@ -30,7 +34,9 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { setTextCursorPosition(tr, "trailing-paragraph"); }); - expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("Nested block", () => { @@ -38,7 +44,9 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { setTextCursorPosition(tr, "nested-paragraph-0"); }); - expect(getTextCursorPosition(getEditor().transaction)).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("Set to start", () => { diff --git a/packages/core/src/api/clipboard/clipboardInternal.test.ts b/packages/core/src/api/clipboard/clipboardInternal.test.ts index 4725b7acb8..6462cdbc79 100644 --- a/packages/core/src/api/clipboard/clipboardInternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardInternal.test.ts @@ -310,10 +310,9 @@ describe("Test ProseMirror selection clipboard HTML", () => { `./__snapshots__/internal/${testCase.testName}.html` ); - const nextTr = editor.transaction; if (testCase.createPasteSelection) { editor.transact((tr) => - tr.setSelection(testCase.createPasteSelection!(nextTr.doc)) + tr.setSelection(testCase.createPasteSelection!(tr.doc)) ); } diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index 69834b1048..ccdca124e6 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -160,14 +160,14 @@ export async function handleFileInsertion< return; } - const tr = editor.transaction; - const posInfo = getNearestBlockPos(tr.doc, pos.pos); - - insertedBlockId = insertOrUpdateBlock( - editor, - editor.getBlock(posInfo.node.attrs.id)!, - fileBlock - ); + insertedBlockId = editor.transact((tr) => { + const posInfo = getNearestBlockPos(tr.doc, pos.pos); + return insertOrUpdateBlock( + editor, + editor.getBlock(posInfo.node.attrs.id)!, + fileBlock + ); + }); } else { return; } diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index 8b0e4196ea..0f5699b4aa 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -31,15 +31,16 @@ async function parseHTMLAndCompareSnapshots( (window as any).__TEST_OPTIONS.mockID = 0; // reset id counter const htmlNode = nestedListsToBlockNoteStructure(html); - const tr = editor.transaction; - const slice = (pmView as any).__parseFromClipboard( - editor.prosemirrorView, - "", - htmlNode.innerHTML, - false, - tr.selection.$from - ); - editor.transact((tr) => tr.replaceSelection(slice)); + editor.transact((tr) => { + const slice = (pmView as any).__parseFromClipboard( + editor.prosemirrorView, + "", + htmlNode.innerHTML, + false, + tr.selection.$from + ); + tr.replaceSelection(slice); + }); // alternative paste simulation doesn't work in a non-browser vitest env // editor._tiptapEditor.view.pasteHTML(html, { diff --git a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts index e52660ff24..c6a0529850 100644 --- a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -4,15 +4,18 @@ import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; export const handleEnter = (editor: BlockNoteEditor) => { - const tr = editor.transaction; - const blockInfo = getBlockInfoFromTransaction(tr); + const { blockInfo, selectionEmpty } = editor.transact((tr) => { + return { + blockInfo: getBlockInfoFromTransaction(tr), + selectionEmpty: tr.selection.anchor === tr.selection.head, + }; + }); + if (!blockInfo.isBlockContainer) { return false; } const { bnBlock: blockContainer, blockContent } = blockInfo; - const selectionEmpty = tr.selection.anchor === tr.selection.head; - if ( !( blockContent.node.type.name === "bulletListItem" || diff --git a/packages/core/src/editor/BlockNoteEditor.test.ts b/packages/core/src/editor/BlockNoteEditor.test.ts index 82364711f2..d77787a93c 100644 --- a/packages/core/src/editor/BlockNoteEditor.test.ts +++ b/packages/core/src/editor/BlockNoteEditor.test.ts @@ -10,8 +10,7 @@ import { BlockNoteEditor } from "./BlockNoteEditor.js"; */ it("creates an editor", () => { const editor = BlockNoteEditor.create(); - const tr = editor.transaction; - const posInfo = getNearestBlockPos(tr.doc, 2); + const posInfo = editor.transact((tr) => getNearestBlockPos(tr.doc, 2)); const info = getBlockInfo(posInfo); expect(info.blockNoteType).toEqual("paragraph"); }); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index de3ce29ca2..f033ddcc7c 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -850,7 +850,7 @@ export class BlockNoteEditor< * editor.dispatch(tr); * ``` */ - public get transaction(): Transaction { + private get transaction(): Transaction { if (this.activeTransaction) { // We are in a `transact` call, so we should return that transaction to accumulate changes on it return this.activeTransaction; diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 35f91a54c7..0c374d0882 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -125,13 +125,14 @@ class SuggestionMenuView< this.editor._tiptapEditor .chain() .focus() + // TODO need to make an API for this .deleteRange({ from: this.pluginState.queryStartPos! - (this.pluginState.deleteTriggerCharacter ? this.pluginState.triggerCharacter!.length : 0), - to: this.editor.transaction.selection.from, + to: this.editor.transact((tr) => tr.selection.from), }) .run(); }; diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index df962b463c..b67dabab95 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -263,7 +263,9 @@ export class TableHandlesView< | BlockFromConfigNoChildren | undefined; - const pmNodeInfo = getNodeById(blockEl.id, this.editor.transaction.doc); + const pmNodeInfo = this.editor.transact((tr) => + getNodeById(blockEl.id, tr.doc) + ); if (!pmNodeInfo) { throw new Error(`Block with ID ${blockEl.id} not found`); } @@ -1148,11 +1150,9 @@ export class TableHandlesProsemirrorPlugin< | BlockFromConfigNoChildren | undefined ) => { - const isSelectingTableCells = isTableCellSelection( - this.editor.transaction.selection - ) - ? this.editor.transaction.selection - : undefined; + const isSelectingTableCells = this.editor.transact((tr) => + isTableCellSelection(tr.selection) ? tr.selection : undefined + ); if ( !isSelectingTableCells || diff --git a/packages/react/src/components/Comments/ThreadsSidebar.tsx b/packages/react/src/components/Comments/ThreadsSidebar.tsx index 0ac0097761..38a4e805a6 100644 --- a/packages/react/src/components/Comments/ThreadsSidebar.tsx +++ b/packages/react/src/components/Comments/ThreadsSidebar.tsx @@ -131,28 +131,30 @@ export function getReferenceText( to: number; } ) { - if (!threadPosition) { - return "Original content deleted"; - } + return editor.transact((tr) => { + if (!threadPosition) { + return "Original content deleted"; + } - // TODO: Handles an edge case where the editor is re-rendered and the document - // is not yet fetched (causing it to be empty). We should store the original - // reference text in the data model, as not only is it a general improvement, - // but it also means we won't have to handle this edge case. - if (editor.transaction.doc.nodeSize < threadPosition.to) { - return ""; - } + // TODO: Handles an edge case where the editor is re-rendered and the document + // is not yet fetched (causing it to be empty). We should store the original + // reference text in the data model, as not only is it a general improvement, + // but it also means we won't have to handle this edge case. + if (tr.doc.nodeSize < threadPosition.to) { + return ""; + } - const referenceText = editor.transaction.doc.textBetween( - threadPosition.from, - threadPosition.to - ); + const referenceText = tr.doc.textBetween( + threadPosition.from, + threadPosition.to + ); - if (referenceText.length > 15) { - return `${referenceText.slice(0, 15)}…`; - } + if (referenceText.length > 15) { + return `${referenceText.slice(0, 15)}…`; + } - return referenceText; + return referenceText; + }); } /** diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx index 6dd4ba1cba..7f3145ad7f 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx @@ -82,6 +82,10 @@ export const CreateLinkButton = () => { [editor] ); + const isTableSelection = editor.transact((tr) => + isTableCellSelection(tr.selection) + ); + const show = useMemo(() => { if (!linkInSchema) { return false; @@ -93,8 +97,8 @@ export const CreateLinkButton = () => { } } - return !isTableCellSelection(editor.transaction.selection); - }, [linkInSchema, selectedBlocks, editor.transaction.selection]); + return !isTableSelection; + }, [linkInSchema, selectedBlocks, isTableSelection]); if ( !show || From 7ee1c426841a8e4ff90244e64e9eb7eb1832d84c Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 10:32:42 +0200 Subject: [PATCH 20/26] refactor: remove reading from `prosemirrorState` add `exec` and `canExec` --- .../commands/mergeBlocks/mergeBlocks.test.ts | 14 +- .../commands/moveBlocks/moveBlocks.test.ts | 6 +- .../commands/nestBlock/nestBlock.ts | 12 +- .../commands/splitBlock/splitBlock.test.ts | 82 +++--- .../textCursorPosition.test.ts | 8 +- packages/core/src/editor/BlockNoteEditor.ts | 252 ++++++++--------- .../TableHandles/TableHandlesPlugin.ts | 259 +++++++++--------- 7 files changed, 324 insertions(+), 309 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts index a3b497a206..dbcd1f568b 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; import { setupTestEnv } from "../../setupTestEnv.js"; import { mergeBlocksCommand } from "./mergeBlocks.js"; @@ -13,8 +13,9 @@ function mergeBlocks(posBetweenBlocks: number) { } function getPosBeforeSelectedBlock() { - return getBlockInfoFromSelection(getEditor().prosemirrorState).bnBlock - .beforePos; + return getEditor().transact( + (tr) => getBlockInfoFromTransaction(tr).bnBlock.beforePos + ); } describe("Test mergeBlocks", () => { @@ -61,15 +62,16 @@ describe("Test mergeBlocks", () => { it("Selection is updated", () => { getEditor().setTextCursorPosition("paragraph-0", "end"); - const firstBlockEndOffset = - getEditor().prosemirrorState.selection.$anchor.parentOffset; + const firstBlockEndOffset = getEditor().transact( + (tr) => tr.selection.$anchor.parentOffset + ); getEditor().setTextCursorPosition("paragraph-1"); mergeBlocks(getPosBeforeSelectedBlock()); const anchorIsAtOldFirstBlockEndPos = - getEditor().prosemirrorState.selection.$anchor.parentOffset === + getEditor().transact((tr) => tr.selection.$anchor.parentOffset) === firstBlockEndOffset; expect(anchorIsAtOldFirstBlockEndPos).toBeTruthy(); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index 28f1d0c156..5a67b7b46c 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -2,7 +2,7 @@ import { NodeSelection, TextSelection } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; import { describe, expect, it } from "vitest"; -import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; import { setupTestEnv } from "../../setupTestEnv.js"; import { moveBlocksDown, @@ -13,7 +13,9 @@ import { const getEditor = setupTestEnv(); function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { - const blockInfo = getBlockInfoFromSelection(getEditor().prosemirrorState); + const blockInfo = getEditor().transact((tr) => + getBlockInfoFromTransaction(tr) + ); if (!blockInfo.isBlockContainer) { throw new Error( `Selection points to a ${blockInfo.blockNoteType} node, not a blockContainer node` diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts index 52d03c6063..9917a9f979 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -1,5 +1,5 @@ import { Fragment, NodeType, Slice } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { EditorState, Transaction } from "prosemirror-state"; import { ReplaceAroundStep } from "prosemirror-transform"; import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; @@ -12,7 +12,7 @@ import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; * The original function derives too many information from the parentnode and itemtype */ function sinkListItem(itemType: NodeType, groupType: NodeType) { - return function ({ state, dispatch }: { state: EditorState; dispatch: any }) { + return function (state: EditorState, dispatch?: (tr: Transaction) => void) { const { $from, $to } = state.selection; const range = $from.blockRange( $to, @@ -67,11 +67,11 @@ function sinkListItem(itemType: NodeType, groupType: NodeType) { } export function nestBlock(editor: BlockNoteEditor) { - return editor._tiptapEditor.commands.command( + return editor.exec((state, dispatch) => sinkListItem( - editor._tiptapEditor.schema.nodes["blockContainer"], - editor._tiptapEditor.schema.nodes["blockGroup"] - ) + state.schema.nodes["blockContainer"], + state.schema.nodes["blockGroup"] + )(state, dispatch) ); } diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts index 4d3770df66..8a6015ffbb 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { getBlockInfo, - getBlockInfoFromSelection, + getBlockInfoFromTransaction, } from "../../../getBlockInfoFromPos.js"; import { getNodeById } from "../../../nodeUtil.js"; import { setupTestEnv } from "../../setupTestEnv.js"; @@ -47,83 +47,103 @@ function setSelectionWithOffset( describe("Test splitBlocks", () => { it("Basic", () => { - setSelectionWithOffset(getEditor().prosemirrorState.doc, "paragraph-0", 4); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-0", 4); + }); - splitBlock(getEditor().prosemirrorState.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); expect(getEditor().document).toMatchSnapshot(); }); it("End of content", () => { - setSelectionWithOffset(getEditor().prosemirrorState.doc, "paragraph-0", 11); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-0", 11); + }); - splitBlock(getEditor().prosemirrorState.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); expect(getEditor().document).toMatchSnapshot(); }); it("Block has children", () => { - setSelectionWithOffset( - getEditor().prosemirrorState.doc, - "paragraph-with-children", - 4 - ); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-with-children", 4); + }); - splitBlock(getEditor().prosemirrorState.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); expect(getEditor().document).toMatchSnapshot(); }); it("Keep type", () => { - setSelectionWithOffset(getEditor().prosemirrorState.doc, "heading-0", 4); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "heading-0", 4); + }); - splitBlock(getEditor().prosemirrorState.selection.anchor, true); + splitBlock( + getEditor().transact((tr) => tr.selection.anchor), + true + ); expect(getEditor().document).toMatchSnapshot(); }); it("Don't keep type", () => { - setSelectionWithOffset(getEditor().prosemirrorState.doc, "heading-0", 4); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "heading-0", 4); + }); - splitBlock(getEditor().prosemirrorState.selection.anchor, false); + splitBlock( + getEditor().transact((tr) => tr.selection.anchor), + false + ); expect(getEditor().document).toMatchSnapshot(); }); it.skip("Keep props", () => { - setSelectionWithOffset( - getEditor().prosemirrorState.doc, - "paragraph-with-props", - 4 + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-with-props", 4); + }); + + splitBlock( + getEditor().transact((tr) => tr.selection.anchor), + false, + true ); - splitBlock(getEditor().prosemirrorState.selection.anchor, false, true); - expect(getEditor().document).toMatchSnapshot(); }); it("Don't keep props", () => { - setSelectionWithOffset( - getEditor().prosemirrorState.doc, - "paragraph-with-props", - 4 + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-with-props", 4); + }); + + splitBlock( + getEditor().transact((tr) => tr.selection.anchor), + false, + false ); - splitBlock(getEditor().prosemirrorState.selection.anchor, false, false); - expect(getEditor().document).toMatchSnapshot(); }); it("Selection is set", () => { - setSelectionWithOffset(getEditor().prosemirrorState.doc, "paragraph-0", 4); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-0", 4); + }); - splitBlock(getEditor().prosemirrorState.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); - const { bnBlock } = getBlockInfoFromSelection(getEditor().prosemirrorState); + const bnBlock = getEditor().transact( + (tr) => getBlockInfoFromTransaction(tr).bnBlock + ); const anchorIsAtStartOfNewBlock = bnBlock.node.attrs.id === "0" && - getEditor().prosemirrorState.selection.$anchor.parentOffset === 0; + getEditor().transact((tr) => tr.selection.$anchor.parentOffset) === 0; expect(anchorIsAtStartOfNewBlock).toBeTruthy(); }); diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts index c97af8f856..4ad98d0808 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts @@ -55,7 +55,7 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { }); expect( - getEditor().prosemirrorState.selection.$from.parentOffset === 0 + getEditor().transact((tr) => tr.selection.$from.parentOffset) === 0 ).toBeTruthy(); }); @@ -65,8 +65,10 @@ describe("Test getTextCursorPosition & setTextCursorPosition", () => { }); expect( - getEditor().prosemirrorState.selection.$from.parentOffset === - getEditor().prosemirrorState.selection.$from.node().firstChild!.nodeSize + getEditor().transact((tr) => tr.selection.$from.parentOffset) === + getEditor().transact( + (tr) => tr.selection.$from.node().firstChild!.nodeSize + ) ).toBeTruthy(); }); }); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index f033ddcc7c..a3326d3a2e 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -93,7 +93,12 @@ import { import { Dictionary } from "../i18n/dictionary.js"; import { en } from "../i18n/locales/index.js"; -import { Plugin, TextSelection, Transaction } from "@tiptap/pm/state"; +import { + TextSelection, + type Command, + type Plugin, + type Transaction, +} from "@tiptap/pm/state"; import { dropCursor } from "prosemirror-dropcursor"; import { EditorView } from "prosemirror-view"; import { ySyncPluginKey } from "y-prosemirror"; @@ -733,28 +738,48 @@ export class BlockNoteEditor< } /** - * Dispatch a ProseMirror transaction. + * Stores the currently active transaction, which is the accumulated transaction from all {@link dispatch} calls during a {@link transact} calls + */ + private activeTransaction: Transaction | null = null; + + /** + * Execute a prosemirror command. This is mostly for backwards compatibility with older code. + * + * @note You should prefer the {@link transact} method when possible, as it will automatically handle the dispatching of the transaction and work across blocknote transactions. * * @example * ```ts - * const tr = editor.transaction; - * tr.insertText("Hello, world!"); - * editor.dispatch(tr); + * editor.command((state, dispatch, view) => { + * dispatch(state.tr.insertText("Hello, world!")); + * }); * ``` */ - private dispatch(tr: Transaction) { - if (this.activeTransaction) { - // We don't want the editor to actually apply the state, so all of this is manipulating the current transaction in-memory - return; - } + public exec(command: Command) { + const state = this._tiptapEditor.state; + const view = this._tiptapEditor.view; + const dispatch = this._tiptapEditor.dispatch; - this._tiptapEditor.dispatch(tr); + return command(state, dispatch, view); } /** - * Stores the currently active transaction, which is the accumulated transaction from all {@link dispatch} calls during a {@link transact} calls + * Check if a command can be executed. A command should return `false` if it is not valid in the current state. + * + * @example + * ```ts + * if (editor.canExec(command)) { + * // show button + * } else { + * // hide button + * } + * ``` */ - private activeTransaction: Transaction | null = null; + public canExec(command: Command): boolean { + const state = this._tiptapEditor.state; + const view = this._tiptapEditor.view; + + return command(state, undefined, view); + } /** * Execute a function within a "blocknote transaction". @@ -781,56 +806,38 @@ export class BlockNoteEditor< * The current active transaction, this will automatically be dispatched to the editor when the callback is complete * If another `transact` call is made within the callback, it will be passed the same transaction as the parent call. */ - tr: Transaction, - /** - * In some situations, you may need to dispatch a transaction which is not the current active transaction. - * This can be done by calling the `dispatch` function passed as an argument to the callback. - * - * @note This will only respect the passed transaction, and throw away any active transaction (e.g. the first argument). - */ - dispatch: (tr: Transaction) => void + tr: Transaction ) => T ): T { - /** - * This allows dispatching a transaction which may not be the current active transaction (e.g. when using a prosemirror-command) - * It will simply set the active transaction to the dispatch'd transaction, and let the `dispatch` method handle the rest. - */ - const dispatch = (tr: Transaction) => { - const activeTr = this.activeTransaction; - if (!activeTr) { - throw new Error( - "`dispatch` was called outside of a `transact` call, this is unsupported" - ); - } - - if (activeTr === tr || !tr) { - // The dispatch'd transaction is the same as the active transaction, so we don't need to do anything - return; - } - - this.activeTransaction = tr; - }; - if (this.activeTransaction) { // Already in a transaction, so we can just callback immediately - return callback(this.activeTransaction, dispatch); + return callback(this.activeTransaction); } try { // Enter transaction mode, by setting a starting transaction - this.activeTransaction = this.prosemirrorState.tr; + this.activeTransaction = this._tiptapEditor.state.tr; // Capture all dispatch'd transactions - const result = callback(this.activeTransaction, dispatch); + const result = callback(this.activeTransaction); // Any transactions captured by the `dispatch` call will be stored in `this.activeTransaction` const activeTr = this.activeTransaction; this.activeTransaction = null; - if (activeTr) { + if ( + activeTr && + // Only dispatch if the transaction was actually modified in some way + (activeTr.docChanged || + activeTr.selectionSet || + activeTr.scrolledIntoView || + activeTr.storedMarksSet || + !activeTr.isGeneric) + ) { // Dispatch the transaction if it was modified - this.dispatch(activeTr); + this._tiptapEditor.dispatch(activeTr); } + return result; } finally { // We wrap this in a finally block to ensure we don't disable future transactions just because of an error in the callback @@ -838,27 +845,6 @@ export class BlockNoteEditor< } } - /** - * Start a new ProseMirror transaction. - * - * @example - * ```ts - * const tr = editor.transaction - * - * tr.insertText("Hello, world!"); - * - * editor.dispatch(tr); - * ``` - */ - private get transaction(): Transaction { - if (this.activeTransaction) { - // We are in a `transact` call, so we should return that transaction to accumulate changes on it - return this.activeTransaction; - } - // Otherwise, we are not in a `transact` call, so we can just return the current state - return this.prosemirrorState.tr; - } - /** * Mount the editor to a parent DOM element. Call mount(undefined) to clean up * @@ -878,16 +864,6 @@ export class BlockNoteEditor< return this._tiptapEditor.view; } - /** - * Get the underlying prosemirror state - */ - public get prosemirrorState() { - if (this.activeTransaction) { - throw new Error("Cannot get prosemirrorState while in a transaction"); - } - return this._tiptapEditor.state; - } - public get domElement() { return this.prosemirrorView?.dom as HTMLDivElement | undefined; } @@ -934,15 +910,17 @@ export class BlockNoteEditor< * @returns A snapshot of all top-level (non-nested) blocks in the editor. */ public get document(): Block[] { - const blocks: Block[] = []; + return this.transact((tr) => { + const blocks: Block[] = []; - this.prosemirrorState.doc.firstChild!.descendants((node) => { - blocks.push(nodeToBlock(node, this.pmSchema)); + tr.doc.firstChild!.descendants((node) => { + blocks.push(nodeToBlock(node, this.pmSchema)); - return false; - }); + return false; + }); - return blocks; + return blocks; + }); } /** @@ -955,7 +933,7 @@ export class BlockNoteEditor< public getBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getBlock(this.transaction.doc, blockIdentifier); + return this.transact((tr) => getBlock(tr.doc, blockIdentifier)); } /** @@ -970,7 +948,7 @@ export class BlockNoteEditor< public getPrevBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getPrevBlock(this.transaction.doc, blockIdentifier); + return this.transact((tr) => getPrevBlock(tr.doc, blockIdentifier)); } /** @@ -984,7 +962,7 @@ export class BlockNoteEditor< public getNextBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getNextBlock(this.transaction.doc, blockIdentifier); + return this.transact((tr) => getNextBlock(tr.doc, blockIdentifier)); } /** @@ -997,7 +975,7 @@ export class BlockNoteEditor< public getParentBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getParentBlock(this.transaction.doc, blockIdentifier); + return this.transact((tr) => getParentBlock(tr.doc, blockIdentifier)); } /** @@ -1067,7 +1045,7 @@ export class BlockNoteEditor< ISchema, SSchema > { - return getTextCursorPosition(this.transaction); + return this.transact((tr) => getTextCursorPosition(tr)); } /** @@ -1089,7 +1067,7 @@ export class BlockNoteEditor< * Gets a snapshot of the current selection. */ public getSelection(): Selection | undefined { - return getSelection(this.transaction); + return this.transact((tr) => getSelection(tr)); } public setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier) { @@ -1211,32 +1189,34 @@ export class BlockNoteEditor< * Gets the active text styles at the text cursor position or at the end of the current selection if it's active. */ public getActiveStyles() { - const styles: Styles = {}; - const marks = this.prosemirrorState.selection.$to.marks(); - - for (const mark of marks) { - const config = this.schema.styleSchema[mark.type.name]; - if (!config) { - if ( - // Links are not considered styles in blocknote - mark.type.name !== "link" && - // "blocknoteIgnore" tagged marks (such as comments) are also not considered BlockNote "styles" - !mark.type.spec.blocknoteIgnore - ) { - // eslint-disable-next-line no-console - console.warn("mark not found in styleschema", mark.type.name); + return this.transact((tr) => { + const styles: Styles = {}; + const marks = tr.selection.$to.marks(); + + for (const mark of marks) { + const config = this.schema.styleSchema[mark.type.name]; + if (!config) { + if ( + // Links are not considered styles in blocknote + mark.type.name !== "link" && + // "blocknoteIgnore" tagged marks (such as comments) are also not considered BlockNote "styles" + !mark.type.spec.blocknoteIgnore + ) { + // eslint-disable-next-line no-console + console.warn("mark not found in styleschema", mark.type.name); + } + + continue; + } + if (config.propSchema === "boolean") { + (styles as any)[config.type] = true; + } else { + (styles as any)[config.type] = mark.attrs.stringValue; } - - continue; - } - if (config.propSchema === "boolean") { - (styles as any)[config.type] = true; - } else { - (styles as any)[config.type] = mark.attrs.stringValue; } - } - return styles; + return styles; + }); } /** @@ -1293,8 +1273,9 @@ export class BlockNoteEditor< * Gets the currently selected text. */ public getSelectedText() { - const state = this.prosemirrorState; - return state.doc.textBetween(state.selection.from, state.selection.to); + return this.transact((tr) => { + return tr.doc.textBetween(tr.selection.from, tr.selection.to); + }); } /** @@ -1314,20 +1295,19 @@ export class BlockNoteEditor< return; } const mark = this.pmSchema.mark("link", { href: url }); - const tr = this.transaction; - const { from, to } = tr.selection; + this.transact((tr) => { + const { from, to } = tr.selection; - if (text) { - this.dispatch( - tr.insertText(text, from, to).addMark(from, from + text.length, mark) - ); - } else { - this.dispatch( - tr - .setSelection(TextSelection.create(tr.doc, to)) - .addMark(from, to, mark) - ); - } + if (text) { + tr.insertText(text, from, to).addMark(from, from + text.length, mark); + } else { + tr.setSelection(TextSelection.create(tr.doc, to)).addMark( + from, + to, + mark + ); + } + }); } /** @@ -1572,16 +1552,16 @@ export class BlockNoteEditor< } this.focus(); - const tr = this.transaction; - if (pluginState && pluginState.deleteTriggerCharacter) { - tr.insertText(triggerCharacter); - } - tr.scrollIntoView().setMeta(this.suggestionMenus.plugin, { - triggerCharacter: triggerCharacter, - deleteTriggerCharacter: pluginState?.deleteTriggerCharacter || false, - ignoreQueryLength: pluginState?.ignoreQueryLength || false, + this.transact((tr) => { + if (pluginState?.deleteTriggerCharacter) { + tr.insertText(triggerCharacter); + } + tr.scrollIntoView().setMeta(this.suggestionMenus.plugin, { + triggerCharacter: triggerCharacter, + deleteTriggerCharacter: pluginState?.deleteTriggerCharacter || false, + ignoreQueryLength: pluginState?.ignoreQueryLength || false, + }); }); - this.dispatch(tr); } // `forceSelectionVisible` determines whether the editor selection is shows diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index b67dabab95..b78c457dc7 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -1,4 +1,4 @@ -import { Plugin, PluginKey, PluginView } from "prosemirror-state"; +import { EditorState, Plugin, PluginKey, PluginView } from "prosemirror-state"; import { CellSelection, addColumnAfter, @@ -934,6 +934,7 @@ export class TableHandlesProsemirrorPlugin< * @returns The new state after the selection has been set. */ private setCellSelection = ( + state: EditorState, relativeStartCell: RelativeCellIndices, relativeEndCell: RelativeCellIndices = relativeStartCell ) => { @@ -943,7 +944,6 @@ export class TableHandlesProsemirrorPlugin< throw new Error("Table handles view not initialized"); } - const state = this.editor.prosemirrorState; const tableResolvedPos = state.doc.resolve(view.tablePos! + 1); const startRowResolvedPos = state.doc.resolve( tableResolvedPos.posAtIndex(relativeStartCell.row) + 1 @@ -981,32 +981,28 @@ export class TableHandlesProsemirrorPlugin< | { orientation: "row"; side: "above" | "below" } | { orientation: "column"; side: "left" | "right" } ) => { - const state = this.setCellSelection( - direction.orientation === "row" - ? { row: index, col: 0 } - : { row: 0, col: index } - ); - if (direction.orientation === "row") { - if (direction.side === "above") { - return this.editor.transact((_tr, dispatch) => - addRowBefore(state, dispatch) - ); - } else { - return this.editor.transact((_tr, dispatch) => - addRowAfter(state, dispatch) - ); - } - } else { - if (direction.side === "left") { - return this.editor.transact((_tr, dispatch) => - addColumnBefore(state, dispatch) - ); + this.editor.exec((beforeState, dispatch) => { + const state = this.setCellSelection( + beforeState, + direction.orientation === "row" + ? { row: index, col: 0 } + : { row: 0, col: index } + ); + + if (direction.orientation === "row") { + if (direction.side === "above") { + return addRowBefore(state, dispatch); + } else { + return addRowAfter(state, dispatch); + } } else { - return this.editor.transact((_tr, dispatch) => - addColumnAfter(state, dispatch) - ); + if (direction.side === "left") { + return addColumnBefore(state, dispatch); + } else { + return addColumnAfter(state, dispatch); + } } - } + }); }; /** @@ -1016,18 +1012,22 @@ export class TableHandlesProsemirrorPlugin< index: RelativeCellIndices["row"] | RelativeCellIndices["col"], direction: "row" | "column" ) => { - const state = this.setCellSelection( - direction === "row" ? { row: index, col: 0 } : { row: 0, col: index } - ); - if (direction === "row") { - return this.editor.transact((_tr, dispatch) => - deleteRow(state, dispatch) - ); + return this.editor.exec((beforeState, dispatch) => { + const state = this.setCellSelection(beforeState, { + row: index, + col: 0, + }); + return deleteRow(state, dispatch); + }); } else { - return this.editor.transact((_tr, dispatch) => - deleteColumn(state, dispatch) - ); + return this.editor.exec((beforeState, dispatch) => { + const state = this.setCellSelection(beforeState, { + row: 0, + col: index, + }); + return deleteColumn(state, dispatch); + }); } }; @@ -1038,14 +1038,17 @@ export class TableHandlesProsemirrorPlugin< relativeStartCell: RelativeCellIndices; relativeEndCell: RelativeCellIndices; }) => { - const state = cellsToMerge - ? this.setCellSelection( - cellsToMerge.relativeStartCell, - cellsToMerge.relativeEndCell - ) - : this.editor.prosemirrorState; - - return this.editor.transact((_tr, dispatch) => mergeCells(state, dispatch)); + return this.editor.exec((beforeState, dispatch) => { + const state = cellsToMerge + ? this.setCellSelection( + beforeState, + cellsToMerge.relativeStartCell, + cellsToMerge.relativeEndCell + ) + : beforeState; + + return mergeCells(state, dispatch); + }); }; /** @@ -1053,11 +1056,13 @@ export class TableHandlesProsemirrorPlugin< * If no cell is provided, the current cell selected will be split. */ splitCell = (relativeCellToSplit?: RelativeCellIndices) => { - const state = relativeCellToSplit - ? this.setCellSelection(relativeCellToSplit) - : this.editor.prosemirrorState; + return this.editor.exec((beforeState, dispatch) => { + const state = relativeCellToSplit + ? this.setCellSelection(beforeState, relativeCellToSplit) + : beforeState; - return this.editor.transact((_tr, dispatch) => splitCell(state, dispatch)); + return splitCell(state, dispatch); + }); }; /** @@ -1075,69 +1080,71 @@ export class TableHandlesProsemirrorPlugin< cells: RelativeCellIndices[]; } => { // Based on the current selection, find the table cells that are within the selected range - const state = this.editor.prosemirrorState; - const selection = state.selection; - - let $fromCell = selection.$from; - let $toCell = selection.$to; - if (isTableCellSelection(selection)) { - // When the selection is a table cell selection, we can find the - // from and to cells by iterating over the ranges in the selection - const { ranges } = selection; - ranges.forEach((range) => { - $fromCell = range.$from.min($fromCell ?? range.$from); - $toCell = range.$to.max($toCell ?? range.$to); - }); - } else { - // When the selection is a normal text selection - // Assumes we are within a tableParagraph - // And find the from and to cells by resolving the positions - $fromCell = state.doc.resolve( - selection.$from.pos - selection.$from.parentOffset - 1 - ); - $toCell = state.doc.resolve( - selection.$to.pos - selection.$to.parentOffset - 1 - ); - - // Opt-out when the selection is not pointing into cells - if ($fromCell.pos === 0 || $toCell.pos === 0) { - return undefined; - } - } - // Find the row and table that the from and to cells are in - const $fromRow = state.doc.resolve( - $fromCell.pos - $fromCell.parentOffset - 1 - ); - const $toRow = state.doc.resolve($toCell.pos - $toCell.parentOffset - 1); - - // Find the table - const $table = state.doc.resolve($fromRow.pos - $fromRow.parentOffset - 1); + return this.editor.transact((tr) => { + const selection = tr.selection; + + let $fromCell = selection.$from; + let $toCell = selection.$to; + if (isTableCellSelection(selection)) { + // When the selection is a table cell selection, we can find the + // from and to cells by iterating over the ranges in the selection + const { ranges } = selection; + ranges.forEach((range) => { + $fromCell = range.$from.min($fromCell ?? range.$from); + $toCell = range.$to.max($toCell ?? range.$to); + }); + } else { + // When the selection is a normal text selection + // Assumes we are within a tableParagraph + // And find the from and to cells by resolving the positions + $fromCell = tr.doc.resolve( + selection.$from.pos - selection.$from.parentOffset - 1 + ); + $toCell = tr.doc.resolve( + selection.$to.pos - selection.$to.parentOffset - 1 + ); - // Find the column and row indices of the from and to cells - const fromColIndex = $fromCell.index($fromRow.depth); - const fromRowIndex = $fromRow.index($table.depth); - const toColIndex = $toCell.index($toRow.depth); - const toRowIndex = $toRow.index($table.depth); + // Opt-out when the selection is not pointing into cells + if ($fromCell.pos === 0 || $toCell.pos === 0) { + return undefined; + } + } - const cells: RelativeCellIndices[] = []; - for (let row = fromRowIndex; row <= toRowIndex; row++) { - for (let col = fromColIndex; col <= toColIndex; col++) { - cells.push({ row, col }); + // Find the row and table that the from and to cells are in + const $fromRow = tr.doc.resolve( + $fromCell.pos - $fromCell.parentOffset - 1 + ); + const $toRow = tr.doc.resolve($toCell.pos - $toCell.parentOffset - 1); + + // Find the table + const $table = tr.doc.resolve($fromRow.pos - $fromRow.parentOffset - 1); + + // Find the column and row indices of the from and to cells + const fromColIndex = $fromCell.index($fromRow.depth); + const fromRowIndex = $fromRow.index($table.depth); + const toColIndex = $toCell.index($toRow.depth); + const toRowIndex = $toRow.index($table.depth); + + const cells: RelativeCellIndices[] = []; + for (let row = fromRowIndex; row <= toRowIndex; row++) { + for (let col = fromColIndex; col <= toColIndex; col++) { + cells.push({ row, col }); + } } - } - return { - from: { - row: fromRowIndex, - col: fromColIndex, - }, - to: { - row: toRowIndex, - col: toColIndex, - }, - cells, - }; + return { + from: { + row: fromRowIndex, + col: fromColIndex, + }, + to: { + row: toRowIndex, + col: toColIndex, + }, + cells, + }; + }); }; /** @@ -1150,30 +1157,32 @@ export class TableHandlesProsemirrorPlugin< | BlockFromConfigNoChildren | undefined ) => { - const isSelectingTableCells = this.editor.transact((tr) => - isTableCellSelection(tr.selection) ? tr.selection : undefined - ); + return this.editor.transact((tr) => { + const isSelectingTableCells = isTableCellSelection(tr.selection) + ? tr.selection + : undefined; - if ( - !isSelectingTableCells || - !block || - // Only offer the merge button if there is more than one cell selected. - isSelectingTableCells.ranges.length <= 1 - ) { - return undefined; - } + if ( + !isSelectingTableCells || + !block || + // Only offer the merge button if there is more than one cell selected. + isSelectingTableCells.ranges.length <= 1 + ) { + return undefined; + } - const cellSelection = this.getCellSelection(); + const cellSelection = this.getCellSelection(); - if (!cellSelection) { - return undefined; - } + if (!cellSelection) { + return undefined; + } - if (areInSameColumn(cellSelection.from, cellSelection.to, block)) { - return "vertical"; - } + if (areInSameColumn(cellSelection.from, cellSelection.to, block)) { + return "vertical"; + } - return "horizontal"; + return "horizontal"; + }); }; cropEmptyRowsOrColumns = ( From cc23f3f54f974d05f34a8313e040529303c41026 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 10:38:07 +0200 Subject: [PATCH 21/26] fix: have dispatch be bound --- packages/core/src/editor/BlockNoteEditor.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index a3326d3a2e..ecd578ecce 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -749,15 +749,20 @@ export class BlockNoteEditor< * * @example * ```ts - * editor.command((state, dispatch, view) => { + * editor.exec((state, dispatch, view) => { * dispatch(state.tr.insertText("Hello, world!")); * }); * ``` */ public exec(command: Command) { + if (this.activeTransaction) { + throw new Error( + "`exec` should not be called within a `transact` call, move the `exec` call outside of the `transact` call" + ); + } const state = this._tiptapEditor.state; const view = this._tiptapEditor.view; - const dispatch = this._tiptapEditor.dispatch; + const dispatch = (tr: Transaction) => this._tiptapEditor.dispatch(tr); return command(state, dispatch, view); } From 95116045f5d86aa11293148253889c5979376fba Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 10:39:37 +0200 Subject: [PATCH 22/26] fix: throw an error for canExec too --- packages/core/src/editor/BlockNoteEditor.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index ecd578ecce..bd160de6be 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -780,6 +780,11 @@ export class BlockNoteEditor< * ``` */ public canExec(command: Command): boolean { + if (this.activeTransaction) { + throw new Error( + "`canExec` should not be called within a `transact` call, move the `canExec` call outside of the `transact` call" + ); + } const state = this._tiptapEditor.state; const view = this._tiptapEditor.view; From 47de76edebde8da1e2ddf29200570490ec78199d Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 10:44:01 +0200 Subject: [PATCH 23/26] chore: rm transactions test --- .../src/api/blockManipulation/editor-doc.json | 48 ---------- .../blockManipulation/transaction-doc.json | 48 ---------- .../blockManipulation/transactions.test.ts | 89 ------------------- 3 files changed, 185 deletions(-) delete mode 100644 packages/core/src/api/blockManipulation/editor-doc.json delete mode 100644 packages/core/src/api/blockManipulation/transaction-doc.json delete mode 100644 packages/core/src/api/blockManipulation/transactions.test.ts diff --git a/packages/core/src/api/blockManipulation/editor-doc.json b/packages/core/src/api/blockManipulation/editor-doc.json deleted file mode 100644 index 278302490b..0000000000 --- a/packages/core/src/api/blockManipulation/editor-doc.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "content": [ - { - "content": [ - { - "attrs": { - "backgroundColor": "default", - "id": "1", - "textColor": "default", - }, - "content": [ - { - "attrs": { - "textAlignment": "left", - }, - "content": [ - { - "text": "Hey-yo", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "blockContainer", - }, - { - "attrs": { - "backgroundColor": "default", - "id": "0", - "textColor": "default", - }, - "content": [ - { - "attrs": { - "textAlignment": "left", - }, - "type": "paragraph", - }, - ], - "type": "blockContainer", - }, - ], - "type": "blockGroup", - }, - ], - "type": "doc", -} \ No newline at end of file diff --git a/packages/core/src/api/blockManipulation/transaction-doc.json b/packages/core/src/api/blockManipulation/transaction-doc.json deleted file mode 100644 index b6fcb9454e..0000000000 --- a/packages/core/src/api/blockManipulation/transaction-doc.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "content": [ - { - "content": [ - { - "attrs": { - "backgroundColor": "default", - "id": null, - "textColor": "default", - }, - "content": [ - { - "attrs": { - "textAlignment": "left", - }, - "content": [ - { - "text": "Hey-yo", - "type": "text", - }, - ], - "type": "paragraph", - }, - ], - "type": "blockContainer", - }, - { - "attrs": { - "backgroundColor": "default", - "id": "0", - "textColor": "default", - }, - "content": [ - { - "attrs": { - "textAlignment": "left", - }, - "type": "paragraph", - }, - ], - "type": "blockContainer", - }, - ], - "type": "blockGroup", - }, - ], - "type": "doc", -} \ No newline at end of file diff --git a/packages/core/src/api/blockManipulation/transactions.test.ts b/packages/core/src/api/blockManipulation/transactions.test.ts deleted file mode 100644 index ba76e79406..0000000000 --- a/packages/core/src/api/blockManipulation/transactions.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// import { describe, expect, it } from "vitest"; - -// import { setupTestEnv } from "./setupTestEnv.js"; -// import { Transaction } from "prosemirror-state"; - -// const getEditor = setupTestEnv(); - -// describe("Test blocknote transactions", () => { -// it("should return the correct block info when not in a transaction", async () => { -// const editor = getEditor(); -// editor.removeBlocks(editor.document); - -// const tr = editor.transaction; -// const nodeToInsert = { -// type: "blockContainer", -// attrs: { -// // Intentionally missing id -// // id: "1", -// textColor: "default", -// backgroundColor: "default", -// }, -// content: [ -// { -// type: "paragraph", -// attrs: { -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Hey-yo", -// }, -// ], -// }, -// ], -// }; - -// tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); -// await expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); - -// editor.dispatch(tr); - -// expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( -// "editor-doc.json" -// ); -// }); - -// it.skip("should return the correct block info", async () => { -// const editor = getEditor(); -// editor.removeBlocks(editor.document); - -// let tr = undefined as unknown as Transaction; - -// editor.transact(() => { -// tr = editor.transaction; -// const nodeToInsert = { -// type: "blockContainer", -// attrs: { -// // Intentionally missing id -// // id: "1", -// textColor: "default", -// backgroundColor: "default", -// }, -// content: [ -// { -// type: "paragraph", -// attrs: { -// textAlignment: "left", -// }, -// content: [ -// { -// type: "text", -// text: "Hey-yo", -// }, -// ], -// }, -// ], -// }; - -// tr.insert(1, editor.pmSchema.nodeFromJSON(nodeToInsert)); -// expect(tr.doc.toJSON()).toMatchFileSnapshot("transaction-doc.json"); - -// editor.dispatch(tr); -// expect(editor.transaction.doc.toJSON()).toMatchFileSnapshot( -// "editor-doc.json" -// ); -// }); -// }); -// }); From 3440c3de0dbe70a316fb597baa46af92ecad0cf2 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 12:18:22 +0200 Subject: [PATCH 24/26] refactor: reorganize the arguments --- packages/core/src/api/blockManipulation/insertContentAt.ts | 2 +- packages/core/src/editor/BlockNoteEditor.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/api/blockManipulation/insertContentAt.ts b/packages/core/src/api/blockManipulation/insertContentAt.ts index eca09e31eb..33ee527291 100644 --- a/packages/core/src/api/blockManipulation/insertContentAt.ts +++ b/packages/core/src/api/blockManipulation/insertContentAt.ts @@ -5,9 +5,9 @@ import type { Transaction } from "prosemirror-state"; // similar to tiptap insertContentAt export function insertContentAt( + tr: Transaction, position: number | { from: number; to: number }, nodes: Node[], - tr: Transaction, options: { updateSelection: boolean; } = { updateSelection: true } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index bd160de6be..098e6f8fdb 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1185,12 +1185,12 @@ export class BlockNoteEditor< this.transact((tr) => { insertContentAt( + tr, { from: tr.selection.from, to: tr.selection.to, }, - nodes, - tr + nodes ); }); } From 387088de9baea9e2d7f5fef49c5eff2da84d5de1 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 12:41:25 +0200 Subject: [PATCH 25/26] refactor: do not use prosemirrorState --- packages/core/src/api/positionMapping.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/api/positionMapping.ts b/packages/core/src/api/positionMapping.ts index 1f89e7b326..595226d099 100644 --- a/packages/core/src/api/positionMapping.ts +++ b/packages/core/src/api/positionMapping.ts @@ -61,7 +61,9 @@ export function trackPosition( */ side: "left" | "right" = "left" ): () => number { - const ySyncPluginState = ySyncPluginKey.getState(editor.prosemirrorState) as { + const ySyncPluginState = ySyncPluginKey.getState( + editor._tiptapEditor.state + ) as { doc: Y.Doc; binding: ProsemirrorBinding; }; @@ -93,7 +95,7 @@ export function trackPosition( return () => { const curYSyncPluginState = ySyncPluginKey.getState( - editor.prosemirrorState + editor._tiptapEditor.state ) as typeof ySyncPluginState; const pos = relativePositionToAbsolutePosition( curYSyncPluginState.doc, From 6e6c5fdf28c6ff65fb67902394ad5a1d00256dcb Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 16 Apr 2025 13:33:12 +0200 Subject: [PATCH 26/26] chore: add jsdoc --- packages/core/src/editor/BlockNoteEditor.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 098e6f8fdb..ff173c3468 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1080,6 +1080,11 @@ export class BlockNoteEditor< return this.transact((tr) => getSelection(tr)); } + /** + * Sets the selection to a range of blocks. + * @param startBlock The identifier of the block that should be the start of the selection. + * @param endBlock The identifier of the block that should be the end of the selection. + */ public setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier) { return this.transact((tr) => setSelection(tr, startBlock, endBlock)); }