From c57944497d07f412d189d4203415365431f6213d Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 12 Nov 2024 17:13:08 +0100 Subject: [PATCH 1/6] Fixed various multi-column bugs --- .../commands/replaceBlocks/replaceBlocks.ts | 4 +- .../DropCursor/MultiColumnDropCursorPlugin.ts | 49 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 536c8b5d89..2dd9c1a633 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -39,12 +39,12 @@ export function replaceBlocks< editor, blocksToRemove, (node, pos, tr, removedSize) => { - if (node.attrs.id === idOfFirstBlock) { + if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { const oldDocSize = tr.doc.nodeSize; tr.insert(pos, nodesToInsert); const newDocSize = tr.doc.nodeSize; - return removedSize + oldDocSize - newDocSize; + return removedSize + oldDocSize - newDocSize + 1; } return removedSize; diff --git a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts index c586a0f942..46fa1b22f2 100644 --- a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts +++ b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts @@ -130,17 +130,24 @@ export function multiColumnDropCursor( (b) => b.id === blockInfo.bnBlock.node.attrs.id ); - const newChildren = columnList.children.toSpliced( - position === "left" ? index : index + 1, - 0, - { + const newChildren = columnList.children + // If the dragged block is in one of the columns, remove it. + .map((column) => ({ + ...column, + children: column.children.filter( + (block) => block.id !== draggedBlock.id + ), + })) + // Remove empty columns (can happen when dragged block is removed). + .filter((column) => column.children.length > 0) + // Insert the dragged block in the correct position. + .toSpliced(position === "left" ? index : index + 1, 0, { type: "column", children: [draggedBlock], props: {}, content: undefined, id: UniqueID.options.generateID(), - } - ); + }); editor.removeBlocks([draggedBlock]); @@ -267,20 +274,22 @@ class DropCursorView { ) { const block = this.editorView.nodeDOM(this.cursorPos.pos); - const blockRect = (block as HTMLElement).getBoundingClientRect(); - const halfWidth = (this.width / 2) * scaleY; - const left = - this.cursorPos.position === "left" - ? blockRect.left - : blockRect.right; - rect = { - left: left - halfWidth, - right: left + halfWidth, - top: blockRect.top, - bottom: blockRect.bottom, - // left: blockRect.left, - // right: blockRect.right, - }; + if (block !== null) { + const blockRect = (block as HTMLElement).getBoundingClientRect(); + const halfWidth = (this.width / 2) * scaleY; + const left = + this.cursorPos.position === "left" + ? blockRect.left + : blockRect.right; + rect = { + left: left - halfWidth, + right: left + halfWidth, + top: blockRect.top, + bottom: blockRect.bottom, + // left: blockRect.left, + // right: blockRect.right, + }; + } } else { // regular logic const node = this.editorView.nodeDOM( From 059da4e0a4bd3aa5bc9bb052249e538d57f6d3c2 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 13 Nov 2024 19:57:02 +0100 Subject: [PATCH 2/6] Refactored `removeBlocksWithCallback` --- .../commands/removeBlocks/removeBlocks.ts | 84 +------------ .../commands/replaceBlocks/replaceBlocks.ts | 115 +++++++++++++++--- 2 files changed, 97 insertions(+), 102 deletions(-) diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts b/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts index f5cd55d2cc..d01606aecb 100644 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.ts @@ -1,6 +1,3 @@ -import { Node } from "prosemirror-model"; -import { Transaction } from "prosemirror-state"; - import { Block } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { @@ -9,84 +6,7 @@ import { InlineContentSchema, StyleSchema, } from "../../../../schema/index.js"; -import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; - -export function removeBlocksWithCallback< - BSchema extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema ->( - editor: BlockNoteEditor, - blocksToRemove: BlockIdentifier[], - // Should return new removedSize. - callback?: ( - node: Node, - pos: number, - tr: Transaction, - removedSize: number - ) => number -): Block[] { - const ttEditor = editor._tiptapEditor; - const tr = ttEditor.state.tr; - - const idsOfBlocksToRemove = new Set( - blocksToRemove.map((block) => - typeof block === "string" ? block : block.id - ) - ); - const removedBlocks: Block[] = []; - let removedSize = 0; - - ttEditor.state.doc.descendants((node, pos) => { - // Skips traversing nodes after all target blocks have been removed. - if (idsOfBlocksToRemove.size === 0) { - return false; - } - - // Keeps traversing nodes if block with target ID has not been found. - if ( - !node.type.isInGroup("bnBlock") || - !idsOfBlocksToRemove.has(node.attrs.id) - ) { - return true; - } - - // Saves the block that is being deleted. - removedBlocks.push( - nodeToBlock( - node, - editor.schema.blockSchema, - editor.schema.inlineContentSchema, - editor.schema.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; - removedSize += oldDocSize - newDocSize; - - return false; - }); - - // Throws an error if now all blocks could be found. - if (idsOfBlocksToRemove.size > 0) { - const notFoundIds = [...idsOfBlocksToRemove].join("\n"); - - throw Error( - "Blocks with the following IDs could not be found in the editor: " + - notFoundIds - ); - } - - editor.dispatch(tr); - - return removedBlocks; -} +import { removeAndInsertBlocks } from "../replaceBlocks/replaceBlocks.js"; export function removeBlocks< BSchema extends BlockSchema, @@ -96,5 +16,5 @@ export function removeBlocks< editor: BlockNoteEditor, blocksToRemove: BlockIdentifier[] ): Block[] { - return removeBlocksWithCallback(editor, blocksToRemove); + return removeAndInsertBlocks(editor, blocksToRemove, []).removedBlocks; } diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index 2dd9c1a633..d2a4bc94a6 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,5 +1,3 @@ -import { Node } from "prosemirror-model"; - import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { @@ -8,11 +6,11 @@ import { InlineContentSchema, StyleSchema, } from "../../../../schema/index.js"; +import { Node } from "prosemirror-model"; import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js"; -import { removeBlocksWithCallback } from "../removeBlocks/removeBlocks.js"; -export function replaceBlocks< +export function removeAndInsertBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -24,6 +22,11 @@ export function replaceBlocks< insertedBlocks: Block[]; removedBlocks: Block[]; } { + const ttEditor = editor._tiptapEditor; + let tr = ttEditor.state.tr; + + // Converts the `PartialBlock`s to ProseMirror nodes to insert them into the + // document. const nodesToInsert: Node[] = []; for (const block of blocksToInsert) { nodesToInsert.push( @@ -31,28 +34,85 @@ export function replaceBlocks< ); } + const idsOfBlocksToRemove = new Set( + blocksToRemove.map((block) => + typeof block === "string" ? block : block.id + ) + ); + const removedBlocks: Block[] = []; + const idOfFirstBlock = typeof blocksToRemove[0] === "string" ? blocksToRemove[0] : blocksToRemove[0].id; - const removedBlocks = removeBlocksWithCallback( - editor, - blocksToRemove, - (node, pos, tr, removedSize) => { - if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { - const oldDocSize = tr.doc.nodeSize; - tr.insert(pos, nodesToInsert); - const newDocSize = tr.doc.nodeSize; - - return removedSize + oldDocSize - newDocSize + 1; - } - - return removedSize; + let removedSize = 0; + + ttEditor.state.doc.descendants((node, pos) => { + // Skips traversing nodes after all target blocks have been removed. + if (idsOfBlocksToRemove.size === 0) { + return false; + } + + // Keeps traversing nodes if block with target ID has not been found. + if ( + !node.type.isInGroup("bnBlock") || + !idsOfBlocksToRemove.has(node.attrs.id) + ) { + return true; + } + + // Saves the block that is being deleted. + removedBlocks.push( + nodeToBlock( + node, + editor.schema.blockSchema, + editor.schema.inlineContentSchema, + editor.schema.styleSchema, + editor.blockCache + ) + ); + idsOfBlocksToRemove.delete(node.attrs.id); + + if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { + const oldDocSize = tr.doc.nodeSize; + tr = tr.insert(pos, nodesToInsert); + const newDocSize = tr.doc.nodeSize; + + removedSize += oldDocSize - newDocSize; } - ); - // Now that the `PartialBlock`s have been converted to nodes, we can - // re-convert them into full `Block`s. + 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($pos.depth - 1).type.name !== "doc" && + $pos.node().childCount === 1 + ) { + tr = tr.delete($pos.before(), $pos.after()); + } else { + tr = 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. + if (idsOfBlocksToRemove.size > 0) { + const notFoundIds = [...idsOfBlocksToRemove].join("\n"); + + throw Error( + "Blocks with the following IDs could not be found in the editor: " + + notFoundIds + ); + } + + editor.dispatch(tr); + + // Converts the nodes created from `blocksToInsert` into full `Block`s. const insertedBlocks: Block[] = []; for (const node of nodesToInsert) { insertedBlocks.push( @@ -68,3 +128,18 @@ export function replaceBlocks< return { insertedBlocks, removedBlocks }; } + +export function replaceBlocks< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + blocksToRemove: BlockIdentifier[], + blocksToInsert: PartialBlock[] +): { + insertedBlocks: Block[]; + removedBlocks: Block[]; +} { + return removeAndInsertBlocks(editor, blocksToRemove, blocksToInsert); +} From 4c2bcc7dc2a92774caca6b7d6b1d363b267c89f4 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 13 Nov 2024 20:11:22 +0100 Subject: [PATCH 3/6] Added `replaceBlocks` unit test --- .../__snapshots__/replaceBlocks.test.ts.snap | 175 ++++++++++++++++++ .../src/test/commands/replaceBlocks.test.ts | 40 ++++ 2 files changed, 215 insertions(+) create mode 100644 packages/xl-multi-column/src/test/commands/__snapshots__/replaceBlocks.test.ts.snap create mode 100644 packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts 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 new file mode 100644 index 0000000000..c0c756e5a4 --- /dev/null +++ b/packages/xl-multi-column/src/test/commands/__snapshots__/replaceBlocks.test.ts.snap @@ -0,0 +1,175 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test replaceBlocks > Replace paragraph with column list above column list empty column list 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted Column Paragraph", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "1", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Inserted Column Paragraph", + "type": "text", + }, + ], + "id": "4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "3", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "0", + "props": {}, + "type": "columnList", + }, + { + "children": [ + { + "children": [ + { + "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", + }, + ], + "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": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; diff --git a/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts b/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts new file mode 100644 index 0000000000..a5be953031 --- /dev/null +++ b/packages/xl-multi-column/src/test/commands/replaceBlocks.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { setupTestEnv } from "../setupTestEnv.js"; + +const getEditor = setupTestEnv(); + +describe("Test replaceBlocks", () => { + it("Replace paragraph with column list above column list empty column list", () => { + getEditor().replaceBlocks( + ["paragraph-0"], + [ + { + type: "columnList", + children: [ + { + type: "column", + children: [ + { + type: "paragraph", + content: "Inserted Column Paragraph", + }, + ], + }, + { + type: "column", + children: [ + { + type: "paragraph", + content: "Inserted Column Paragraph", + }, + ], + }, + ], + }, + ] + ); + + expect(getEditor().document).toMatchSnapshot(); + }); +}); From 2e3c16e20ea9e5c959af0ab792bf2985dca07941 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Thu, 14 Nov 2024 12:10:06 +0100 Subject: [PATCH 4/6] Added additional unit tests --- .../__snapshots__/removeBlocks.test.ts.snap | 440 ++++++++++++++++++ .../removeBlocks/removeBlocks.test.ts | 6 + .../commands/replaceBlocks/replaceBlocks.ts | 1 + .../__snapshots__/removeBlocks.test.ts.snap | 226 +++++++++ .../src/test/commands/removeBlocks.test.ts | 19 + 5 files changed, 692 insertions(+) create mode 100644 packages/xl-multi-column/src/test/commands/__snapshots__/removeBlocks.test.ts.snap create mode 100644 packages/xl-multi-column/src/test/commands/removeBlocks.test.ts diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap b/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap index 27ed2158a4..713566c788 100644 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +++ b/packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap @@ -1,5 +1,445 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Test removeBlocks > Remove all child blocks 1`] = ` +[ + { + "children": [], + "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 with children", + "type": "text", + }, + ], + "id": "paragraph-with-children", + "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": [ + { + "styles": {}, + "text": "Paragraph with props", + "type": "text", + }, + ], + "id": "paragraph-with-props", + "props": { + "backgroundColor": "default", + "textAlignment": "center", + "textColor": "red", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 3", + "type": "text", + }, + ], + "id": "paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Paragraph", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "paragraph-with-styled-content", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 4", + "type": "text", + }, + ], + "id": "paragraph-4", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Heading 1", + "type": "text", + }, + ], + "id": "heading-0", + "props": { + "backgroundColor": "default", + "level": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "heading", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 5", + "type": "text", + }, + ], + "id": "paragraph-5", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": undefined, + "id": "image-0", + "props": { + "backgroundColor": "default", + "caption": "", + "name": "", + "previewWidth": 512, + "showPreview": true, + "textAlignment": "left", + "url": "https://via.placeholder.com/150", + }, + "type": "image", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 6", + "type": "text", + }, + ], + "id": "paragraph-6", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + undefined, + ], + "rows": [ + { + "cells": [ + [ + { + "styles": {}, + "text": "Cell 1", + "type": "text", + }, + ], + [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + [ + { + "styles": {}, + "text": "Cell 3", + "type": "text", + }, + ], + ], + }, + { + "cells": [ + [ + { + "styles": {}, + "text": "Cell 4", + "type": "text", + }, + ], + [ + { + "styles": {}, + "text": "Cell 5", + "type": "text", + }, + ], + [ + { + "styles": {}, + "text": "Cell 6", + "type": "text", + }, + ], + ], + }, + { + "cells": [ + [ + { + "styles": {}, + "text": "Cell 7", + "type": "text", + }, + ], + [ + { + "styles": {}, + "text": "Cell 8", + "type": "text", + }, + ], + [ + { + "styles": {}, + "text": "Cell 9", + "type": "text", + }, + ], + ], + }, + ], + "type": "tableContent", + }, + "id": "table-0", + "props": { + "backgroundColor": "default", + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 7", + "type": "text", + }, + ], + "id": "paragraph-7", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [], + "id": "empty-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 8", + "type": "text", + }, + ], + "id": "paragraph-8", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Double Nested Paragraph 1", + "type": "text", + }, + ], + "id": "double-nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 1", + "type": "text", + }, + ], + "id": "nested-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": { + "bold": true, + }, + "text": "Heading", + "type": "text", + }, + { + "styles": {}, + "text": " with styled ", + "type": "text", + }, + { + "styles": { + "italic": true, + }, + "text": "content", + "type": "text", + }, + ], + "id": "heading-with-everything", + "props": { + "backgroundColor": "red", + "level": 2, + "textAlignment": "center", + "textColor": "red", + }, + "type": "heading", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; + exports[`Test removeBlocks > Remove multiple consecutive blocks 1`] = ` [ { diff --git a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts index 55625e11d7..fcdbd4cf07 100644 --- a/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts @@ -31,4 +31,10 @@ describe("Test removeBlocks", () => { expect(getEditor().document).toMatchSnapshot(); }); + + it("Remove all child blocks", () => { + removeBlocks(getEditor(), ["nested-paragraph-0"]); + + expect(getEditor().document).toMatchSnapshot(); + }); }); diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index d2a4bc94a6..bd6ad6687e 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -87,6 +87,7 @@ export function removeAndInsertBlocks< // `blockContainer`. const $pos = tr.doc.resolve(pos - removedSize); if ( + $pos.node().type.name === "blockGroup" && $pos.node($pos.depth - 1).type.name !== "doc" && $pos.node().childCount === 1 ) { 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 new file mode 100644 index 0000000000..ed727925db --- /dev/null +++ b/packages/xl-multi-column/src/test/commands/__snapshots__/removeBlocks.test.ts.snap @@ -0,0 +1,226 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test removeBlocks > Remove all blocks in 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": [ + { + "children": [ + { + "children": [], + "content": [], + "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": [], + "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": [ + { + "children": [ + { + "children": [], + "content": [], + "id": "1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "0", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "2", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "column-list-0", + "props": {}, + "type": "columnList", + }, + { + "children": [], + "content": [], + "id": "trailing-paragraph", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, +] +`; diff --git a/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts b/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts new file mode 100644 index 0000000000..2e5e4977e7 --- /dev/null +++ b/packages/xl-multi-column/src/test/commands/removeBlocks.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { setupTestEnv } from "../setupTestEnv.js"; + +const getEditor = setupTestEnv(); + +describe("Test removeBlocks", () => { + it("Remove all blocks in column", () => { + getEditor().removeBlocks(["column-paragraph-0", "column-paragraph-1"]); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("Remove all columns in columnList", () => { + getEditor().removeBlocks(["column-0", "column-1"]); + + expect(getEditor().document).toMatchSnapshot(); + }); +}); From 8065e09a643cfefd3514dddc2e1bd1d4299ab1a6 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Thu, 14 Nov 2024 12:36:57 +0100 Subject: [PATCH 5/6] Fixed error on live reload --- .../src/extensions/ColumnResize/ColumnResizeExtension.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/xl-multi-column/src/extensions/ColumnResize/ColumnResizeExtension.ts b/packages/xl-multi-column/src/extensions/ColumnResize/ColumnResizeExtension.ts index a9a3ba445a..aed630092e 100644 --- a/packages/xl-multi-column/src/extensions/ColumnResize/ColumnResizeExtension.ts +++ b/packages/xl-multi-column/src/extensions/ColumnResize/ColumnResizeExtension.ts @@ -296,9 +296,11 @@ class ColumnResizePluginView implements PluginView { this.editor.sideMenu.unfreezeMenu(); }; - // This is a required method for PluginView, so we get a type error if we - // don't implement it. - update: undefined; + destroy() { + this.view.dom.removeEventListener("mousedown", this.mouseDownHandler); + document.body.removeEventListener("mousemove", this.mouseMoveHandler); + document.body.removeEventListener("mouseup", this.mouseUpHandler); + } } const createColumnResizePlugin = (editor: BlockNoteEditor) => From 2ae450e35b21783856578c464cec4c0f33736c82 Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 14 Nov 2024 13:12:55 +0100 Subject: [PATCH 6/6] fix: drop handling edge case --- .../DropCursor/MultiColumnDropCursorPlugin.ts | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts index 46fa1b22f2..71c8b40743 100644 --- a/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts +++ b/packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts @@ -208,14 +208,25 @@ class DropCursorView { const handler = (e: Event) => { (this as any)[name](e); }; - editorView.dom.addEventListener(name, handler); + editorView.dom.addEventListener( + name, + handler, + // drop event captured in bubbling phase to make sure + // "cursorPos" is set to undefined before the "handleDrop" handler is called + // (otherwise an error could be thrown, see https://github.com/TypeCellOS/BlockNote/pull/1240) + name === "drop" ? true : undefined + ); return { name, handler }; }); } destroy() { this.handlers.forEach(({ name, handler }) => - this.editorView.dom.removeEventListener(name, handler) + this.editorView.dom.removeEventListener( + name, + handler, + name === "drop" ? true : undefined + ) ); } @@ -274,22 +285,24 @@ class DropCursorView { ) { const block = this.editorView.nodeDOM(this.cursorPos.pos); - if (block !== null) { - const blockRect = (block as HTMLElement).getBoundingClientRect(); - const halfWidth = (this.width / 2) * scaleY; - const left = - this.cursorPos.position === "left" - ? blockRect.left - : blockRect.right; - rect = { - left: left - halfWidth, - right: left + halfWidth, - top: blockRect.top, - bottom: blockRect.bottom, - // left: blockRect.left, - // right: blockRect.right, - }; + if (!block) { + throw new Error("nodeDOM returned null in updateOverlay"); } + + const blockRect = (block as HTMLElement).getBoundingClientRect(); + const halfWidth = (this.width / 2) * scaleY; + const left = + this.cursorPos.position === "left" + ? blockRect.left + : blockRect.right; + rect = { + left: left - halfWidth, + right: left + halfWidth, + top: blockRect.top, + bottom: blockRect.bottom, + // left: blockRect.left, + // right: blockRect.right, + }; } else { // regular logic const node = this.editorView.nodeDOM( @@ -443,7 +456,7 @@ class DropCursorView { target = point; } } - // console.log("target", target); + this.setCursor({ pos: target, position }); this.scheduleRemoval(5000); } @@ -454,7 +467,7 @@ class DropCursorView { } drop() { - this.scheduleRemoval(20); + this.setCursor(undefined); } dragleave(event: DragEvent) {