Skip to content
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

feature: add support for blocknote transactions #1147

Open
wants to merge 2 commits into
base: refactor/clean-blockcontainer
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Fragment, Node as PMNode, Slice } from "prosemirror-model";
import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import {
EditorState,
NodeSelection,
TextSelection,
Transaction,
} from "prosemirror-state";

import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js";
import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
Expand All @@ -19,6 +24,161 @@ import {
} from "../../../nodeConversions/nodeConversions.js";
import { getNodeById } from "../../../nodeUtil.js";

export const updateBlockCommandWithTr = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
posInBlock: number,
tr: Transaction,
block: PartialBlock<BSchema, I, S>,
editor: BlockNoteEditor<BSchema, I, S>
) => {
const blockInfo = getBlockInfoFromPos(tr.doc, posInBlock);
if (blockInfo === undefined) {
return false;
}

const { blockContainer, blockContent, blockGroup } = blockInfo;

// Adds blockGroup node with child blocks if necessary.
if (block.children !== undefined) {
const childNodes = [];

// Creates ProseMirror nodes for each child block, including their descendants.
for (const child of block.children) {
childNodes.push(
blockToNode(child, editor.pmSchema, editor.schema.styleSchema)
);
}

// Checks if a blockGroup node already exists.
if (blockGroup) {
// Replaces all child nodes in the existing blockGroup with the ones created earlier.
tr.replace(
blockGroup.beforePos + 1,
blockGroup.afterPos - 1,
new Slice(Fragment.from(childNodes), 0, 0)
);
} else {
// Inserts a new blockGroup containing the child nodes created earlier.
tr.insert(
blockContent.afterPos,
editor.pmSchema.nodes["blockGroup"].create({}, childNodes)
);
}
}

const oldType = blockContent.node.type.name;
const newType = block.type || oldType;

// The code below determines the new content of the block.
// or "keep" to keep as-is
let content: PMNode[] | "keep" = "keep";

// Has there been any custom content provided?
if (block.content) {
if (typeof block.content === "string") {
// Adds a single text node with no marks to the content.
content = inlineContentToNodes(
[block.content],
editor.pmSchema,
editor.schema.styleSchema
);
} else if (Array.isArray(block.content)) {
// Adds a text node with the provided styles converted into marks to the content,
// for each InlineContent object.
content = inlineContentToNodes(
block.content,
editor.pmSchema,
editor.schema.styleSchema
);
} else if (block.content.type === "tableContent") {
content = tableContentToNodes(
block.content,
editor.pmSchema,
editor.schema.styleSchema
);
} else {
throw new UnreachableCaseError(block.content.type);
}
} else {
// no custom content has been provided, use existing content IF possible

// Since some block types contain inline content and others don't,
// we either need to call setNodeMarkup to just update type &
// attributes, or replaceWith to replace the whole blockContent.
const oldContentType = editor.pmSchema.nodes[oldType].spec.content;
const newContentType = editor.pmSchema.nodes[newType].spec.content;

if (oldContentType === "") {
// keep old content, because it's empty anyway and should be compatible with
// any newContentType
} else if (newContentType !== oldContentType) {
// the content type changed, replace the previous content
content = [];
} else {
// keep old content, because the content type is the same and should be compatible
}
}

// Now, changes the blockContent node type and adds the provided props
// as attributes. Also preserves all existing attributes that are
// compatible with the new type.
//
// Use either setNodeMarkup or replaceWith depending on whether the
// content is being replaced or not.
if (content === "keep") {
// use setNodeMarkup to only update the type and attributes
tr.setNodeMarkup(
blockContent.beforePos,
block.type === undefined ? undefined : editor.pmSchema.nodes[block.type],
{
...blockContent.node.attrs,
...block.props,
}
);
} else {
// use replaceWith to replace the content and the block itself
// also reset the selection since replacing the block content
// sets it to the next block.
tr.replaceWith(
blockContent.beforePos,
blockContent.afterPos,
editor.pmSchema.nodes[newType].create(
{
...blockContent.node.attrs,
...block.props,
},
content
)
)
// If the node doesn't contain editable content, we want to
// select the whole node. But if it does have editable content,
// we want to set the selection to the start of it.
.setSelection(
editor.pmSchema.nodes[newType].spec.content === ""
? new NodeSelection(tr.doc.resolve(blockContent.beforePos))
: editor.pmSchema.nodes[newType].spec.content === "inline*"
? new TextSelection(tr.doc.resolve(blockContent.beforePos))
: // Need to offset the position as we have to get through the
// `tableRow` and `tableCell` nodes to get to the
// `tableParagraph` node we want to set the selection in.
new TextSelection(tr.doc.resolve(blockContent.beforePos + 4))
);
}

// Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing
// attributes.
tr.setNodeMarkup(blockContainer.beforePos, undefined, {
...blockContainer.node.attrs,
...block.props,
});
return true;
};

// TODO: this entire command should be removed as we're deprecating tiptap style transactions and commands
// Calls should be replaced with editor.updateBlock() or updateBlock below
export const updateBlockCommand =
<
BSchema extends BlockSchema,
Expand All @@ -29,160 +189,11 @@ export const updateBlockCommand =
posInBlock: number,
block: PartialBlock<BSchema, I, S>
) =>
({
state,
dispatch,
}: {
(args: {
state: EditorState;
dispatch: ((args?: any) => any) | undefined;
}) => {
const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
if (blockInfo === undefined) {
return false;
}

const { blockContainer, blockContent, blockGroup } = blockInfo;

if (dispatch) {
// Adds blockGroup node with child blocks if necessary.
if (block.children !== undefined) {
const childNodes = [];

// Creates ProseMirror nodes for each child block, including their descendants.
for (const child of block.children) {
childNodes.push(
blockToNode(child, state.schema, editor.schema.styleSchema)
);
}

// Checks if a blockGroup node already exists.
if (blockGroup) {
// Replaces all child nodes in the existing blockGroup with the ones created earlier.
state.tr.replace(
blockGroup.beforePos + 1,
blockGroup.afterPos - 1,
new Slice(Fragment.from(childNodes), 0, 0)
);
} else {
// Inserts a new blockGroup containing the child nodes created earlier.
state.tr.insert(
blockContent.afterPos,
state.schema.nodes["blockGroup"].create({}, childNodes)
);
}
}

const oldType = blockContent.node.type.name;
const newType = block.type || oldType;

// The code below determines the new content of the block.
// or "keep" to keep as-is
let content: PMNode[] | "keep" = "keep";

// Has there been any custom content provided?
if (block.content) {
if (typeof block.content === "string") {
// Adds a single text node with no marks to the content.
content = inlineContentToNodes(
[block.content],
state.schema,
editor.schema.styleSchema
);
} else if (Array.isArray(block.content)) {
// Adds a text node with the provided styles converted into marks to the content,
// for each InlineContent object.
content = inlineContentToNodes(
block.content,
state.schema,
editor.schema.styleSchema
);
} else if (block.content.type === "tableContent") {
content = tableContentToNodes(
block.content,
state.schema,
editor.schema.styleSchema
);
} else {
throw new UnreachableCaseError(block.content.type);
}
} else {
// no custom content has been provided, use existing content IF possible

// Since some block types contain inline content and others don't,
// we either need to call setNodeMarkup to just update type &
// attributes, or replaceWith to replace the whole blockContent.
const oldContentType = state.schema.nodes[oldType].spec.content;
const newContentType = state.schema.nodes[newType].spec.content;

if (oldContentType === "") {
// keep old content, because it's empty anyway and should be compatible with
// any newContentType
} else if (newContentType !== oldContentType) {
// the content type changed, replace the previous content
content = [];
} else {
// keep old content, because the content type is the same and should be compatible
}
}

// Now, changes the blockContent node type and adds the provided props
// as attributes. Also preserves all existing attributes that are
// compatible with the new type.
//
// Use either setNodeMarkup or replaceWith depending on whether the
// content is being replaced or not.
if (content === "keep") {
// use setNodeMarkup to only update the type and attributes
state.tr.setNodeMarkup(
blockContent.beforePos,
block.type === undefined ? undefined : state.schema.nodes[block.type],
{
...blockContent.node.attrs,
...block.props,
}
);
} else {
// use replaceWith to replace the content and the block itself
// also reset the selection since replacing the block content
// sets it to the next block.
state.tr
.replaceWith(
blockContent.beforePos,
blockContent.afterPos,
state.schema.nodes[newType].create(
{
...blockContent.node.attrs,
...block.props,
},
content
)
)
// If the node doesn't contain editable content, we want to
// select the whole node. But if it does have editable content,
// we want to set the selection to the start of it.
.setSelection(
state.schema.nodes[newType].spec.content === ""
? new NodeSelection(state.tr.doc.resolve(blockContent.beforePos))
: state.schema.nodes[newType].spec.content === "inline*"
? new TextSelection(state.tr.doc.resolve(blockContent.beforePos))
: // Need to offset the position as we have to get through the
// `tableRow` and `tableCell` nodes to get to the
// `tableParagraph` node we want to set the selection in.
new TextSelection(
state.tr.doc.resolve(blockContent.beforePos + 4)
)
);
}

// Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing
// attributes.
state.tr.setNodeMarkup(blockContainer.beforePos, undefined, {
...blockContainer.node.attrs,
...block.props,
});
}

return true;
return updateBlockCommandWithTr(posInBlock, args.state.tr, block, editor);
};

export function updateBlock<
Expand All @@ -192,22 +203,21 @@ export function updateBlock<
>(
editor: BlockNoteEditor<BSchema, I, S>,
blockToUpdate: BlockIdentifier,
update: PartialBlock<BSchema, I, S>
update: PartialBlock<BSchema, I, S>,
existingTr?: Transaction
): Block<BSchema, I, S> {
const ttEditor = editor._tiptapEditor;
const tr = existingTr ?? ttEditor.state.tr;

const id =
typeof blockToUpdate === "string" ? blockToUpdate : blockToUpdate.id;
const { posBeforeNode } = getNodeById(id, ttEditor.state.doc);
const { posBeforeNode } = getNodeById(id, tr.doc);

ttEditor.commands.command(({ state, dispatch }) => {
updateBlockCommand(editor, posBeforeNode + 1, update)({ state, dispatch });
return true;
});
updateBlockCommandWithTr(posBeforeNode + 1, tr, update, editor);

const blockContainerNode = tr.doc.resolve(posBeforeNode + 1).node();

const blockContainerNode = ttEditor.state.doc
.resolve(posBeforeNode + 1)
.node();
editor.dispatch(tr);

return nodeToBlock(
blockContainerNode,
Expand Down
Loading
Loading