Skip to content

feat: Return values for block manipulation methods #458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 106 additions & 39 deletions packages/core/src/api/blockManipulation/blockManipulation.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -22,7 +22,7 @@ export function insertBlocks<
referenceBlock: BlockIdentifier,
placement: "before" | "after" | "nested" = "before",
editor: BlockNoteEditor<BSchema, I, S>
): void {
): Block<BSchema, I, S>[] {
const ttEditor = editor._tiptapEditor;

const id =
Expand All @@ -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<BSchema, I, S>[] = [];
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<
Expand All @@ -77,37 +91,56 @@ export function updateBlock<
>(
blockToUpdate: BlockIdentifier,
update: PartialBlock<BSchema, I, S>,
editor: Editor
) {
editor: BlockNoteEditor<BSchema, I, S>
): Block<BSchema, I, S> {
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<BSchema, I, S>,
// Should return new removedSize.
callback?: (
node: Node,
pos: number,
tr: Transaction,
removedSize: number
) => number
) {
const tr = editor.state.tr;
): Block<BSchema, I, S>[] {
const ttEditor = editor._tiptapEditor;
const tr = ttEditor.state.tr;

const idsOfBlocksToRemove = new Set<string>(
blocksToRemove.map((block) =>
typeof block === "string" ? block : block.id
)
);

const removedBlocks: Block<BSchema, I, S>[] = [];
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;
Expand All @@ -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;
Expand All @@ -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");

Expand All @@ -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<BSchema, I, S>
): Block<BSchema, I, S>[] {
return removeBlocksWithCallback(blocksToRemove, editor);
}

export function replaceBlocks<
Expand All @@ -160,24 +210,24 @@ export function replaceBlocks<
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock<BSchema, I, S>[],
editor: BlockNoteEditor<BSchema, I, S>
) {
): {
insertedBlocks: Block<BSchema, I, S>[];
removedBlocks: Block<BSchema, I, S>[];
} {
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;
Expand All @@ -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<BSchema, I, S>[] = [];
for (const node of nodesToInsert) {
insertedBlocks.push(
nodeToBlock(
node,
editor.blockSchema,
editor.inlineContentSchema,
editor.styleSchema,
editor.blockCache
)
);
}

return { insertedBlocks, removedBlocks };
}
22 changes: 11 additions & 11 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -775,8 +775,8 @@ export class BlockNoteEditor<
blocksToInsert: PartialBlock<BSchema, ISchema, SSchema>[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" | "nested" = "before"
): void {
insertBlocks(blocksToInsert, referenceBlock, placement, this);
) {
return insertBlocks(blocksToInsert, referenceBlock, placement, this);
}

/**
Expand All @@ -790,15 +790,15 @@ export class BlockNoteEditor<
blockToUpdate: BlockIdentifier,
update: PartialBlock<BSchema, ISchema, SSchema>
) {
updateBlock(blockToUpdate, update, this._tiptapEditor);
return updateBlock(blockToUpdate, update, this);
}

/**
* Removes existing blocks from the editor. Throws an error if any of the blocks could not be found.
* @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);
}

/**
Expand All @@ -812,7 +812,7 @@ export class BlockNoteEditor<
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock<BSchema, ISchema, SSchema>[]
) {
replaceBlocks(blocksToRemove, blocksToInsert, this);
return replaceBlocks(blocksToRemove, blocksToInsert, this);
}

/**
Expand Down