Skip to content
Open
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
13 changes: 10 additions & 3 deletions docs/content/docs/reference/editor/manipulating-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ editor.forEachBlock((block) => {
insertBlocks(
blocksToInsert: PartialBlock[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" = "before"
): void
placement: "before" | "after" | "first-child" | "last-child" = "before"
): Block[]
```

Inserts new blocks relative to an existing block.
Inserts new blocks relative to an existing block. `"before"` and `"after"` make the new blocks siblings of the reference block; `"first-child"` and `"last-child"` nest them inside it. Returns the inserted blocks.

```typescript
// Insert a paragraph before an existing block
Expand All @@ -164,6 +164,13 @@ editor.insertBlocks(
"existing-block-id",
"after",
);

// Insert a paragraph as the last child of an existing block
editor.insertBlocks(
[{ type: "paragraph", content: "Nested paragraph" }],
"existing-block-id",
"last-child",
);
```

### Updating Blocks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
InlineContentSchema,
StyleSchema,
} from "../../../../schema/index.js";
import {
type BlockPlacement,
getInsertionPos,
getBlockInfoAt,
} from "../../../getBlockInfoFromPos.js";
import { blockToNode } from "../../../nodeConversions/blockToNode.js";
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
import { getNodeById } from "../../../nodeUtil.js";
Expand All @@ -21,7 +26,7 @@ export function insertBlocks<
tr: Transaction,
blocksToInsert: PartialBlock<BSchema, I, S>[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" = "before",
placement: BlockPlacement = "before",
): Block<BSchema, I, S>[] {
const id =
typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id;
Expand All @@ -37,14 +42,47 @@ export function insertBlocks<
throw new Error(`Block with ID ${id} not found`);
}

let pos = posInfo.posBeforeNode;
if (placement === "after") {
pos += posInfo.node.nodeSize;
if (nodesToInsert.length === 0) {
return [];
}

tr.step(
new ReplaceStep(pos, pos, new Slice(Fragment.from(nodesToInsert), 0, 0)),
const target = getInsertionPos(
tr.doc,
getBlockInfoAt(tr.doc, posInfo.posBeforeNode),
placement,
nodesToInsert[0].type,
);
if (!target) {
throw new Error(
`Cannot insert blocks at "${placement}" of block "${id}": no valid position for them`,
);
}

// `getInsertionPos` can only answer for the first node's type: the fragment
// doesn't exist yet when it runs. The whole fragment still has to fit, so it
// is checked here, where the nodes are known, rather than left to `tr.step`
// to reject with a ProseMirror-level message.
if (
target.wrapIn &&
!target.wrapIn.validContent(Fragment.from(nodesToInsert))
) {
throw new Error(
`Cannot insert blocks at "${placement}" of block "${id}": a "${target.wrapIn.name}" doesn't accept them`,
);
}

const fragment = target.wrapIn
? Fragment.from(target.wrapIn.create(null, nodesToInsert))
: Fragment.from(nodesToInsert);

const $target = tr.doc.resolve(target.pos);
if (!$target.parent.canReplace($target.index(), $target.index(), fragment)) {
throw new Error(
`Cannot insert blocks at "${placement}" of block "${id}": a "${$target.parent.type.name}" doesn't accept them`,
);
}

tr.step(new ReplaceStep(target.pos, target.pos, new Slice(fragment, 0, 0)));

// Now that the `PartialBlock`s have been converted to nodes, we can
// re-convert them into full `Block`s.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// @vitest-environment node
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
} from "vite-plus/test";

import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";

let editor: BlockNoteEditor<any, any, any>;

beforeAll(() => {
editor = BlockNoteEditor.create() as any;
});

afterAll(() => {
editor._tiptapEditor.destroy();
editor = undefined as any;
});

beforeEach(() => {
editor.replaceBlocks(editor.document, [
{ id: "p-0", type: "paragraph", content: "Paragraph 0" },
]);
});

describe('insertBlocks "first-child" / "last-child"', () => {
it("nests under a childless block, creating the blockGroup", () => {
expect(editor.getBlock("p-0")!.children).toHaveLength(0);

editor.insertBlocks(
[{ id: "first", type: "paragraph" }],
"p-0",
"first-child",
);
editor.insertBlocks(
[{ id: "last", type: "paragraph" }],
"p-0",
"last-child",
);

expect(editor.getBlock("p-0")!.children.map((child) => child.id)).toEqual([
"first",
"last",
]);
});

it("prepends and appends around existing children", () => {
editor.replaceBlocks(editor.document, [
{
id: "p-0",
type: "paragraph",
content: "Paragraph 0",
children: [{ id: "existing", type: "paragraph", content: "Existing" }],
},
]);

editor.insertBlocks(
[{ id: "first", type: "paragraph" }],
"p-0",
"first-child",
);
editor.insertBlocks(
[{ id: "last", type: "paragraph" }],
"p-0",
"last-child",
);

expect(editor.getBlock("p-0")!.children.map((child) => child.id)).toEqual([
"first",
"existing",
"last",
]);
});

it("still inserts siblings with the default and explicit placements", () => {
editor.insertBlocks([{ id: "after", type: "paragraph" }], "p-0");
editor.insertBlocks([{ id: "before", type: "paragraph" }], "p-0", "before");
editor.insertBlocks([{ id: "sibling", type: "paragraph" }], "p-0", "after");

expect(editor.document.map((block) => block.id)).toEqual([
"after",
"before",
"p-0",
"sibling",
]);
});

it("still inserts a batch that fits in full", () => {
editor.insertBlocks(
[
{ id: "one", type: "paragraph" },
{ id: "two", type: "paragraph" },
],
"p-0",
"last-child",
);

expect(editor.getBlock("p-0")!.children.map((child) => child.id)).toEqual([
"one",
"two",
]);
});

it("throws when the reference block does not exist", () => {
expect(() =>
editor.insertBlocks([{ type: "paragraph" }], "missing-id", "last-child"),
).toThrow(/Block with ID missing-id not found/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { describe, expect, it } from "vite-plus/test";

import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js";
import { setupTestEnv } from "../../setupTestEnv.js";
import { getParentBlockInfo, mergeBlocksCommand } from "./mergeBlocks.js";
import { getParentBlockInfo } from "../../../getBlockInfoFromPos.js";
import { mergeBlocksCommand } from "./mergeBlocks.js";

const getEditor = setupTestEnv();

Expand All @@ -14,7 +15,7 @@ function mergeBlocks(posBetweenBlocks: number) {

function getPosBeforeSelectedBlock() {
return getEditor().transact(
(tr) => getBlockInfoFromSelection(tr).bnBlock.beforePos,
(tr) => getBlockInfoFromSelection(tr).block.beforePos,
);
}

Expand Down
Loading
Loading