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/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..789e31b499 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.test.ts @@ -1,137 +1,170 @@ 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, blocksToInsert, referenceBlock, placement) + ); +} + 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/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index dc0ea2fd80..0863484df0 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -1,7 +1,6 @@ -import { Node } from "prosemirror-model"; +import { Fragment, Slice } from "prosemirror-model"; import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier, BlockSchema, @@ -11,63 +10,46 @@ import { 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 { getPmSchema } from "../../../pmUtil.js"; export function insertBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" = "before" ): Block[] { const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; + const pmSchema = getPmSchema(tr); + const nodesToInsert = blocksToInsert.map((block) => + blockToNode(block, pmSchema) + ); - const nodesToInsert: Node[] = []; - for (const blockSpec of blocksToInsert) { - nodesToInsert.push( - blockToNode(blockSpec, editor.pmSchema, editor.schema.styleSchema) - ); - } - - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + 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; } + tr.step( + new ReplaceStep(pos, pos, new Slice(Fragment.from(nodesToInsert), 0, 0)) + ); + // 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, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ) - ); - } + const insertedBlocks = nodesToInsert.map((node) => + nodeToBlock(node, pmSchema) + ); return insertedBlocks; } 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..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()._tiptapEditor.state).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()._tiptapEditor.state.selection.$anchor.parentOffset; + const firstBlockEndOffset = getEditor().transact( + (tr) => tr.selection.$anchor.parentOffset + ); getEditor().setTextCursorPosition("paragraph-1"); mergeBlocks(getPosBeforeSelectedBlock()); const anchorIsAtOldFirstBlockEndPos = - getEditor()._tiptapEditor.state.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 d8b4e5d40d..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()._tiptapEditor.state); + const blockInfo = getEditor().transact((tr) => + getBlockInfoFromTransaction(tr) + ); if (!blockInfo.isBlockContainer) { throw new Error( `Selection points to a ${blockInfo.blockNoteType} node, not a blockContainer node` @@ -21,34 +23,26 @@ function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { } const { blockContent } = blockInfo; + const editor = getEditor(); if (selectionType === "cell") { - getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( + editor.transact((tr) => + 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.transact((tr) => + tr.setSelection(NodeSelection.create(tr.doc, blockContent.beforePos)) ); } else { - getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( + editor.transact((tr) => + tr.setSelection( TextSelection.create( - getEditor()._tiptapEditor.state.doc, + tr.doc, blockContent.beforePos + 1, blockContent.afterPos - 1 ) @@ -64,12 +58,12 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setTextCursorPosition("paragraph-1"); makeSelectionSpanContent("text"); expect( - selection.eq(getEditor()._tiptapEditor.state.selection) + selection.eq(getEditor().transact((tr) => tr.selection)) ).toBeTruthy(); }); @@ -79,12 +73,12 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setTextCursorPosition("image-0"); makeSelectionSpanContent("node"); expect( - selection.eq(getEditor()._tiptapEditor.state.selection) + selection.eq(getEditor().transact((tr) => tr.selection)) ).toBeTruthy(); }); @@ -94,12 +88,12 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setTextCursorPosition("table-0"); makeSelectionSpanContent("cell"); expect( - selection.eq(getEditor()._tiptapEditor.state.selection) + selection.eq(getEditor().transact((tr) => tr.selection)) ).toBeTruthy(); }); @@ -108,11 +102,11 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setSelection("paragraph-1", "paragraph-2"); expect( - selection.eq(getEditor()._tiptapEditor.state.selection) + selection.eq(getEditor().transact((tr) => tr.selection)) ).toBeTruthy(); }); @@ -121,11 +115,11 @@ describe("Test moveSelectedBlockAndSelection", () => { moveSelectedBlocksAndSelection(getEditor(), "paragraph-0", "before"); - const selection = getEditor()._tiptapEditor.state.selection; + const selection = getEditor().transact((tr) => tr.selection); getEditor().setSelection("paragraph-6", "table-0"); expect( - selection.eq(getEditor()._tiptapEditor.state.selection) + 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 c99d92de6f..acbb5d3973 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"; @@ -38,36 +43,35 @@ type BlockSelectionData = ( function getBlockSelectionData( editor: BlockNoteEditor ): BlockSelectionData { - const state = editor._tiptapEditor.state; - const selection = state.selection; - - const anchorBlockPosInfo = getNearestBlockPos(state.doc, selection.anchor); - - if (selection instanceof CellSelection) { - return { - type: "cell" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, - anchorCellOffset: - selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, - headCellOffset: - selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, - }; - } else if (editor._tiptapEditor.state.selection instanceof NodeSelection) { - return { - type: "node" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, - }; - } else { - const headBlockPosInfo = getNearestBlockPos(state.doc, 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, - }; - } + 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, + }; + } + }); } /** @@ -77,18 +81,15 @@ 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 anchorBlockPos = getNodeById( - data.anchorBlockId, - editor._tiptapEditor.state.doc - )?.posBeforeNode; + 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 +99,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 +114,13 @@ 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)); + tr.setSelection(selection); } /** @@ -168,15 +163,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((tr) => { + const blocks = editor.getSelection()?.blocks || [ + editor.getTextCursorPosition().block, + ]; + const selectionData = getBlockSelectionData(editor); + + editor.removeBlocks(blocks); + editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); + + updateBlockSelectionFromData(tr, selectionData); + }); } // Checks if a block is in a valid place after being moved. This check is @@ -292,45 +290,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; - moveSelectedBlocksAndSelection( - editor, - moveDownPlacement.referenceBlock, - moveDownPlacement.placement - ); + const moveDownPlacement = getMoveDownPlacement( + editor, + editor.getNextBlock(block), + editor.getParentBlock(block) + ); + + 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..9917a9f979 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -1,9 +1,9 @@ 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"; -import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; // TODO: Unit tests /** @@ -12,7 +12,7 @@ import { getBlockInfoFromSelection } 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) ); } @@ -80,21 +80,17 @@ export function unnestBlock(editor: BlockNoteEditor) { } export function canNestBlock(editor: BlockNoteEditor) { - const { bnBlock: blockContainer } = getBlockInfoFromSelection( - editor._tiptapEditor.state - ); + return editor.transact((tr) => { + const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); - return ( - editor._tiptapEditor.state.doc.resolve(blockContainer.beforePos) - .nodeBefore !== null - ); + return tr.doc.resolve(blockContainer.beforePos).nodeBefore !== null; + }); } + export function canUnnestBlock(editor: BlockNoteEditor) { - const { bnBlock: blockContainer } = getBlockInfoFromSelection( - editor._tiptapEditor.state - ); + return editor.transact((tr) => { + const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); - return ( - editor._tiptapEditor.state.doc.resolve(blockContainer.beforePos).depth > 1 - ); + return tr.doc.resolve(blockContainer.beforePos).depth > 1; + }); } 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..4ba6721b4d 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,23 @@ 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, blocksToRemove, blocksToInsert) + ); +} + 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 bd6ad6687e..10796c3f61 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,38 +1,34 @@ -import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; -import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; -import { +import type { Node } from "prosemirror-model"; +import type { Transaction } from "prosemirror-state"; +import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import type { 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"; +import { getPmSchema } from "../../../pmUtil.js"; export function removeAndInsertBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + tr: Transaction, blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock[] ): { insertedBlocks: Block[]; removedBlocks: Block[]; } { - const ttEditor = editor._tiptapEditor; - let tr = ttEditor.state.tr; - + 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, editor.pmSchema, editor.schema.styleSchema) - ); - } + const nodesToInsert: Node[] = blocksToInsert.map((block) => + blockToNode(block, pmSchema) + ); const idsOfBlocksToRemove = new Set( blocksToRemove.map((block) => @@ -47,7 +43,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; @@ -62,20 +58,12 @@ export function removeAndInsertBlocks< } // Saves the block that is being deleted. - removedBlocks.push( - nodeToBlock( - node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ) - ); + removedBlocks.push(nodeToBlock(node, pmSchema)); idsOfBlocksToRemove.delete(node.attrs.id); 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 +79,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; @@ -111,36 +99,10 @@ 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 - ) - ); - } + const insertedBlocks = nodesToInsert.map((node) => + nodeToBlock(node, pmSchema) + ); 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/splitBlock/splitBlock.test.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts index 444729e69c..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"; @@ -38,8 +38,8 @@ function setSelectionWithOffset( throw new Error("Target block is not a block container"); } - getEditor().dispatch( - getEditor()._tiptapEditor.state.tr.setSelection( + getEditor().transact((tr) => + tr.setSelection( TextSelection.create(doc, info.blockContent.beforePos + offset + 1) ) ); @@ -47,97 +47,103 @@ function setSelectionWithOffset( describe("Test splitBlocks", () => { it("Basic", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-0", - 4 - ); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-0", 4); + }); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); expect(getEditor().document).toMatchSnapshot(); }); it("End of content", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-0", - 11 - ); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-0", 11); + }); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); expect(getEditor().document).toMatchSnapshot(); }); it("Block has children", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-with-children", - 4 - ); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-with-children", 4); + }); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); expect(getEditor().document).toMatchSnapshot(); }); it("Keep type", () => { - setSelectionWithOffset(getEditor()._tiptapEditor.state.doc, "heading-0", 4); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "heading-0", 4); + }); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor, true); + splitBlock( + getEditor().transact((tr) => tr.selection.anchor), + true + ); expect(getEditor().document).toMatchSnapshot(); }); it("Don't keep type", () => { - setSelectionWithOffset(getEditor()._tiptapEditor.state.doc, "heading-0", 4); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "heading-0", 4); + }); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor, false); + splitBlock( + getEditor().transact((tr) => tr.selection.anchor), + false + ); expect(getEditor().document).toMatchSnapshot(); }); it.skip("Keep props", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.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()._tiptapEditor.state.selection.anchor, false, true); - expect(getEditor().document).toMatchSnapshot(); }); it("Don't keep props", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.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()._tiptapEditor.state.selection.anchor, false, false); - expect(getEditor().document).toMatchSnapshot(); }); it("Selection is set", () => { - setSelectionWithOffset( - getEditor()._tiptapEditor.state.doc, - "paragraph-0", - 4 - ); + getEditor().transact((tr) => { + setSelectionWithOffset(tr.doc, "paragraph-0", 4); + }); - splitBlock(getEditor()._tiptapEditor.state.selection.anchor); + splitBlock(getEditor().transact((tr) => tr.selection.anchor)); - const { bnBlock } = getBlockInfoFromSelection( - getEditor()._tiptapEditor.state + const bnBlock = getEditor().transact( + (tr) => getBlockInfoFromTransaction(tr).bnBlock ); const anchorIsAtStartOfNewBlock = bnBlock.node.attrs.id === "0" && - getEditor()._tiptapEditor.state.selection.$anchor.parentOffset === 0; + getEditor().transact((tr) => tr.selection.$anchor.parentOffset) === 0; expect(anchorIsAtStartOfNewBlock).toBeTruthy(); }); 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..cb738b43b2 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,367 @@ 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, "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, "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, "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, "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, "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, "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, "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, "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", + expect( + getEditor().transact((tr) => + updateBlock(tr, "heading-with-everything", { children: [ { - id: "new-double-nested-paragraph", + id: "new-nested-paragraph", type: "paragraph", - content: "New double Nested Paragraph 2", + 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", + expect( + getEditor().transact((tr) => + updateBlock(tr, "heading-with-everything", { + id: "new-id", type: "paragraph", - content: "New nested Paragraph 2", + 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-double-nested-paragraph", + id: "new-nested-paragraph", type: "paragraph", - content: "New double Nested Paragraph 2", + 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, "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, "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"], - }, - { - cells: ["Cell 4", "Cell 5", "Cell 6"], - }, - { - cells: ["Cell 7", "Cell 8", "Cell 9"], + expect( + getEditor().transact((tr) => + 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(); 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, "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, "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, "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, "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, "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, "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, "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 a1cd756a3c..fbea6739d8 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -1,18 +1,22 @@ -import { Fragment, NodeType, Node as PMNode, Slice } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; +import { + Fragment, + 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 { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { +import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.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 { @@ -22,98 +26,90 @@ import { } 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 - ) => - ({ - state, +export const updateBlockCommand = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + posBeforeBlock: number, + block: PartialBlock +) => { + return ({ + tr, dispatch, }: { - state: EditorState; - dispatch: ((args?: any) => any) | undefined; - }) => { - const blockInfo = getBlockInfoFromResolvedPos( - state.doc.resolve(posBeforeBlock) - ); - + tr: Transaction; + dispatch?: () => void; + }): boolean => { if (dispatch) { - // Adds blockGroup node with child blocks if necessary. + updateBlockTr(tr, posBeforeBlock, block); + } + return true; + }; +}; - const oldNodeType = state.schema.nodes[blockInfo.blockNoteType]; - const newNodeType = - state.schema.nodes[block.type || blockInfo.blockNoteType]; - const newBnBlockNodeType = newNodeType.isInGroup("bnBlock") - ? newNodeType - : state.schema.nodes["blockContainer"]; +const updateBlockTr = < + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + tr: Transaction, + posBeforeBlock: number, + block: PartialBlock +) => { + const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock)); - if (blockInfo.isBlockContainer && newNodeType.isInGroup("blockContent")) { - updateChildren(block, state, editor, blockInfo); - // The code below determines the new content of the block. - // or "keep" to keep as-is - updateBlockContentNode( - block, - state, - editor, - oldNodeType, - newNodeType, - blockInfo - ); - } else if ( - !blockInfo.isBlockContainer && - newNodeType.isInGroup("bnBlock") - ) { - updateChildren(block, state, 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 { - // switching from blockContainer to non-blockContainer or v.v. - // currently breaking for column slash menu items converting empty block - // to column. + const pmSchema = getPmSchema(tr); + // 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, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); - state.tr.replaceWith( - blockInfo.bnBlock.beforePos, - blockInfo.bnBlock.afterPos, - blockToNode( - { - children: existingBlock.children, // if no children are passed in, use existing children - ...block, - }, - state.schema, - editor.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, blockInfo); + // The code below determines the new content of the block. + // or "keep" to keep as-is + updateBlockContentNode(block, tr, oldNodeType, newNodeType, blockInfo); + } else if (!blockInfo.isBlockContainer && newNodeType.isInGroup("bnBlock")) { + 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 { + // 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. - state.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, pmSchema); + tr.replaceWith( + blockInfo.bnBlock.beforePos, + blockInfo.bnBlock.afterPos, + blockToNode( + { + children: existingBlock.children, // if no children are passed in, use existing children + ...block, + }, + pmSchema + ) + ); - 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, @@ -121,8 +117,7 @@ function updateBlockContentNode< S extends StyleSchema >( block: PartialBlock, - state: EditorState, - editor: BlockNoteEditor, + tr: Transaction, oldNodeType: NodeType, newNodeType: NodeType, blockInfo: { @@ -132,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? @@ -140,25 +136,15 @@ function updateBlockContentNode< // Adds a single text node with no marks to the content. content = inlineContentToNodes( [block.content], - state.schema, - editor.schema.styleSchema, + pmSchema, 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, - state.schema, - editor.schema.styleSchema, - newNodeType.name - ); + content = inlineContentToNodes(block.content, pmSchema, newNodeType.name); } else if (block.content.type === "tableContent") { - content = tableContentToNodes( - block.content, - state.schema, - editor.schema.styleSchema - ); + content = tableContentToNodes(block.content, pmSchema); } else { throw new UnreachableCaseError(block.content.type); } @@ -186,9 +172,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 : pmSchema.nodes[block.type], { ...blockInfo.blockContent.node.attrs, ...block.props, @@ -198,7 +184,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( @@ -216,15 +202,11 @@ function updateChildren< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->( - block: PartialBlock, - state: EditorState, - editor: BlockNoteEditor, - 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, state.schema, editor.schema.styleSchema); + return blockToNode(child, pmSchema); }); // Checks if a blockGroup node already exists. @@ -232,7 +214,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,51 +226,36 @@ 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) + pmSchema.nodes["blockGroup"].createChecked({}, childNodes) ); } } } export function updateBlock< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema + BSchema extends BlockSchema = any, + I extends InlineContentSchema = any, + S extends StyleSchema = any >( - editor: BlockNoteEditor, + tr: Transaction, blockToUpdate: BlockIdentifier, update: PartialBlock ): Block { - const ttEditor = editor._tiptapEditor; - const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; - - const posInfo = getNodeById(id, ttEditor.state.doc); + const posInfo = getNodeById(id, tr.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; - }); + updateBlockTr(tr, posInfo.posBeforeNode, update); - const blockContainerNode = ttEditor.state.doc + 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 - ); + 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 4bf9ed225b..fe8c607f67 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -1,6 +1,6 @@ -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 { BlockIdentifier, BlockSchema, InlineContentSchema, @@ -8,30 +8,26 @@ import { } 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, I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + doc: Node, blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; + const pmSchema = getPmSchema(doc); - const posInfo = getNodeById(id, editor._tiptapEditor.state.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 - ); + return nodeToBlock(posInfo.node, pmSchema); } export function getPrevBlock< @@ -39,32 +35,25 @@ export function getPrevBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + doc: Node, blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, doc); + const pmSchema = getPmSchema(doc); if (!posInfo) { return undefined; } - const $posBeforeNode = editor._tiptapEditor.state.doc.resolve( - posInfo.posBeforeNode - ); + const $posBeforeNode = doc.resolve(posInfo.posBeforeNode); const nodeToConvert = $posBeforeNode.nodeBefore; if (!nodeToConvert) { return undefined; } - return nodeToBlock( - nodeToConvert, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); + return nodeToBlock(nodeToConvert, pmSchema); } export function getNextBlock< @@ -72,18 +61,18 @@ export function getNextBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + doc: Node, blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, doc); + const pmSchema = getPmSchema(doc); if (!posInfo) { return undefined; } - const $posAfterNode = editor._tiptapEditor.state.doc.resolve( + const $posAfterNode = doc.resolve( posInfo.posBeforeNode + posInfo.node.nodeSize ); const nodeToConvert = $posAfterNode.nodeAfter; @@ -91,13 +80,7 @@ export function getNextBlock< return undefined; } - return nodeToBlock( - nodeToConvert, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); + return nodeToBlock(nodeToConvert, pmSchema); } export function getParentBlock< @@ -105,20 +88,18 @@ export function getParentBlock< I extends InlineContentSchema, S extends StyleSchema >( - editor: BlockNoteEditor, + doc: Node, blockIdentifier: BlockIdentifier ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - - const posInfo = getNodeById(id, editor._tiptapEditor.state.doc); + const pmSchema = getPmSchema(doc); + const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } - const $posBeforeNode = editor._tiptapEditor.state.doc.resolve( - posInfo.posBeforeNode - ); + const $posBeforeNode = doc.resolve(posInfo.posBeforeNode); const parentNode = $posBeforeNode.node(); const grandparentNode = $posBeforeNode.node(-1); const nodeToConvert = @@ -131,11 +112,5 @@ export function getParentBlock< return undefined; } - return nodeToBlock( - nodeToConvert, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); + return nodeToBlock(nodeToConvert, pmSchema); } diff --git a/packages/core/src/api/blockManipulation/insertContentAt.ts b/packages/core/src/api/blockManipulation/insertContentAt.ts index 64791083d3..33ee527291 100644 --- a/packages/core/src/api/blockManipulation/insertContentAt.ts +++ b/packages/core/src/api/blockManipulation/insertContentAt.ts @@ -1,28 +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( + tr: Transaction, + position: number | { from: number; to: number }, nodes: Node[], - editor: BlockNoteEditor, options: { updateSelection: boolean; } = { updateSelection: true } ) { - const tr = editor._tiptapEditor.state.tr; - // don’t dispatch an empty fragment because this can lead to strange errors // if (content.toString() === "<>") { // return true; @@ -90,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..d39869b4be 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.test.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.test.ts @@ -7,50 +7,66 @@ const getEditor = setupTestEnv(); describe("Test getSelection & setSelection", () => { it("Basic", () => { - setSelection(getEditor(), "paragraph-0", "paragraph-1"); + getEditor().transact((tr) => { + setSelection(tr, "paragraph-0", "paragraph-1"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Starts in block with children", () => { - setSelection(getEditor(), "paragraph-with-children", "paragraph-2"); + getEditor().transact((tr) => { + setSelection(tr, "paragraph-with-children", "paragraph-2"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Starts in nested block", () => { - setSelection(getEditor(), "nested-paragraph-0", "paragraph-2"); + getEditor().transact((tr) => { + setSelection(tr, "nested-paragraph-0", "paragraph-2"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Ends in block with children", () => { - setSelection(getEditor(), "paragraph-1", "paragraph-with-children"); + getEditor().transact((tr) => { + setSelection(tr, "paragraph-1", "paragraph-with-children"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Ends in nested block", () => { - setSelection(getEditor(), "paragraph-1", "nested-paragraph-0"); + getEditor().transact((tr) => { + setSelection(tr, "paragraph-1", "nested-paragraph-0"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Contains block with children", () => { - setSelection(getEditor(), "paragraph-1", "paragraph-2"); + getEditor().transact((tr) => { + setSelection(tr, "paragraph-1", "paragraph-2"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Starts in table", () => { - setSelection(getEditor(), "table-0", "paragraph-7"); + getEditor().transact((tr) => { + setSelection(tr, "table-0", "paragraph-7"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); it("Ends in table", () => { - setSelection(getEditor(), "paragraph-6", "table-0"); + getEditor().transact((tr) => { + setSelection(tr, "paragraph-6", "table-0"); + }); - expect(getSelection(getEditor())).toMatchSnapshot(); + expect(getEditor().transact((tr) => getSelection(tr))).toMatchSnapshot(); }); }); diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index 2017581e41..aed087f080 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -1,8 +1,7 @@ -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 { Selection } from "../../../editor/selectionTypes.js"; import { BlockIdentifier, @@ -13,26 +12,24 @@ 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 ->( - editor: BlockNoteEditor -): Selection | undefined { - const state = editor._tiptapEditor.state; - +>(tr: Transaction): Selection | undefined { + const pmSchema = getPmSchema(tr); // 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` @@ -43,7 +40,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( @@ -51,13 +48,7 @@ export function getSelection< ); } - return nodeToBlock( - node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ); + return nodeToBlock(node, pmSchema); }; const blocks: Block[] = []; @@ -98,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!, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.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. @@ -137,7 +120,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})` ); } @@ -146,32 +129,27 @@ export function getSelection< }; } -export function setSelection< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, +export function setSelection( + tr: Transaction, 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( `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 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`); } @@ -180,12 +158,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 ( @@ -226,7 +204,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 +214,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._tiptapEditor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create(editor._tiptapEditor.state.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 f7ed692796..4ad98d0808 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.test.ts @@ -10,44 +10,65 @@ const getEditor = setupTestEnv(); describe("Test getTextCursorPosition & setTextCursorPosition", () => { it("Basic", () => { - setTextCursorPosition(getEditor(), "paragraph-1"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, "paragraph-1"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("First block", () => { - setTextCursorPosition(getEditor(), "paragraph-0"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, "paragraph-0"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("Last block", () => { - setTextCursorPosition(getEditor(), "trailing-paragraph"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, "trailing-paragraph"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("Nested block", () => { - setTextCursorPosition(getEditor(), "nested-paragraph-0"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, "nested-paragraph-0"); + }); - expect(getTextCursorPosition(getEditor())).toMatchSnapshot(); + expect( + getEditor().transact((tr) => getTextCursorPosition(tr)) + ).toMatchSnapshot(); }); it("Set to start", () => { - setTextCursorPosition(getEditor(), "paragraph-1", "start"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, "paragraph-1", "start"); + }); expect( - getEditor()._tiptapEditor.state.selection.$from.parentOffset === 0 + getEditor().transact((tr) => tr.selection.$from.parentOffset) === 0 ).toBeTruthy(); }); it("Set to end", () => { - setTextCursorPosition(getEditor(), "paragraph-1", "end"); + getEditor().transact((tr) => { + setTextCursorPosition(tr, "paragraph-1", "end"); + }); expect( - getEditor()._tiptapEditor.state.selection.$from.parentOffset === - getEditor()._tiptapEditor.state.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/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts index b7013162e6..0da001e9cc 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition/textCursorPosition.ts @@ -1,8 +1,11 @@ -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 { TextCursorPosition } from "../../../../editor/cursorPositionTypes.js"; +import type { BlockIdentifier, BlockSchema, InlineContentSchema, @@ -11,26 +14,26 @@ 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"; +import { getBlockNoteSchema, getPmSchema } from "../../../pmUtil.js"; export function getTextCursorPosition< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema ->(editor: BlockNoteEditor): TextCursorPosition { - const { bnBlock } = getBlockInfoFromSelection(editor._tiptapEditor.state); +>(tr: Transaction): TextCursorPosition { + const { bnBlock } = getBlockInfoFromTransaction(tr); + const pmSchema = getPmSchema(tr.doc); - const resolvedPos = editor._tiptapEditor.state.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._tiptapEditor.state.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; @@ -44,58 +47,24 @@ export function getTextCursorPosition< } return { - block: nodeToBlock( - bnBlock.node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ), - prevBlock: - prevNode === null - ? undefined - : nodeToBlock( - prevNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ), - nextBlock: - nextNode === null - ? undefined - : nodeToBlock( - nextNode, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.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, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema, - editor.blockCache - ), + parentNode === undefined ? undefined : nodeToBlock(parentNode, pmSchema), }; } -export function setTextCursorPosition< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, +export function setTextCursorPosition( + tr: Transaction, targetBlock: BlockIdentifier, 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, editor._tiptapEditor.state.doc); + const posInfo = getNodeById(id, tr.doc); if (!posInfo) { throw new Error(`Block with ID ${id} not found`); } @@ -103,23 +72,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") { - editor._tiptapEditor.commands.setNodeSelection(blockContent.beforePos); + tr.setSelection(NodeSelection.create(tr.doc, blockContent.beforePos)); return; } if (contentType === "inline") { 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") { @@ -127,12 +96,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 { @@ -144,6 +113,6 @@ export function setTextCursorPosition< ? info.childContainer.node.firstChild! : info.childContainer.node.lastChild!; - setTextCursorPosition(editor, child.attrs.id, placement); + setTextCursorPosition(tr, child.attrs.id, placement); } } diff --git a/packages/core/src/api/clipboard/clipboardExternal.test.ts b/packages/core/src/api/clipboard/clipboardExternal.test.ts index 97ad51bedc..d34d1d2fed 100644 --- a/packages/core/src/api/clipboard/clipboardExternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardExternal.test.ts @@ -83,11 +83,7 @@ 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) - ) - ); + 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 9f958ee60f..6462cdbc79 100644 --- a/packages/core/src/api/clipboard/clipboardInternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardInternal.test.ts @@ -297,10 +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) - ) + editor.transact((tr) => + tr.setSelection(testCase.createCopySelection(tr.doc)) ); const { clipboardHTML, externalHTML } = selectedFragmentToHTML( @@ -313,10 +311,8 @@ describe("Test ProseMirror selection clipboard HTML", () => { ); if (testCase.createPasteSelection) { - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - testCase.createPasteSelection(editor.prosemirrorView.state.doc) - ) + editor.transact((tr) => + 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 6615b048a2..ccdca124e6 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -160,16 +160,14 @@ export async function handleFileInsertion< return; } - const posInfo = getNearestBlockPos( - editor._tiptapEditor.state.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/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 30d57bd66f..4ad69e8456 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; @@ -120,9 +120,9 @@ export function selectedFragmentToHTML< "node" in view.state.selection && (view.state.selection.node as Node).type.spec.group === "blockContent" ) { - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - new NodeSelection(view.state.doc.resolve(view.state.selection.from - 1)) + editor.transact((tr) => + tr.setSelection( + new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)) ) ); } @@ -251,10 +251,10 @@ export const createCopyToClipboardExtension = < } // Expands the selection to the parent `blockContainer` node. - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( + editor.transact((tr) => + 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/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/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/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index e689ef2578..8280d4dda8 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 { getStyleSchema } from "../pmUtil.js"; /** * Convert a StyledText inline element to a @@ -139,8 +140,8 @@ export function inlineContentToNodes< >( blockContent: PartialInlineContent, schema: Schema, - styleSchema: S, - blockType?: string + blockType?: string, + styleSchema: S = getStyleSchema(schema) ): Node[] { const nodes: Node[] = []; @@ -173,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 @@ -222,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); @@ -234,7 +240,12 @@ export function tableContentToNodes< }); } } else { - content = inlineContentToNodes(cell, schema, styleSchema); + content = inlineContentToNodes( + cell, + schema, + "tableParagraph", + styleSchema + ); } const cellNode = schema.nodes[ @@ -279,16 +290,16 @@ function blockOrInlineContentToContentNode( const nodes = inlineContentToNodes( [block.content], schema, - styleSchema, - type + type, + styleSchema ); contentNode = schema.nodes[type].createChecked(block.props, nodes); } else if (Array.isArray(block.content)) { const nodes = inlineContentToNodes( block.content, schema, - styleSchema, - type + type, + styleSchema ); contentNode = schema.nodes[type].createChecked(block.props, nodes); } else if (block.content.type === "tableContent") { @@ -306,7 +317,7 @@ function blockOrInlineContentToContentNode( 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 671121a609..59edf8a60d 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 { @@ -21,6 +21,9 @@ import { isStyledTextInlineContent, } from "../../schema/inlineContent/types.js"; import { UnreachableCaseError } from "../../util/typescript.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 @@ -385,15 +388,14 @@ export function nodeToBlock< S extends StyleSchema >( node: Node, - blockSchema: BSchema, - inlineContentSchema: I, - styleSchema: S, - blockCache?: WeakMap> + 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); @@ -439,6 +441,7 @@ export function nodeToBlock< children.push( nodeToBlock( child, + schema, blockSchema, inlineContentSchema, styleSchema, diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index 7602d0be1c..0f5699b4aa 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -31,14 +31,16 @@ async function parseHTMLAndCompareSnapshots( (window as any).__TEST_OPTIONS.mockID = 0; // reset id counter const htmlNode = nestedListsToBlockNoteStructure(html); - const slice = (pmView as any).__parseFromClipboard( - editor.prosemirrorView, - "", - htmlNode.innerHTML, - false, - editor._tiptapEditor.state.selection.$from - ); - editor.dispatch(editor._tiptapEditor.state.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/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 new file mode 100644 index 0000000000..95fa59fca3 --- /dev/null +++ b/packages/core/src/api/pmUtil.ts @@ -0,0 +1,54 @@ +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 getPmSchema(trOrNode: Transaction | Node) { + if ("doc" in trOrNode) { + return trOrNode.doc.type.schema; + } + return trOrNode.type.schema; +} + +function getBlockNoteEditor< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>(schema: Schema): BlockNoteEditor { + return schema.cached.blockNoteEditor as BlockNoteEditor; +} + +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 getBlockSchema( + schema: Schema +): BSchema { + return getBlockNoteSchema(schema).blockSchema as BSchema; +} + +export function getInlineContentSchema( + schema: Schema +): I { + return getBlockNoteSchema(schema).inlineContentSchema as I; +} + +export function getStyleSchema(schema: Schema): S { + return getBlockNoteSchema(schema).styleSchema as S; +} + +export function getBlockCache(schema: Schema) { + return getBlockNoteEditor(schema).blockCache; +} 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, diff --git a/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts b/packages/core/src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts index 1b033a4bc5..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._tiptapEditor.state.tr.setMeta(editor.filePanel!.plugin, { + editor.transact((tr) => + tr.setMeta(editor.filePanel!.plugin, { block: block, }) ); 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 93122fdce2..c6a0529850 100644 --- a/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts @@ -1,19 +1,21 @@ 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 ttEditor = editor._tiptapEditor; - const blockInfo = getBlockInfoFromSelection(ttEditor.state); + 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 = - ttEditor.state.selection.anchor === ttEditor.state.selection.head; - if ( !( blockContent.node.type.name === "bulletListItem" || @@ -25,13 +27,13 @@ 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(() => { 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.test.ts b/packages/core/src/editor/BlockNoteEditor.test.ts index f0f4669437..d77787a93c 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 = 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 1a9a0abe78..ff173c3468 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, @@ -94,18 +93,23 @@ 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"; 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 @@ -117,6 +121,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 +426,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. @@ -723,13 +733,127 @@ export class BlockNoteEditor< // but we still need the schema this.pmSchema = getSchema(tiptapOptions.extensions!); } - + this.pmSchema.cached.blockNoteEditor = this; this.emit("create"); } - dispatch = (tr: Transaction) => { - 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 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 + * 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 = (tr: Transaction) => this._tiptapEditor.dispatch(tr); + + return command(state, dispatch, view); + } + + /** + * 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 + * } + * ``` + */ + 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; + + return command(state, undefined, view); + } + + /** + * 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) + * + * @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((tr) => { + * tr.insertText("Hello, world!"); + * // These two operations will be grouped together in a single undo step + * editor.transact((tr) => { + * tr.insertText("Hello, world!"); + * }); + * }); + * ``` + */ + 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 + ) => T + ): T { + if (this.activeTransaction) { + // Already in a transaction, so we can just callback immediately + return callback(this.activeTransaction); + } + + try { + // Enter transaction mode, by setting a starting transaction + this.activeTransaction = this._tiptapEditor.state.tr; + + // Capture all dispatch'd transactions + 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 && + // 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._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 + this.activeTransaction = null; + } + } /** * Mount the editor to a parent DOM element. Call mount(undefined) to clean up @@ -750,13 +874,6 @@ export class BlockNoteEditor< return this._tiptapEditor.view; } - /** - * Get the underlying prosemirror state - */ - public get prosemirrorState() { - return this._tiptapEditor.state; - } - public get domElement() { return this.prosemirrorView?.dom as HTMLDivElement | undefined; } @@ -803,23 +920,17 @@ export class BlockNoteEditor< * @returns A snapshot of all top-level (non-nested) blocks in the editor. */ public get document(): Block[] { - const blocks: Block[] = []; - - this.prosemirrorState.doc.firstChild!.descendants((node) => { - blocks.push( - nodeToBlock( - node, - this.schema.blockSchema, - this.schema.inlineContentSchema, - this.schema.styleSchema, - this.blockCache - ) - ); + return this.transact((tr) => { + const blocks: Block[] = []; - return false; - }); + tr.doc.firstChild!.descendants((node) => { + blocks.push(nodeToBlock(node, this.pmSchema)); + + return false; + }); - return blocks; + return blocks; + }); } /** @@ -832,7 +943,7 @@ export class BlockNoteEditor< public getBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getBlock(this, blockIdentifier); + return this.transact((tr) => getBlock(tr.doc, blockIdentifier)); } /** @@ -847,7 +958,7 @@ export class BlockNoteEditor< public getPrevBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getPrevBlock(this, blockIdentifier); + return this.transact((tr) => getPrevBlock(tr.doc, blockIdentifier)); } /** @@ -861,7 +972,7 @@ export class BlockNoteEditor< public getNextBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getNextBlock(this, blockIdentifier); + return this.transact((tr) => getNextBlock(tr.doc, blockIdentifier)); } /** @@ -874,7 +985,7 @@ export class BlockNoteEditor< public getParentBlock( blockIdentifier: BlockIdentifier ): Block | undefined { - return getParentBlock(this, blockIdentifier); + return this.transact((tr) => getParentBlock(tr.doc, blockIdentifier)); } /** @@ -944,7 +1055,7 @@ export class BlockNoteEditor< ISchema, SSchema > { - return getTextCursorPosition(this); + return this.transact((tr) => getTextCursorPosition(tr)); } /** @@ -957,18 +1068,25 @@ export class BlockNoteEditor< targetBlock: BlockIdentifier, placement: "start" | "end" = "start" ) { - setTextCursorPosition(this, targetBlock, placement); + return this.transact((tr) => + setTextCursorPosition(tr, targetBlock, placement) + ); } /** * Gets a snapshot of the current selection. */ public getSelection(): Selection | undefined { - return getSelection(this); + 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) { - setSelection(this, startBlock, endBlock); + return this.transact((tr) => setSelection(tr, startBlock, endBlock)); } /** @@ -1017,7 +1135,9 @@ export class BlockNoteEditor< referenceBlock: BlockIdentifier, placement: "before" | "after" = "before" ) { - return insertBlocks(this, blocksToInsert, referenceBlock, placement); + return this.transact((tr) => + insertBlocks(tr, blocksToInsert, referenceBlock, placement) + ); } /** @@ -1031,7 +1151,7 @@ export class BlockNoteEditor< blockToUpdate: BlockIdentifier, update: PartialBlock ) { - return updateBlock(this, blockToUpdate, update); + return this.transact((tr) => updateBlock(tr, blockToUpdate, update)); } /** @@ -1039,7 +1159,9 @@ 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, blocksToRemove, []).removedBlocks + ); } /** @@ -1053,7 +1175,9 @@ export class BlockNoteEditor< blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock[] ) { - return replaceBlocks(this, blocksToRemove, blocksToInsert); + return this.transact((tr) => + removeAndInsertBlocks(tr, blocksToRemove, blocksToInsert) + ); } /** @@ -1062,52 +1186,52 @@ 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 - ); - - insertContentAt( - { - from: this._tiptapEditor.state.selection.from, - to: this._tiptapEditor.state.selection.to, - }, - nodes, - this - ); + const nodes = inlineContentToNodes(content, this.pmSchema); + + this.transact((tr) => { + insertContentAt( + tr, + { + from: tr.selection.from, + to: tr.selection.to, + }, + nodes + ); + }); } /** * 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._tiptapEditor.state.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; + }); } /** @@ -1164,10 +1288,9 @@ 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 - ); + return this.transact((tr) => { + return tr.doc.textBetween(tr.selection.from, tr.selection.to); + }); } /** @@ -1186,21 +1309,20 @@ export class BlockNoteEditor< if (url === "") { return; } - - const { from, to } = this._tiptapEditor.state.selection; const mark = this.pmSchema.mark("link", { href: url }); + this.transact((tr) => { + 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) { + tr.insertText(text, from, to).addMark(from, from + text.length, mark); + } else { + tr.setSelection(TextSelection.create(tr.doc, to)).addMark( + from, + to, + mark + ); + } + }); } /** @@ -1237,7 +1359,7 @@ export class BlockNoteEditor< * current blocks share a common parent, moves them out of & before it. */ public moveBlocksUp() { - moveBlocksUp(this); + return moveBlocksUp(this); } /** @@ -1246,7 +1368,7 @@ export class BlockNoteEditor< * current blocks share a common parent, moves them out of & after it. */ public moveBlocksDown() { - moveBlocksDown(this); + return moveBlocksDown(this); } /** @@ -1288,13 +1410,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); } /** @@ -1319,13 +1435,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); } /** @@ -1452,24 +1562,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, { + this.focus(); + 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, - }) - ); + }); + }); } // `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..cac5f022bb 100644 --- a/packages/core/src/extensions/Comments/CommentsPlugin.ts +++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts @@ -89,41 +89,39 @@ 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); + this.editor.transact((tr) => { + 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, + }) + ); - if (isOrphan && this.selectedThreadId === markThreadId) { - // unselect - this.selectedThreadId = undefined; - this.emitStateUpdate(); + if (isOrphan && this.selectedThreadId === markThreadId) { + // unselect + this.selectedThreadId = undefined; + this.emitStateUpdate(); + } } } - } + }); }); }); }; @@ -262,8 +260,8 @@ export class CommentsPlugin extends EventEmitter { } this.selectedThreadId = threadId; this.emitStateUpdate(); - this.editor.dispatch( - this.editor.prosemirrorView!.state.tr.setMeta(PLUGIN_KEY, { + this.editor.transact((tr) => + tr.setMeta(PLUGIN_KEY, { name: SET_SELECTED_THREAD_ID, }) ); 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/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 acb8a61cc3..40cb1e0c49 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.transact((tr) => tr.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/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/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index de3bb37247..3a069c2321 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -115,9 +115,7 @@ class SuggestionMenuView< } closeMenu = () => { - this.editor.dispatch( - this.editor._tiptapEditor.state.tr.setMeta(suggestionMenuPluginKey, null) - ); + this.editor.transact((tr) => tr.setMeta(suggestionMenuPluginKey, null)); }; clearQuery = () => { @@ -128,13 +126,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._tiptapEditor.state.selection.from, + to: this.editor.transact((tr) => tr.selection.from), }) .run(); }; diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 2c070b2366..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._tiptapEditor.state.tr.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._tiptapEditor.state.tr.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._tiptapEditor.state.tr.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._tiptapEditor.state.tr.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 f4ccfcb6a2..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, @@ -263,9 +263,8 @@ export class TableHandlesView< | BlockFromConfigNoChildren | undefined; - const pmNodeInfo = getNodeById( - blockEl.id, - this.editor._tiptapEditor.state.doc + const pmNodeInfo = this.editor.transact((tr) => + getNodeById(blockEl.id, tr.doc) ); if (!pmNodeInfo) { throw new Error(`Block with ID ${blockEl.id} not found`); @@ -273,10 +272,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)) { @@ -447,9 +446,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)); } }; @@ -814,12 +811,12 @@ export class TableHandlesProsemirrorPlugin< }; this.view!.emitUpdate(); - this.editor.dispatch( - this.editor._tiptapEditor.state.tr.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, }) ); @@ -857,12 +854,12 @@ export class TableHandlesProsemirrorPlugin< }; this.view!.emitUpdate(); - this.editor.dispatch( - this.editor._tiptapEditor.state.tr.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, }) ); @@ -890,9 +887,7 @@ export class TableHandlesProsemirrorPlugin< this.view!.state.draggingState = undefined; this.view!.emitUpdate(); - this.editor.dispatch( - this.editor._tiptapEditor.state.tr.setMeta(tableHandlesPluginKey, null) - ); + this.editor.transact((tr) => tr.setMeta(tableHandlesPluginKey, null)); if (!this.editor.prosemirrorView) { throw new Error("Editor view not initialized."); @@ -939,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 ) => { @@ -948,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 @@ -986,24 +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 addRowBefore(state, this.editor.dispatch); - } else { - return addRowAfter(state, this.editor.dispatch); - } - } else { - if (direction.side === "left") { - return addColumnBefore(state, this.editor.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 addColumnAfter(state, this.editor.dispatch); + if (direction.side === "left") { + return addColumnBefore(state, dispatch); + } else { + return addColumnAfter(state, dispatch); + } } - } + }); }; /** @@ -1013,14 +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 deleteRow(state, this.editor.dispatch); + return this.editor.exec((beforeState, dispatch) => { + const state = this.setCellSelection(beforeState, { + row: index, + col: 0, + }); + return deleteRow(state, dispatch); + }); } else { - return deleteColumn(state, this.editor.dispatch); + return this.editor.exec((beforeState, dispatch) => { + const state = this.setCellSelection(beforeState, { + row: 0, + col: index, + }); + return deleteColumn(state, dispatch); + }); } }; @@ -1031,14 +1038,17 @@ export class TableHandlesProsemirrorPlugin< relativeStartCell: RelativeCellIndices; relativeEndCell: RelativeCellIndices; }) => { - const state = cellsToMerge - ? this.setCellSelection( - cellsToMerge.relativeStartCell, - cellsToMerge.relativeEndCell - ) - : this.editor.prosemirrorState; - - return mergeCells(state, this.editor.dispatch); + return this.editor.exec((beforeState, dispatch) => { + const state = cellsToMerge + ? this.setCellSelection( + beforeState, + cellsToMerge.relativeStartCell, + cellsToMerge.relativeEndCell + ) + : beforeState; + + return mergeCells(state, dispatch); + }); }; /** @@ -1046,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 splitCell(state, this.editor.dispatch); + return splitCell(state, dispatch); + }); }; /** @@ -1068,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; + 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 + ); + + // 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); - - // 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 }); + // 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, + }; + }); }; /** @@ -1143,32 +1157,32 @@ export class TableHandlesProsemirrorPlugin< | BlockFromConfigNoChildren | undefined ) => { - const isSelectingTableCells = isTableCellSelection( - this.editor.prosemirrorState.selection - ) - ? this.editor.prosemirrorState.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 = ( 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/blocks/FileBlockContent/helpers/render/AddFileButton.tsx b/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx index 0e13dfbb31..56b14c3739 100644 --- a/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx +++ b/packages/react/src/blocks/FileBlockContent/helpers/render/AddFileButton.tsx @@ -25,13 +25,10 @@ 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.transact((tr) => + tr.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..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.prosemirrorState.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.prosemirrorState.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 5fb2ab974a..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.prosemirrorState.selection); - }, [linkInSchema, selectedBlocks, editor.prosemirrorState.selection]); + return !isTableSelection; + }, [linkInSchema, selectedBlocks, isTableSelection]); 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) => { diff --git a/packages/react/src/schema/ReactInlineContentSpec.tsx b/packages/react/src/schema/ReactInlineContentSpec.tsx index bd8e0b555a..fe7dd36f71 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.transact((tr) => + 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..b2dfaa12bb 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,11 +91,9 @@ export function multiColumnDropCursor( .resolve(blockInfo.bnBlock.beforePos) .node(); - const columnList = nodeToBlock( + const columnList = nodeToBlock( parentBlock, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.styleSchema + editor.pmSchema ); // In a `columnList`, we expect that the average width of each column @@ -156,12 +152,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/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: [ 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,