diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index 9530dfa967..892a8be8bb 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -1,15 +1,15 @@ -import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { + Block, BlockIdentifier, BlockSchema, InlineContentSchema, PartialBlock, StyleSchema, } from "../../schema"; -import { blockToNode } from "../nodeConversions/nodeConversions"; +import { blockToNode, nodeToBlock } from "../nodeConversions/nodeConversions"; import { getNodeById } from "../nodeUtil"; import { Transaction } from "prosemirror-state"; @@ -22,7 +22,7 @@ export function insertBlocks< referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before", editor: BlockNoteEditor -): void { +): Block[] { const ttEditor = editor._tiptapEditor; const id = @@ -35,39 +35,53 @@ export function insertBlocks< ); } - let insertionPos = -1; - const { node, posBeforeNode } = getNodeById(id, ttEditor.state.doc); if (placement === "before") { - insertionPos = posBeforeNode; + ttEditor.view.dispatch( + ttEditor.state.tr.insert(posBeforeNode, nodesToInsert) + ); } if (placement === "after") { - insertionPos = posBeforeNode + node.nodeSize; + ttEditor.view.dispatch( + ttEditor.state.tr.insert(posBeforeNode + node.nodeSize, nodesToInsert) + ); } if (placement === "nested") { // Case if block doesn't already have children. if (node.childCount < 2) { - insertionPos = posBeforeNode + node.firstChild!.nodeSize + 1; - const blockGroupNode = ttEditor.state.schema.nodes["blockGroup"].create( {}, nodesToInsert ); ttEditor.view.dispatch( - ttEditor.state.tr.insert(insertionPos, blockGroupNode) + ttEditor.state.tr.insert( + posBeforeNode + node.firstChild!.nodeSize + 1, + blockGroupNode + ) ); - - return; } + } - insertionPos = posBeforeNode + node.firstChild!.nodeSize + 2; + // 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.blockSchema, + editor.inlineContentSchema, + editor.styleSchema, + editor.blockCache + ) + ); } - ttEditor.view.dispatch(ttEditor.state.tr.insert(insertionPos, nodesToInsert)); + return insertedBlocks; } export function updateBlock< @@ -77,18 +91,36 @@ export function updateBlock< >( blockToUpdate: BlockIdentifier, update: PartialBlock, - editor: Editor -) { + editor: BlockNoteEditor +): Block { + const ttEditor = editor._tiptapEditor; + const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; - const { posBeforeNode } = getNodeById(id, editor.state.doc); + const { posBeforeNode } = getNodeById(id, ttEditor.state.doc); - editor.commands.BNUpdateBlock(posBeforeNode + 1, update); + ttEditor.commands.BNUpdateBlock(posBeforeNode + 1, update); + + const blockContainerNode = ttEditor.state.doc + .resolve(posBeforeNode + 1) + .node(); + + return nodeToBlock( + blockContainerNode, + editor.blockSchema, + editor.inlineContentSchema, + editor.styleSchema, + editor.blockCache + ); } -function removeBlocksWithCallback( +function removeBlocksWithCallback< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( blocksToRemove: BlockIdentifier[], - editor: Editor, + editor: BlockNoteEditor, // Should return new removedSize. callback?: ( node: Node, @@ -96,18 +128,19 @@ function removeBlocksWithCallback( tr: Transaction, removedSize: number ) => number -) { - const tr = editor.state.tr; +): Block[] { + const ttEditor = editor._tiptapEditor; + const tr = ttEditor.state.tr; const idsOfBlocksToRemove = new Set( blocksToRemove.map((block) => typeof block === "string" ? block : block.id ) ); - + const removedBlocks: Block[] = []; let removedSize = 0; - editor.state.doc.descendants((node, pos) => { + ttEditor.state.doc.descendants((node, pos) => { // Skips traversing nodes after all target blocks have been removed. if (idsOfBlocksToRemove.size === 0) { return false; @@ -121,10 +154,20 @@ function removeBlocksWithCallback( return true; } - removedSize = callback?.(node, pos, tr, removedSize) || removedSize; - + // Saves the block that is being deleted. + removedBlocks.push( + nodeToBlock( + node, + editor.blockSchema, + editor.inlineContentSchema, + editor.styleSchema, + editor.blockCache + ) + ); idsOfBlocksToRemove.delete(node.attrs.id); + // Removes the block and calculates the change in document size. + removedSize = callback?.(node, pos, tr, removedSize) || removedSize; const oldDocSize = tr.doc.nodeSize; tr.delete(pos - removedSize - 1, pos - removedSize + node.nodeSize + 1); const newDocSize = tr.doc.nodeSize; @@ -133,6 +176,7 @@ function removeBlocksWithCallback( return false; }); + // Throws an error if now all blocks could be found. if (idsOfBlocksToRemove.size > 0) { const notFoundIds = [...idsOfBlocksToRemove].join("\n"); @@ -142,14 +186,20 @@ function removeBlocksWithCallback( ); } - editor.view.dispatch(tr); + ttEditor.view.dispatch(tr); + + return removedBlocks; } -export function removeBlocks( +export function removeBlocks< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( blocksToRemove: BlockIdentifier[], - editor: Editor -) { - removeBlocksWithCallback(blocksToRemove, editor); + editor: BlockNoteEditor +): Block[] { + return removeBlocksWithCallback(blocksToRemove, editor); } export function replaceBlocks< @@ -160,24 +210,24 @@ export function replaceBlocks< blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock[], editor: BlockNoteEditor -) { +): { + insertedBlocks: Block[]; + removedBlocks: Block[]; +} { const ttEditor = editor._tiptapEditor; const nodesToInsert: Node[] = []; - for (const blockSpec of blocksToInsert) { - nodesToInsert.push( - blockToNode(blockSpec, ttEditor.schema, editor.styleSchema) - ); + for (const block of blocksToInsert) { + nodesToInsert.push(blockToNode(block, ttEditor.schema, editor.styleSchema)); } const idOfFirstBlock = typeof blocksToRemove[0] === "string" ? blocksToRemove[0] : blocksToRemove[0].id; - - removeBlocksWithCallback( + const removedBlocks = removeBlocksWithCallback( blocksToRemove, - ttEditor, + editor, (node, pos, tr, removedSize) => { if (node.attrs.id === idOfFirstBlock) { const oldDocSize = tr.doc.nodeSize; @@ -190,4 +240,21 @@ export function replaceBlocks< return removedSize; } ); + + // 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.blockSchema, + editor.inlineContentSchema, + editor.styleSchema, + editor.blockCache + ) + ); + } + + return { insertedBlocks, removedBlocks }; } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index fa78eb7c6b..38d2a85641 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -21,11 +21,11 @@ import { HTMLToBlocks } from "../api/parsers/html/parseHTML"; import { markdownToBlocks } from "../api/parsers/markdown/parseMarkdown"; import { DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, defaultBlockSchema, defaultBlockSpecs, + DefaultInlineContentSchema, defaultInlineContentSpecs, + DefaultStyleSchema, defaultStyleSpecs, } from "../blocks/defaultBlocks"; import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin"; @@ -45,17 +45,17 @@ import { BlockSchemaFromSpecs, BlockSchemaWithBlock, BlockSpecs, + getBlockSchemaFromSpecs, + getInlineContentSchemaFromSpecs, + getStyleSchemaFromSpecs, InlineContentSchema, InlineContentSchemaFromSpecs, InlineContentSpecs, PartialBlock, + Styles, StyleSchema, StyleSchemaFromSpecs, StyleSpecs, - Styles, - getBlockSchemaFromSpecs, - getInlineContentSchemaFromSpecs, - getStyleSchemaFromSpecs, } from "../schema"; import { mergeCSSClasses } from "../util/browser"; import { UnreachableCaseError } from "../util/typescript"; @@ -775,8 +775,8 @@ export class BlockNoteEditor< blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before" - ): void { - insertBlocks(blocksToInsert, referenceBlock, placement, this); + ) { + return insertBlocks(blocksToInsert, referenceBlock, placement, this); } /** @@ -790,7 +790,7 @@ export class BlockNoteEditor< blockToUpdate: BlockIdentifier, update: PartialBlock ) { - updateBlock(blockToUpdate, update, this._tiptapEditor); + return updateBlock(blockToUpdate, update, this); } /** @@ -798,7 +798,7 @@ export class BlockNoteEditor< * @param blocksToRemove An array of identifiers for existing blocks that should be removed. */ public removeBlocks(blocksToRemove: BlockIdentifier[]) { - removeBlocks(blocksToRemove, this._tiptapEditor); + return removeBlocks(blocksToRemove, this); } /** @@ -812,7 +812,7 @@ export class BlockNoteEditor< blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock[] ) { - replaceBlocks(blocksToRemove, blocksToInsert, this); + return replaceBlocks(blocksToRemove, blocksToInsert, this); } /**