UNPKG

@blocknote/core

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

147 lines (129 loc) 4.27 kB
import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier, BlockSchema, InlineContentSchema, StyleSchema, } from "../../../../schema/index.js"; import { Node } from "prosemirror-model"; import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; export function removeAndInsertBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( editor: BlockNoteEditor<BSchema, I, S>, blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock<BSchema, I, S>[] ): { insertedBlocks: Block<BSchema, I, S>[]; removedBlocks: Block<BSchema, I, S>[]; } { const ttEditor = editor._tiptapEditor; let tr = ttEditor.state.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 idsOfBlocksToRemove = new Set<string>( blocksToRemove.map((block) => typeof block === "string" ? block : block.id ) ); const removedBlocks: Block<BSchema, I, S>[] = []; const idOfFirstBlock = typeof blocksToRemove[0] === "string" ? blocksToRemove[0] : blocksToRemove[0].id; let removedSize = 0; ttEditor.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.isInGroup("bnBlock") || !idsOfBlocksToRemove.has(node.attrs.id) ) { return true; } // Saves the block that is being deleted. removedBlocks.push( nodeToBlock( node, editor.schema.blockSchema, editor.schema.inlineContentSchema, editor.schema.styleSchema, editor.blockCache ) ); idsOfBlocksToRemove.delete(node.attrs.id); if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { const oldDocSize = tr.doc.nodeSize; tr = tr.insert(pos, nodesToInsert); const newDocSize = tr.doc.nodeSize; removedSize += oldDocSize - newDocSize; } const oldDocSize = tr.doc.nodeSize; // Checks if the block is the only child of its parent. In this case, we // need to delete the parent `blockGroup` node instead of just the // `blockContainer`. const $pos = tr.doc.resolve(pos - removedSize); if ( $pos.node().type.name === "blockGroup" && $pos.node($pos.depth - 1).type.name !== "doc" && $pos.node().childCount === 1 ) { tr = tr.delete($pos.before(), $pos.after()); } else { tr = tr.delete(pos - removedSize, pos - removedSize + node.nodeSize); } const newDocSize = tr.doc.nodeSize; removedSize += oldDocSize - newDocSize; return false; }); // Throws an error if now all blocks could be found. if (idsOfBlocksToRemove.size > 0) { const notFoundIds = [...idsOfBlocksToRemove].join("\n"); throw Error( "Blocks with the following IDs could not be found in the editor: " + notFoundIds ); } editor.dispatch(tr); // Converts the nodes created from `blocksToInsert` into full `Block`s. const insertedBlocks: Block<BSchema, I, S>[] = []; for (const node of nodesToInsert) { insertedBlocks.push( nodeToBlock( node, editor.schema.blockSchema, editor.schema.inlineContentSchema, editor.schema.styleSchema, editor.blockCache ) ); } return { insertedBlocks, removedBlocks }; } export function replaceBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema >( editor: BlockNoteEditor<BSchema, I, S>, blocksToRemove: BlockIdentifier[], blocksToInsert: PartialBlock<BSchema, I, S>[] ): { insertedBlocks: Block<BSchema, I, S>[]; removedBlocks: Block<BSchema, I, S>[]; } { return removeAndInsertBlocks(editor, blocksToRemove, blocksToInsert); }