import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockIdentifier, BlockSchema, InlineContentSchema, PartialBlock, StyleSchema, } from "../../schema"; import { blockToNode } from "../nodeConversions/nodeConversions"; import { getNodeById } from "../nodeUtil"; import { EditorState } from "prosemirror-state"; export function insertBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( blocksToInsert: PartialBlock[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before", editor: BlockNoteEditor, eraseHistory?: boolean ): void { const ttEditor = editor._tiptapEditor; const id = typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; const nodesToInsert: Node[] = []; for (const blockSpec of blocksToInsert) { nodesToInsert.push( blockToNode(blockSpec, ttEditor.schema, editor.styleSchema) ); } let insertionPos = -1; const { node, posBeforeNode } = getNodeById(id, ttEditor.state.doc); if (placement === "before") { insertionPos = posBeforeNode; } if (placement === "after") { insertionPos = posBeforeNode + node.nodeSize; } 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) ); return; } insertionPos = posBeforeNode + node.firstChild!.nodeSize + 2; } ttEditor.view.dispatch(ttEditor.state.tr.insert(insertionPos, nodesToInsert)); if (eraseHistory) { const newEditorState = EditorState.create({ doc: ttEditor.state.doc, plugins: ttEditor.state.plugins, schema: ttEditor.state.schema, }); ttEditor.view.updateState(newEditorState); } } export function updateBlock< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( blockToUpdate: BlockIdentifier, update: PartialBlock, editor: Editor ) { const id = typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id; const { posBeforeNode } = getNodeById(id, editor.state.doc); editor.commands.BNUpdateBlock(posBeforeNode + 1, update); } export function removeBlocks( blocksToRemove: BlockIdentifier[], editor: Editor, eraseHistory?: boolean ) { const idsOfBlocksToRemove = new Set( blocksToRemove.map((block) => typeof block === "string" ? block : block.id ) ); let removedSize = 0; editor.state.doc.descendants((node, pos) => { // Skips traversing nodes after all target blocks have been removed. if (idsOfBlocksToRemove.size === 0) { return false; } // Keeps traversing nodes if block with target ID has not been found. if ( node.type.name !== "blockContainer" || !idsOfBlocksToRemove.has(node.attrs.id) ) { return true; } idsOfBlocksToRemove.delete(node.attrs.id); const oldDocSize = editor.state.doc.nodeSize; editor.commands.BNDeleteBlock(pos - removedSize + 1); const newDocSize = editor.state.doc.nodeSize; removedSize += oldDocSize - newDocSize; return false; }); if (eraseHistory) { const newEditorState = EditorState.create({ doc: editor.state.doc, plugins: editor.state.plugins, schema: editor.state.schema, }); editor.view.updateState(newEditorState); } if (idsOfBlocksToRemove.size > 0) { const notFoundIds = [...idsOfBlocksToRemove].join("\n"); throw Error( "Blocks with the following IDs could not be found in the editor: " + notFoundIds ); } } export function replaceBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock[], editor: BlockNoteEditor, eraseHistory = false ) { insertBlocks( blocksToInsert, blocksToRemove[0], "before", editor, eraseHistory ); removeBlocks(blocksToRemove, editor._tiptapEditor, eraseHistory); }