diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 4ba8107636..04a2425a33 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,5 +1,5 @@ -import type { Node } from "prosemirror-model"; -import type { Transaction } from "prosemirror-state"; +import { type Node } from "prosemirror-model"; +import { type Transaction } from "prosemirror-state"; import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockIdentifier, @@ -10,6 +10,7 @@ import type { import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; import { getPmSchema } from "../../../pmUtil.js"; +import { fixColumnList } from "./util/fixColumnList.js"; export function removeAndInsertBlocks< BSchema extends BlockSchema, @@ -36,6 +37,7 @@ export function removeAndInsertBlocks< ), ); const removedBlocks: Block[] = []; + const columnListPositions = new Set(); const idOfFirstBlock = typeof blocksToRemove[0] === "string" @@ -70,26 +72,35 @@ export function removeAndInsertBlocks< } 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 === "column") { + columnListPositions.add($pos.before(-1)); + } else if ($pos.node().type.name === "columnList") { + columnListPositions.add($pos.before()); + } + if ( $pos.node().type.name === "blockGroup" && $pos.node($pos.depth - 1).type.name !== "doc" && $pos.node().childCount === 1 ) { + // Checks if the block is the only child of a parent `blockGroup` node. + // In this case, we need to delete the parent `blockGroup` node instead + // of just the `blockContainer`. tr.delete($pos.before(), $pos.after()); } else { 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. + // Throws an error if not all blocks could be found. if (idsOfBlocksToRemove.size > 0) { const notFoundIds = [...idsOfBlocksToRemove].join("\n"); @@ -99,6 +110,8 @@ export function removeAndInsertBlocks< ); } + columnListPositions.forEach((pos) => fixColumnList(tr, pos)); + // Converts the nodes created from `blocksToInsert` into full `Block`s. const insertedBlocks = nodesToInsert.map((node) => nodeToBlock(node, pmSchema), diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts new file mode 100644 index 0000000000..3097851f47 --- /dev/null +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts @@ -0,0 +1,173 @@ +import { Slice, type Node } from "prosemirror-model"; +import { type Transaction } from "prosemirror-state"; +import { ReplaceAroundStep } from "prosemirror-transform"; + +/** + * Checks if a `column` node is empty, i.e. if it has only a single empty + * paragraph. + * @param column The column to check. + * @returns Whether the column is empty. + */ +export function isEmptyColumn(column: Node) { + if (!column || column.type.name !== "column") { + throw new Error("Invalid columnPos: does not point to column node."); + } + + const blockContainer = column.firstChild; + if (!blockContainer) { + throw new Error("Invalid column: does not have child node."); + } + + const blockContent = blockContainer.firstChild; + if (!blockContent) { + throw new Error("Invalid blockContainer: does not have child node."); + } + + return ( + column.childCount === 1 && + blockContainer.childCount === 1 && + blockContent.type.name === "paragraph" && + blockContent.content.content.length === 0 + ); +} + +/** + * Removes all empty `column` nodes in a `columnList`. A `column` node is empty + * if it has only a single empty block. If, however, removing the `column`s + * leaves the `columnList` that has fewer than two, ProseMirror will re-add + * empty columns. + * @param tr The `Transaction` to add the changes to. + * @param columnListPos The position just before the `columnList` node. + */ +export function removeEmptyColumns(tr: Transaction, columnListPos: number) { + const $columnListPos = tr.doc.resolve(columnListPos); + const columnList = $columnListPos.nodeAfter; + if (!columnList || columnList.type.name !== "columnList") { + throw new Error( + "Invalid columnListPos: does not point to columnList node.", + ); + } + + for ( + let columnIndex = columnList.childCount - 1; + columnIndex >= 0; + columnIndex-- + ) { + const columnPos = tr.doc + .resolve($columnListPos.pos + 1) + .posAtIndex(columnIndex); + const $columnPos = tr.doc.resolve(columnPos); + const column = $columnPos.nodeAfter; + if (!column || column.type.name !== "column") { + throw new Error("Invalid columnPos: does not point to column node."); + } + + if (isEmptyColumn(column)) { + tr.delete(columnPos, columnPos + column.nodeSize); + } + } +} + +/** + * Fixes potential issues in a `columnList` node after a + * `blockContainer`/`column` node is (re)moved from it: + * + * - Removes all empty `column` nodes. A `column` node is empty if it has only + * a single empty block. + * - If all but one `column` nodes are empty, replaces the `columnList` with + * the content of the non-empty `column`. + * - If all `column` nodes are empty, removes the `columnList` entirely. + * @param tr The `Transaction` to add the changes to. + * @param columnListPos + * @returns The position just before the `columnList` node. + */ +export function fixColumnList(tr: Transaction, columnListPos: number) { + removeEmptyColumns(tr, columnListPos); + + const $columnListPos = tr.doc.resolve(columnListPos); + const columnList = $columnListPos.nodeAfter; + if (!columnList || columnList.type.name !== "columnList") { + throw new Error( + "Invalid columnListPos: does not point to columnList node.", + ); + } + + if (columnList.childCount > 2) { + // Do nothing if the `columnList` has more than two non-empty `column`s. In + // the case that the `columnList` has exactly two columns, we may need to + // still remove it, as it's possible that one or both columns are empty. + // This is because after `removeEmptyColumns` is called, if the + // `columnList` has fewer than two `column`s, ProseMirror will re-add empty + // `column`s until there are two total, in order to fit the schema. + return; + } + + if (columnList.childCount < 2) { + // Throw an error if the `columnList` has fewer than two columns. After + // `removeEmptyColumns` is called, if the `columnList` has fewer than two + // `column`s, ProseMirror will re-add empty `column`s until there are two + // total, in order to fit the schema. So if there are fewer than two here, + // either the schema, or ProseMirror's internals, must have changed. + throw new Error("Invalid columnList: contains fewer than two children."); + } + + const firstColumnBeforePos = columnListPos + 1; + const $firstColumnBeforePos = tr.doc.resolve(firstColumnBeforePos); + const firstColumn = $firstColumnBeforePos.nodeAfter; + + const lastColumnAfterPos = columnListPos + columnList.nodeSize - 1; + const $lastColumnAfterPos = tr.doc.resolve(lastColumnAfterPos); + const lastColumn = $lastColumnAfterPos.nodeBefore; + + if (!firstColumn || !lastColumn) { + throw new Error("Invalid columnList: does not contain children."); + } + + const firstColumnEmpty = isEmptyColumn(firstColumn); + const lastColumnEmpty = isEmptyColumn(lastColumn); + + if (firstColumnEmpty && lastColumnEmpty) { + // Removes `columnList` + tr.delete(columnListPos, columnListPos + columnList.nodeSize); + + return; + } + + if (firstColumnEmpty) { + tr.step( + new ReplaceAroundStep( + // Replaces `columnList`. + columnListPos, + columnListPos + columnList.nodeSize, + // Replaces with content of last `column`. + lastColumnAfterPos - lastColumn.nodeSize + 1, + lastColumnAfterPos - 1, + // Doesn't append anything. + Slice.empty, + 0, + false, + ), + ); + + return; + } + + if (lastColumnEmpty) { + tr.step( + new ReplaceAroundStep( + // Replaces `columnList`. + columnListPos, + columnListPos + columnList.nodeSize, + // Replaces with content of first `column`. + firstColumnBeforePos + 1, + firstColumnBeforePos + firstColumn.nodeSize - 1, + // Doesn't append anything. + Slice.empty, + 0, + false, + ), + ); + + return; + } +} diff --git a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index 94230143db..da63452923 100644 --- a/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -1,10 +1,8 @@ import { Extension } from "@tiptap/core"; import { TextSelection } from "prosemirror-state"; -import { ReplaceAroundStep } from "prosemirror-transform"; import { getBottomNestedBlockInfo, - getParentBlockInfo, getPrevBlockInfo, mergeBlocksCommand, } from "../../api/blockManipulation/commands/mergeBlocks/mergeBlocks.js"; @@ -13,6 +11,7 @@ import { splitBlockCommand } from "../../api/blockManipulation/commands/splitBlo import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import { fixColumnList } from "../../api/blockManipulation/commands/replaceBlocks/util/fixColumnList.js"; export const KeyboardShortcutsExtension = Extension.create<{ editor: BlockNoteEditor; @@ -98,7 +97,7 @@ export const KeyboardShortcutsExtension = Extension.create<{ return false; }), () => - commands.command(({ state, dispatch }) => { + commands.command(({ state, tr, dispatch }) => { // when at the start of a first block in a column const blockInfo = getBlockInfoFromSelection(state); if (!blockInfo.isBlockContainer) { @@ -106,151 +105,54 @@ export const KeyboardShortcutsExtension = Extension.create<{ } const selectionAtBlockStart = - state.selection.from === blockInfo.blockContent.beforePos + 1; - + tr.selection.from === blockInfo.blockContent.beforePos + 1; if (!selectionAtBlockStart) { return false; } - const prevBlockInfo = getPrevBlockInfo( - state.doc, - blockInfo.bnBlock.beforePos, - ); + const $pos = tr.doc.resolve(blockInfo.bnBlock.beforePos); - if (prevBlockInfo) { + const prevBlock = $pos.nodeBefore; + if (prevBlock) { // should be no previous block return false; } - const parentBlockInfo = getParentBlockInfo( - state.doc, - blockInfo.bnBlock.beforePos, - ); - - if (parentBlockInfo?.blockNoteType !== "column") { + const parentBlock = $pos.node(); + if (parentBlock.type.name !== "column") { return false; } - const column = parentBlockInfo; - - const columnList = getParentBlockInfo( - state.doc, - column.bnBlock.beforePos, - ); - if (columnList?.blockNoteType !== "columnList") { - throw new Error("parent of column is not a column list"); - } - - const shouldRemoveColumn = - column.childContainer!.node.childCount === 1; - - const shouldRemoveColumnList = - shouldRemoveColumn && - columnList.childContainer!.node.childCount === 2; - - const isFirstColumn = - columnList.childContainer!.node.firstChild === - column.bnBlock.node; + const $blockPos = tr.doc.resolve(blockInfo.bnBlock.beforePos); + const $columnPos = tr.doc.resolve($blockPos.before()); + const columnListPos = $columnPos.before(); if (dispatch) { - const blockToMove = state.doc.slice( + const fragment = tr.doc.slice( blockInfo.bnBlock.beforePos, blockInfo.bnBlock.afterPos, - false, - ); + ).content; - /* - There are 3 different cases: - a) remove entire column list (if no columns would be remaining) - b) remove just a column (if no blocks inside a column would be remaining) - c) keep columns (if there are blocks remaining inside a column) - - Each of these 3 cases has 2 sub-cases, depending on whether the backspace happens at the start of the first (most-left) column, - or at the start of a non-first column. - */ - if (shouldRemoveColumnList) { - if (isFirstColumn) { - state.tr.step( - new ReplaceAroundStep( - // replace entire column list - columnList.bnBlock.beforePos, - columnList.bnBlock.afterPos, - // select content of remaining column: - column.bnBlock.afterPos + 1, - columnList.bnBlock.afterPos - 2, - blockToMove, - blockToMove.size, // append existing content to blockToMove - false, - ), - ); - const pos = state.tr.doc.resolve(column.bnBlock.beforePos); - state.tr.setSelection(TextSelection.between(pos, pos)); - } else { - // replaces the column list with the blockToMove slice, prepended with the content of the remaining column - state.tr.step( - new ReplaceAroundStep( - // replace entire column list - columnList.bnBlock.beforePos, - columnList.bnBlock.afterPos, - // select content of existing column: - columnList.bnBlock.beforePos + 2, - column.bnBlock.beforePos - 1, - blockToMove, - 0, // prepend existing content to blockToMove - false, - ), - ); - const pos = state.tr.doc.resolve( - state.tr.mapping.map(column.bnBlock.beforePos - 1), - ); - state.tr.setSelection(TextSelection.between(pos, pos)); - } - } else if (shouldRemoveColumn) { - if (isFirstColumn) { - // delete column - state.tr.delete( - column.bnBlock.beforePos, - column.bnBlock.afterPos, - ); - - // move before columnlist - state.tr.insert( - columnList.bnBlock.beforePos, - blockToMove.content, - ); + tr.delete( + blockInfo.bnBlock.beforePos, + blockInfo.bnBlock.afterPos, + ); - const pos = state.tr.doc.resolve( - columnList.bnBlock.beforePos, - ); - state.tr.setSelection(TextSelection.between(pos, pos)); - } else { - // just delete the closing and opening tags to merge the columns - state.tr.delete( - column.bnBlock.beforePos - 1, - column.bnBlock.beforePos + 1, - ); - } + if ($columnPos.index() === 0) { + // Fix `columnList` and insert the block before it. + fixColumnList(tr, columnListPos); + tr.insert(columnListPos, fragment); + tr.setSelection( + TextSelection.near(tr.doc.resolve(columnListPos)), + ); } else { - // delete block - state.tr.delete( - blockInfo.bnBlock.beforePos, - blockInfo.bnBlock.afterPos, + // Insert the block at the end of the first column and fix + // `columnList`. + tr.insert($columnPos.pos - 1, fragment); + tr.setSelection( + TextSelection.near(tr.doc.resolve($columnPos.pos - 1)), ); - if (isFirstColumn) { - // move before columnlist - state.tr.insert( - columnList.bnBlock.beforePos - 1, - blockToMove.content, - ); - } else { - // append block to previous column - state.tr.insert( - column.bnBlock.beforePos - 1, - blockToMove.content, - ); - } - const pos = state.tr.doc.resolve(column.bnBlock.beforePos - 1); - state.tr.setSelection(TextSelection.between(pos, pos)); + fixColumnList(tr, columnListPos); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 11fe0e5460..4b6994d5d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ export * from "./api/blockManipulation/commands/insertBlocks/insertBlocks.js"; export * from "./api/blockManipulation/commands/replaceBlocks/replaceBlocks.js"; export * from "./api/blockManipulation/commands/updateBlock/updateBlock.js"; +export * from "./api/blockManipulation/commands/replaceBlocks/util/fixColumnList.js"; export * from "./api/exporters/html/externalHTMLExporter.js"; export * from "./api/exporters/html/internalHTMLSerializer.js"; export * from "./api/getBlockInfoFromPos.js"; diff --git a/packages/xl-multi-column/src/test/commands/__snapshots__/removeBlocks.test.ts.snap b/packages/xl-multi-column/src/test/commands/__snapshots__/removeBlocks.test.ts.snap index f6902f31bc..e67cdf0be4 100644 --- a/packages/xl-multi-column/src/test/commands/__snapshots__/removeBlocks.test.ts.snap +++ b/packages/xl-multi-column/src/test/commands/__snapshots__/removeBlocks.test.ts.snap @@ -54,78 +54,668 @@ exports[`Test removeBlocks > Remove all blocks in column 1`] = ` }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 2", + "type": "text", + }, + ], + "id": "column-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", + }, + ], + "id": "column-paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove all blocks in column and block after 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 2", + "type": "text", + }, + ], + "id": "column-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", + }, + ], + "id": "column-paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove all blocks in columns 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove all blocks in columns and block after 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove all blocks in second column 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 0", + "type": "text", + }, + ], + "id": "column-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 1", + "type": "text", + }, + ], + "id": "column-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove all columns in columnList 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove all columns in columnList and block after 1`] = ` +[ { "children": [ { - "children": [ + "children": [], + "content": [ { - "children": [], - "content": [], - "id": "0", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", }, ], - "content": undefined, - "id": "column-0", + "id": "nested-paragraph-0", "props": { - "width": 1, + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", }, - "type": "column", + "type": "paragraph", }, + ], + "content": [ { - "children": [ - { - "children": [], - "content": [ - { - "styles": {}, - "text": "Column Paragraph 2", - "type": "text", - }, - ], - "id": "column-paragraph-2", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove column and and block in column after 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ { - "children": [], - "content": [ - { - "styles": {}, - "text": "Column Paragraph 3", - "type": "text", - }, - ], - "id": "column-paragraph-3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", }, ], - "content": undefined, - "id": "column-1", + "id": "nested-paragraph-0", "props": { - "width": 1, + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", }, - "type": "column", + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", }, ], - "content": undefined, - "id": "column-list-0", - "props": {}, - "type": "columnList", + "id": "column-paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", }, { "children": [], @@ -158,7 +748,7 @@ exports[`Test removeBlocks > Remove all blocks in column 1`] = ` ] `; -exports[`Test removeBlocks > Remove all columns in columnList 1`] = ` +exports[`Test removeBlocks > Remove column in columnList 1`] = ` [ { "children": [ @@ -212,66 +802,152 @@ exports[`Test removeBlocks > Remove all columns in columnList 1`] = ` }, "type": "paragraph", }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 2", + "type": "text", + }, + ], + "id": "column-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", + }, + ], + "id": "column-paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test removeBlocks > Remove column in columnList and block after 1`] = ` +[ { "children": [ { - "children": [ + "children": [], + "content": [ { - "children": [], - "content": [], - "id": "1", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", }, ], - "content": undefined, - "id": "0", + "id": "nested-paragraph-0", "props": { - "width": 1, + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", }, - "type": "column", + "type": "paragraph", }, + ], + "content": [ { - "children": [ - { - "children": [], - "content": [], - "id": "3", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "textColor": "default", - }, - "type": "paragraph", - }, - ], - "content": undefined, - "id": "2", - "props": { - "width": 1, - }, - "type": "column", + "styles": {}, + "text": "Paragraph 0", + "type": "text", }, ], - "content": undefined, - "id": "column-list-0", - "props": {}, - "type": "columnList", + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", }, { "children": [], "content": [ { "styles": {}, - "text": "Paragraph 2", + "text": "Paragraph 1", "type": "text", }, ], - "id": "paragraph-2", + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 2", + "type": "text", + }, + ], + "id": "column-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", + }, + ], + "id": "column-paragraph-3", "props": { "backgroundColor": "default", "textAlignment": "left", diff --git a/packages/xl-multi-column/src/test/commands/__snapshots__/replaceBlocks.test.ts.snap b/packages/xl-multi-column/src/test/commands/__snapshots__/replaceBlocks.test.ts.snap index 94782491fe..b19ccf1eec 100644 --- a/packages/xl-multi-column/src/test/commands/__snapshots__/replaceBlocks.test.ts.snap +++ b/packages/xl-multi-column/src/test/commands/__snapshots__/replaceBlocks.test.ts.snap @@ -1,5 +1,271 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Test replaceBlocks > Replace all blocks in column with single block 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "New Paragraph", + "type": "text", + }, + ], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-0", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 2", + "type": "text", + }, + ], + "id": "column-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", + }, + ], + "id": "column-paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-1", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "column-list-0", + "props": {}, + "type": "columnList", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + +exports[`Test replaceBlocks > Replace all blocks in columns with single block 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "New Paragraph", + "type": "text", + }, + ], + "id": "0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + exports[`Test replaceBlocks > Replace paragraph with column list above column list 1`] = ` [ { diff --git a/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts b/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts index 2e5e4977e7..6fb58e9306 100644 --- a/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts +++ b/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts @@ -11,9 +11,72 @@ describe("Test removeBlocks", () => { expect(getEditor().document).toMatchSnapshot(); }); + it("Remove all blocks in second column", () => { + getEditor().removeBlocks(["column-paragraph-2", "column-paragraph-3"]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove all blocks in columns", () => { + getEditor().removeBlocks([ + "column-paragraph-0", + "column-paragraph-1", + "column-paragraph-2", + "column-paragraph-3", + ]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove column in columnList", () => { + getEditor().removeBlocks(["column-0"]); + + expect(getEditor().document).toMatchSnapshot(); + }); + it("Remove all columns in columnList", () => { getEditor().removeBlocks(["column-0", "column-1"]); expect(getEditor().document).toMatchSnapshot(); }); + + it("Remove all blocks in column and block after", () => { + getEditor().removeBlocks([ + "column-paragraph-0", + "column-paragraph-1", + "paragraph-2", + ]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove all blocks in columns and block after", () => { + getEditor().removeBlocks([ + "column-paragraph-0", + "column-paragraph-1", + "column-paragraph-2", + "column-paragraph-3", + "paragraph-2", + ]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove column in columnList and block after", () => { + getEditor().removeBlocks(["column-0", "paragraph-2"]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove all columns in columnList and block after", () => { + getEditor().removeBlocks(["column-0", "column-1", "paragraph-2"]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove column and and block in column after", () => { + getEditor().removeBlocks(["column-0", "column-paragraph-2"]); + + expect(getEditor().document).toMatchSnapshot(); + }); }); diff --git a/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts b/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts index 6839873dd5..5640804fc7 100644 --- a/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts +++ b/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts @@ -37,4 +37,37 @@ describe("Test replaceBlocks", () => { expect(getEditor().document).toMatchSnapshot(); }); + + it("Replace all blocks in column with single block", () => { + getEditor().replaceBlocks( + ["column-paragraph-0", "column-paragraph-1"], + [ + { + type: "paragraph", + content: "New Paragraph", + }, + ], + ); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Replace all blocks in columns with single block", () => { + getEditor().replaceBlocks( + [ + "column-paragraph-0", + "column-paragraph-1", + "column-paragraph-2", + "column-paragraph-3", + ], + [ + { + type: "paragraph", + content: "New Paragraph", + }, + ], + ); + + expect(getEditor().document).toMatchSnapshot(); + }); }); diff --git a/packages/xl-multi-column/src/test/commands/util/__snapshots__/fixColumnLists.test.ts.snap b/packages/xl-multi-column/src/test/commands/util/__snapshots__/fixColumnLists.test.ts.snap new file mode 100644 index 0000000000..87b5f2e588 --- /dev/null +++ b/packages/xl-multi-column/src/test/commands/util/__snapshots__/fixColumnLists.test.ts.snap @@ -0,0 +1,408 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test fixColumnList > First of two columns empty 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Paragraph 1", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; + +exports[`Test fixColumnList > Last of two columns empty 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Paragraph 1", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; + +exports[`Test fixColumnList > Two empty columns 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; + +exports[`Test removeEmptyColumns > First of two columns empty 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Paragraph 1", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + ], + "type": "columnList", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; + +exports[`Test removeEmptyColumns > Last of two columns empty 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Paragraph 1", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + ], + "type": "columnList", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; + +exports[`Test removeEmptyColumns > Start and end columns empty 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Paragraph 1", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "content": [ + { + "text": "Paragraph 2", + "type": "text", + }, + ], + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + ], + "type": "columnList", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; + +exports[`Test removeEmptyColumns > Two empty columns 1`] = ` +{ + "content": [ + { + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + { + "attrs": { + "id": null, + "width": 1, + }, + "content": [ + { + "attrs": { + "id": null, + }, + "content": [ + { + "attrs": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "type": "blockContainer", + }, + ], + "type": "column", + }, + ], + "type": "columnList", + }, + ], + "type": "blockGroup", + }, + ], + "type": "doc", +} +`; diff --git a/packages/xl-multi-column/src/test/commands/util/fixColumnLists.test.ts b/packages/xl-multi-column/src/test/commands/util/fixColumnLists.test.ts new file mode 100644 index 0000000000..5212030057 --- /dev/null +++ b/packages/xl-multi-column/src/test/commands/util/fixColumnLists.test.ts @@ -0,0 +1,283 @@ +import { describe, expect, it } from "vitest"; + +import { setupTestEnv } from "../../setupTestEnv.js"; +import { + fixColumnList, + isEmptyColumn, + removeEmptyColumns, +} from "@blocknote/core"; + +const getEditor = setupTestEnv(); + +describe("Test isEmptyColumn", () => { + it("Empty blocks", () => { + const schema = getEditor()._tiptapEditor.schema; + + const column = schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]); + + expect(isEmptyColumn(column)).toBeTruthy(); + }); + + it("Multiple blocks", () => { + const schema = getEditor()._tiptapEditor.schema; + + const column = schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined), + ]), + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]); + + expect(isEmptyColumn(column)).toBeFalsy(); + }); + + it("Block with children", () => { + const schema = getEditor()._tiptapEditor.schema; + + const column = schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined), + schema.nodes["blockGroup"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + ]), + ]); + + expect(isEmptyColumn(column)).toBeFalsy(); + }); + + it("Block with text", () => { + const schema = getEditor()._tiptapEditor.schema; + + const column = schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 1"), + ]), + ]), + ]); + + expect(isEmptyColumn(column)).toBeFalsy(); + }); + + it("Non-text block", () => { + const schema = getEditor()._tiptapEditor.schema; + + const column = schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["image"].create(), + ]), + ]); + + expect(isEmptyColumn(column)).toBeFalsy(); + }); +}); + +describe("Test removeEmptyColumns", () => { + it("Start and end columns empty", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 1"), + ]), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 2"), + ]), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + removeEmptyColumns(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); + + it("First of two columns empty", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 1"), + ]), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + removeEmptyColumns(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); + + it("Last of two columns empty", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 1"), + ]), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + removeEmptyColumns(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); + + it("Two empty columns", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + removeEmptyColumns(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); +}); + +describe("Test fixColumnList", () => { + it("First of two columns empty", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 1"), + ]), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + fixColumnList(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); + + it("Last of two columns empty", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(undefined, [ + schema.text("Paragraph 1"), + ]), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + fixColumnList(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); + + it("Two empty columns", () => { + const editor = getEditor(); + const schema = editor._tiptapEditor.schema; + + const columnList = schema.nodes["columnList"].create(undefined, [ + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + schema.nodes["column"].create(undefined, [ + schema.nodes["blockContainer"].create(undefined, [ + schema.nodes["paragraph"].create(), + ]), + ]), + ]); + + const tr = editor.prosemirrorState.tr; + + tr.replaceRangeWith(1, tr.doc.firstChild!.content.size, columnList); + fixColumnList(tr, 1); + + expect(tr.doc).toMatchSnapshot(); + }); +});