Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,27 @@ describe("Test mergeBlocks", () => {
expect(result).toBeUndefined();
});

// We expect a no-op for each of the remaining tests as merging should only
// happen for blocks which both have inline content. We also expect
// `mergeBlocks` to return false as TipTap commands should do that instead of
// throwing an error, when the command cannot be executed.
it("First block is empty", () => {
getEditor().setTextCursorPosition("paragraph-8");

const originalDocument = getEditor().document;
const ret = mergeBlocks(getPosBeforeSelectedBlock());

expect(getEditor().document).toEqual(originalDocument);
expect(ret).toBeFalsy();
expect(getEditor().document).toEqual(
originalDocument
.filter((block) => block.id !== "paragraph-8")
.map((block) =>
block.id === "empty-paragraph"
? {
...block,
content: originalDocument.find(
(source) => source.id === "paragraph-8",
)!.content,
}
: block,
),
);
expect(ret).toBe(true);
});

it("Inline content & no content", () => {
Expand Down Expand Up @@ -132,7 +141,7 @@ describe("Test mergeBlocks", () => {
const ret = mergeBlocks(getPosBeforeSelectedBlock());

expect(getEditor().document).toEqual(originalDocument);
expect(ret).toBeFalsy();
expect(ret).toBe(false);
});

it("Table content & inline content", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Node } from "prosemirror-model";
import { EditorState } from "prosemirror-state";
import { Fragment, Node } from "prosemirror-model";
import { Command } from "@tiptap/core";
import { EditorState, Selection, Transaction } from "prosemirror-state";

import {
BlockInfo,
Expand Down Expand Up @@ -107,15 +108,14 @@ const canMerge = (prevBlockInfo: BlockInfo, nextBlockInfo: BlockInfo) => {
return (
prevBlockInfo.isBlockContainer &&
prevBlockInfo.blockContent.node.type.spec.content === "inline*" &&
prevBlockInfo.blockContent.node.childCount > 0 &&
nextBlockInfo.isBlockContainer &&
nextBlockInfo.blockContent.node.type.spec.content === "inline*"
);
};

const mergeBlocks = (
state: EditorState,
dispatch: ((args?: any) => any) | undefined,
dispatch: ((tr: Transaction) => void) | undefined,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
prevBlockInfo: BlockInfo,
nextBlockInfo: BlockInfo,
) => {
Expand Down Expand Up @@ -165,14 +165,65 @@ const mergeBlocks = (
return true;
};

export const mergeBlocksCommand =
(posBetweenBlocks: number) =>
({
type ContentBlockInfo = Extract<BlockInfo, { isBlockContainer: true }>;

/** Merge a first child into its parent, promoting descendants into its place. */
function mergeIntoParent(
state: EditorState,
dispatch: ((tr: Transaction) => void) | undefined,
parent: ContentBlockInfo,
child: ContentBlockInfo,
): boolean {
if (!parent.childContainer || !canMerge(parent, child)) {
return false;
}
const content = child.blockContent.node.content;
if (
content.size > 0 &&
(parent.blockContent.node.type.spec.content !== "inline*" ||
!parent.blockContent.node.type.validContent(
parent.blockContent.node.content.append(content),
))
) {
return false;
}

if (dispatch) {
const tr = state.tr;
if (parent.childContainer.node.childCount === 1 && !child.childContainer) {
tr.delete(
parent.childContainer.beforePos,
parent.childContainer.afterPos,
);
} else {
tr.replaceWith(
child.bnBlock.beforePos,
child.bnBlock.afterPos,
child.childContainer?.node.content ?? Fragment.empty,
);
}
const cursorPos = parent.blockContent.afterPos - 1;
if (content.size > 0) {
tr.insert(cursorPos, content);
}
tr.setSelection(Selection.near(tr.doc.resolve(cursorPos), -1));
dispatch(tr.scrollIntoView());
}
return true;
}

/**
* Merges into the previous sibling's deepest descendant, or into the parent
* when the position is before its first child. Both blocks must support inline
* content; incompatible blocks return false for the caller to handle.
*/
export function mergeBlocksCommand(posBetweenBlocks: number): Command {
return ({
state,
dispatch,
}: {
state: EditorState;
dispatch: ((args?: any) => any) | undefined;
dispatch: ((tr: Transaction) => void) | undefined;
}) => {
const $pos = state.doc.resolve(posBetweenBlocks);
const nextBlockInfo = getBlockInfoFromResolvedPos($pos);
Expand All @@ -183,6 +234,18 @@ export const mergeBlocksCommand =
);

if (!prevBlockInfo) {
if (
nextBlockInfo.isBlockContainer &&
nextBlockInfo.blockContent.node.type.spec.content === "inline*"
) {
const parent = getParentBlockInfo(
state.doc,
nextBlockInfo.bnBlock.beforePos,
);
if (parent?.isBlockContainer) {
return mergeIntoParent(state, dispatch, parent, nextBlockInfo);
}
}
return false;
}

Expand All @@ -197,3 +260,4 @@ export const mergeBlocksCommand =

return mergeBlocks(state, dispatch, bottomNestedBlockInfo, nextBlockInfo);
};
}
Loading
Loading