From be48f2d66a06744d79ff17a9705a3049c96b0151 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 24 Dec 2024 14:02:08 +0100 Subject: [PATCH 1/9] WIP --- examples/01-basic/04-all-blocks/App.tsx | 392 ++++++++++-------- .../src/extensions/SideMenu/SideMenuPlugin.ts | 176 ++++---- 2 files changed, 308 insertions(+), 260 deletions(-) diff --git a/examples/01-basic/04-all-blocks/App.tsx b/examples/01-basic/04-all-blocks/App.tsx index 73681ebcf..92018ee86 100644 --- a/examples/01-basic/04-all-blocks/App.tsx +++ b/examples/01-basic/04-all-blocks/App.tsx @@ -1,207 +1,255 @@ import { + BlockNoteEditorOptions, BlockNoteSchema, - combineByGroup, - filterSuggestionItems, + getBlockInfoFromSelection, locales, } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; import { - SuggestionMenuController, - getDefaultReactSlashMenuItems, - useCreateBlockNote, -} from "@blocknote/react"; -import { - getMultiColumnSlashMenuItems, multiColumnDropCursor, locales as multiColumnLocales, withMultiColumn, } from "@blocknote/xl-multi-column"; -import { useMemo } from "react"; -export default function App() { - // Creates a new editor instance. - const editor = useCreateBlockNote({ - schema: withMultiColumn(BlockNoteSchema.create()), - dropCursor: multiColumnDropCursor, - dictionary: { - ...locales.en, - multi_column: multiColumnLocales.en, - }, - initialContent: [ - { - type: "paragraph", - content: "Welcome to this demo!", - }, - { - type: "paragraph", - }, - { - type: "paragraph", - content: [ - { - type: "text", - text: "Blocks:", - styles: { bold: true }, - }, - ], - }, - { - type: "paragraph", - content: "Paragraph", - }, - { - type: "columnList", - children: [ - { - type: "column", - props: { - width: 0.8, - }, - children: [ - { - type: "paragraph", - content: "Hello to the left!", - }, - ], - }, - { - type: "column", - props: { - width: 1.2, - }, - children: [ - { - type: "paragraph", - content: "Hello to the right!", - }, - ], +import { Slice, Node } from "@tiptap/pm/model"; +import { useState } from "react"; + +const schema = withMultiColumn(BlockNoteSchema.create()); +const options = { + schema: withMultiColumn(BlockNoteSchema.create()), + dropCursor: multiColumnDropCursor, + dictionary: { + ...locales.en, + multi_column: multiColumnLocales.en, + }, + initialContent: [ + { + type: "paragraph", + content: "Welcome to this demo!", + }, + { + type: "paragraph", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Blocks:", + styles: { bold: true }, + }, + ], + }, + { + type: "paragraph", + content: "Paragraph", + }, + { + type: "columnList", + children: [ + { + type: "column", + props: { + width: 0.8, }, - ], - }, - { - type: "heading", - content: "Heading", - }, - { - type: "bulletListItem", - content: "Bullet List Item", - }, - { - type: "numberedListItem", - content: "Numbered List Item", - }, - { - type: "checkListItem", - content: "Check List Item", - }, - { - type: "codeBlock", - props: { language: "javascript" }, - content: "console.log('Hello, world!');", - }, - { - type: "table", - content: { - type: "tableContent", - rows: [ + children: [ { - cells: ["Table Cell", "Table Cell", "Table Cell"], - }, - { - cells: ["Table Cell", "Table Cell", "Table Cell"], + type: "paragraph", + content: "Hello to the left!", }, + ], + }, + { + type: "column", + props: { + width: 1.2, + }, + children: [ { - cells: ["Table Cell", "Table Cell", "Table Cell"], + type: "paragraph", + content: "Hello to the right!", }, ], }, - }, - { - type: "file", - }, - { - type: "image", - props: { - url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", - caption: - "From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", - }, - }, - { - type: "video", - props: { - url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", - caption: - "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", - }, - }, - { - type: "audio", - props: { - url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", - caption: - "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", - }, - }, - { - type: "paragraph", - }, - { - type: "paragraph", - content: [ - { - type: "text", - text: "Inline Content:", - styles: { bold: true }, - }, - ], - }, - { - type: "paragraph", - content: [ + ], + }, + { + type: "heading", + content: "Heading", + }, + { + type: "bulletListItem", + content: "Bullet List Item", + }, + { + type: "numberedListItem", + content: "Numbered List Item", + }, + { + type: "checkListItem", + content: "Check List Item", + }, + { + type: "codeBlock", + props: { language: "javascript" }, + content: "console.log('Hello, world!');", + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ { - type: "text", - text: "Styled Text", - styles: { - bold: true, - italic: true, - textColor: "red", - backgroundColor: "blue", - }, + cells: ["Table Cell", "Table Cell", "Table Cell"], }, { - type: "text", - text: " ", - styles: {}, + cells: ["Table Cell", "Table Cell", "Table Cell"], }, { - type: "link", - content: "Link", - href: "https://www.blocknotejs.org", + cells: ["Table Cell", "Table Cell", "Table Cell"], }, ], }, - { - type: "paragraph", + }, + { + type: "file", + }, + { + type: "image", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", }, - ], - }); + }, + { + type: "video", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + }, + }, + { + type: "audio", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + }, + }, + { + type: "paragraph", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline Content:", + styles: { bold: true }, + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Styled Text", + styles: { + bold: true, + italic: true, + textColor: "red", + backgroundColor: "blue", + }, + }, + { + type: "text", + text: " ", + styles: {}, + }, + { + type: "link", + content: "Link", + href: "https://www.blocknotejs.org", + }, + ], + }, + { + type: "paragraph", + }, + ], +} satisfies Partial< + BlockNoteEditorOptions< + typeof schema.blockSchema, + typeof schema.inlineContentSchema, + typeof schema.styleSchema + > +>; - const slashMenuItems = useMemo(() => { - return combineByGroup( - getDefaultReactSlashMenuItems(editor), - getMultiColumnSlashMenuItems(editor) - ); - }, [editor]); +export default function App() { + // Creates a new editor instance. + const editor1 = useCreateBlockNote(options); + const editor2 = useCreateBlockNote(options); + + const [node, setNode] = useState(undefined); // Renders the editor instance using a React component. return ( - - filterSuggestionItems(slashMenuItems, query)} +
{ + // editor2.prosemirrorView!.dragging = editor1.prosemirrorView!.dragging; + // // editor2.prosemirrorView.dragging = true; + // }} + // onDrag={() => { + // // console.log("editor1", editor1.prosemirrorView!.dragging); + // console.log("editor2", editor2.prosemirrorView!.dragging); + // }} + > + + + + { + editor2.prosemirrorView!.props.handleDrop?.( + editor2.prosemirrorView!, + e, + editor2.prosemirrorView!.dragging!.slice, + false + ); + }} /> - +
); } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index eb50f4248..8a10ff9a0 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -249,23 +249,23 @@ export class SideMenuView< onDrop = (event: DragEvent) => { this.editor._tiptapEditor.commands.blur(); - if ( - (event as any).synthetic || - !event.dataTransfer?.types.includes("blocknote/html") - ) { - return; - } - - const pos = this.pmView.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (!pos || pos.inside === -1) { - const evt = this.createSyntheticEvent(event); - // console.log("dispatch fake drop"); - this.pmView.dom.dispatchEvent(evt); - } + // if ( + // (event as any).synthetic || + // !event.dataTransfer?.types.includes("blocknote/html") + // ) { + // return; + // } + // + // const pos = this.pmView.posAtCoords({ + // left: event.clientX, + // top: event.clientY, + // }); + // + // if (!pos || pos.inside === -1) { + // const evt = this.createSyntheticEvent(event); + // // console.log("dispatch fake drop"); + // this.pmView.dom.dispatchEvent(evt); + // } }; /** @@ -274,22 +274,22 @@ export class SideMenuView< * when dragging / dropping to the side of the editor */ onDragOver = (event: DragEvent) => { - if ( - (event as any).synthetic || - !event.dataTransfer?.types.includes("blocknote/html") - ) { - return; - } - const pos = this.pmView.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (!pos || (pos.inside === -1 && this.pmView.dom.firstChild)) { - const evt = this.createSyntheticEvent(event); - // console.log("dispatch fake dragover"); - this.pmView.dom.dispatchEvent(evt); - } + // if ( + // (event as any).synthetic || + // !event.dataTransfer?.types.includes("blocknote/html") + // ) { + // return; + // } + // const pos = this.pmView.posAtCoords({ + // left: event.clientX, + // top: event.clientY, + // }); + // + // if (!pos || (pos.inside === -1 && this.pmView.dom.firstChild)) { + // const evt = this.createSyntheticEvent(event); + // // console.log("dispatch fake dragover"); + // this.pmView.dom.dispatchEvent(evt); + // } }; onKeyDown = (_event: KeyboardEvent) => { @@ -344,61 +344,61 @@ export class SideMenuView< this.updateStateFromMousePos(); }; - private createSyntheticEvent(event: DragEvent) { - const evt = new Event(event.type, event) as any; - const editorBoundingBox = ( - this.pmView.dom.firstChild as HTMLElement - ).getBoundingClientRect(); - evt.clientX = event.clientX; - evt.clientY = event.clientY; - if ( - event.clientX < editorBoundingBox.left && - event.clientX > - editorBoundingBox.left - - editorBoundingBox.width * - PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP - ) { - // when we're slightly left of the editor, we can drop to the side of the block - evt.clientX = - editorBoundingBox.left + - (editorBoundingBox.width * - PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / - 2; - } else if ( - event.clientX > editorBoundingBox.right && - event.clientX < - editorBoundingBox.right + - editorBoundingBox.width * - PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP - ) { - // when we're slightly right of the editor, we can drop to the side of the block - evt.clientX = - editorBoundingBox.right - - (editorBoundingBox.width * - PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / - 2; - } else if ( - event.clientX < editorBoundingBox.left || - event.clientX > editorBoundingBox.right - ) { - // when mouse is outside of the editor on x axis, drop it somewhere safe (but not to the side of a block) - evt.clientX = - editorBoundingBox.left + - PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP * - editorBoundingBox.width * - 2; // put it somewhere in first block, but safe outside of the PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP margin - } - - evt.clientY = Math.min( - Math.max(event.clientY, editorBoundingBox.top), - editorBoundingBox.top + editorBoundingBox.height - ); - - evt.dataTransfer = event.dataTransfer; - evt.preventDefault = () => event.preventDefault(); - evt.synthetic = true; // prevent recursion - return evt; - } + // private createSyntheticEvent(event: DragEvent) { + // const evt = new Event(event.type, event) as any; + // const editorBoundingBox = ( + // this.pmView.dom.firstChild as HTMLElement + // ).getBoundingClientRect(); + // evt.clientX = event.clientX; + // evt.clientY = event.clientY; + // if ( + // event.clientX < editorBoundingBox.left && + // event.clientX > + // editorBoundingBox.left - + // editorBoundingBox.width * + // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP + // ) { + // // when we're slightly left of the editor, we can drop to the side of the block + // evt.clientX = + // editorBoundingBox.left + + // (editorBoundingBox.width * + // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / + // 2; + // } else if ( + // event.clientX > editorBoundingBox.right && + // event.clientX < + // editorBoundingBox.right + + // editorBoundingBox.width * + // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP + // ) { + // // when we're slightly right of the editor, we can drop to the side of the block + // evt.clientX = + // editorBoundingBox.right - + // (editorBoundingBox.width * + // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / + // 2; + // } else if ( + // event.clientX < editorBoundingBox.left || + // event.clientX > editorBoundingBox.right + // ) { + // // when mouse is outside of the editor on x axis, drop it somewhere safe (but not to the side of a block) + // evt.clientX = + // editorBoundingBox.left + + // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP * + // editorBoundingBox.width * + // 2; // put it somewhere in first block, but safe outside of the PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP margin + // } + // + // evt.clientY = Math.min( + // Math.max(event.clientY, editorBoundingBox.top), + // editorBoundingBox.top + editorBoundingBox.height + // ); + // + // evt.dataTransfer = event.dataTransfer; + // evt.preventDefault = () => event.preventDefault(); + // evt.synthetic = true; // prevent recursion + // return evt; + // } // Needed in cases where the editor state updates without the mouse cursor // moving, as some state updates can require a side menu update. For example, From 281a84421c4827620f7603f9325a066a58201e1c Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 10 Jan 2025 15:16:33 +0100 Subject: [PATCH 2/9] Fixed multi-editor drag and drop --- examples/01-basic/04-all-blocks/App.tsx | 57 +---- .../src/extensions/SideMenu/SideMenuPlugin.ts | 204 ++++-------------- .../core/src/extensions/SideMenu/dragging.ts | 1 - 3 files changed, 46 insertions(+), 216 deletions(-) diff --git a/examples/01-basic/04-all-blocks/App.tsx b/examples/01-basic/04-all-blocks/App.tsx index 92018ee86..110c2b418 100644 --- a/examples/01-basic/04-all-blocks/App.tsx +++ b/examples/01-basic/04-all-blocks/App.tsx @@ -1,7 +1,6 @@ import { BlockNoteEditorOptions, BlockNoteSchema, - getBlockInfoFromSelection, locales, } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; @@ -13,8 +12,6 @@ import { locales as multiColumnLocales, withMultiColumn, } from "@blocknote/xl-multi-column"; -import { Slice, Node } from "@tiptap/pm/model"; -import { useState } from "react"; const schema = withMultiColumn(BlockNoteSchema.create()); const options = { @@ -195,61 +192,11 @@ export default function App() { const editor1 = useCreateBlockNote(options); const editor2 = useCreateBlockNote(options); - const [node, setNode] = useState(undefined); - // Renders the editor instance using a React component. return ( -
{ - // editor2.prosemirrorView!.dragging = editor1.prosemirrorView!.dragging; - // // editor2.prosemirrorView.dragging = true; - // }} - // onDrag={() => { - // // console.log("editor1", editor1.prosemirrorView!.dragging); - // console.log("editor2", editor2.prosemirrorView!.dragging); - // }} - > - - +
- { - editor2.prosemirrorView!.props.handleDrop?.( - editor2.prosemirrorView!, - e, - editor2.prosemirrorView!.dragging!.slice, - false - ); - }} - /> +
); } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 8a10ff9a0..feb663682 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -14,6 +14,9 @@ import { EventEmitter } from "../../util/EventEmitter.js"; import { initializeESMDependencies } from "../../util/esmDependencies.js"; import { getDraggableBlockFromElement } from "../getDraggableBlockFromElement.js"; import { dragStart, unsetDragImage } from "./dragging.js"; +import { DOMParser } from "prosemirror-model"; +import { Slice } from "@tiptap/pm/model"; + export type SideMenuState< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -23,14 +26,13 @@ export type SideMenuState< block: Block; }; -const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1; - function getBlockFromCoords( view: EditorView, coords: { left: number; top: number }, adjustForColumns = true ) { - const elements = view.root.elementsFromPoint(coords.left, coords.top); + // bit hacky - offset x position to right to account for the width of sidemenu itself + const elements = view.root.elementsFromPoint(coords.left + 50, coords.top); for (const element of elements) { if (!view.dom.contains(element)) { @@ -43,7 +45,7 @@ function getBlockFromCoords( return getBlockFromCoords( view, { - left: coords.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself + left: coords.left, top: coords.top, }, false @@ -62,56 +64,12 @@ function getBlockFromMousePos( }, view: EditorView ): { node: HTMLElement; id: string } | undefined { - // Editor itself may have padding or other styling which affects - // size/position, so we get the boundingRect of the first child (i.e. the - // blockGroup that wraps all blocks in the editor) for more accurate side - // menu placement. - if (!view.dom.firstChild) { - return; - } - - const editorBoundingBox = ( - view.dom.firstChild as HTMLElement - ).getBoundingClientRect(); - - // this.horizontalPosAnchor = editorBoundingBox.x; - - // Gets block at mouse cursor's vertical position. const coords = { left: mousePos.x, top: mousePos.y, }; - const mouseLeftOfEditor = coords.left < editorBoundingBox.left; - const mouseRightOfEditor = coords.left > editorBoundingBox.right; - - if (mouseLeftOfEditor) { - coords.left = editorBoundingBox.left + 10; - } - - if (mouseRightOfEditor) { - coords.left = editorBoundingBox.right - 10; - } - - let block = getBlockFromCoords(view, coords); - - if (!mouseRightOfEditor && block) { - // note: this case is not necessary when we're on the right side of the editor - - /* Now, because blocks can be nested - | BlockA | - x | BlockB y| - - hovering over position x (the "margin of block B") will return block A instead of block B. - to fix this, we get the block from the right side of block A (position y, which will fall in BlockB correctly) - */ - - const rect = block.node.getBoundingClientRect(); - coords.left = rect.right - 10; - block = getBlockFromCoords(view, coords, false); - } - - return block; + return getBlockFromCoords(view, coords); } /** @@ -150,10 +108,6 @@ export class SideMenuView< this.onDrop as EventListener, true ); - this.pmView.root.addEventListener( - "dragover", - this.onDragOver as EventListener - ); initializeESMDependencies(); // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen. @@ -169,8 +123,36 @@ export class SideMenuView< this.onKeyDown as EventListener, true ); + + this.pmView.root.addEventListener( + "dragstart", + this.dragStart as EventListener + ); } + dragStart = (event: DragEvent) => { + const html = event.dataTransfer?.getData("blocknote/html"); + + if (!html) { + return; + } + + const element = document.createElement("div"); + element.innerHTML = html; + + const parser = DOMParser.fromSchema(this.pmView.state.schema); + const node = parser.parse(element, { + topNode: this.pmView.state.schema.nodes["blockGroup"].create(), + }); + console.log(html); + console.log(node); + + this.pmView.dragging = { + slice: new Slice(node.content, 0, 0), + move: true, + }; + }; + updateState = (state: SideMenuState) => { this.state = state; this.emitUpdate(this.state); @@ -241,55 +223,17 @@ export class SideMenuView< } }; - /** - * If the event is outside the editor contents, - * we dispatch a fake event, so that we can still drop the content - * when dragging / dropping to the side of the editor - */ onDrop = (event: DragEvent) => { - this.editor._tiptapEditor.commands.blur(); - - // if ( - // (event as any).synthetic || - // !event.dataTransfer?.types.includes("blocknote/html") - // ) { - // return; - // } - // - // const pos = this.pmView.posAtCoords({ - // left: event.clientX, - // top: event.clientY, - // }); - // - // if (!pos || pos.inside === -1) { - // const evt = this.createSyntheticEvent(event); - // // console.log("dispatch fake drop"); - // this.pmView.dom.dispatchEvent(evt); - // } - }; + if ( + this.pmView.dragging && + this.pmView.dom.contains(event.target as Node) + ) { + return; + } - /** - * If the event is outside the editor contents, - * we dispatch a fake event, so that we can still drop the content - * when dragging / dropping to the side of the editor - */ - onDragOver = (event: DragEvent) => { - // if ( - // (event as any).synthetic || - // !event.dataTransfer?.types.includes("blocknote/html") - // ) { - // return; - // } - // const pos = this.pmView.posAtCoords({ - // left: event.clientX, - // top: event.clientY, - // }); - // - // if (!pos || (pos.inside === -1 && this.pmView.dom.firstChild)) { - // const evt = this.createSyntheticEvent(event); - // // console.log("dispatch fake dragover"); - // this.pmView.dom.dispatchEvent(evt); - // } + this.pmView.dispatch(this.pmView.state.tr.deleteSelection()); + + return; }; onKeyDown = (_event: KeyboardEvent) => { @@ -344,62 +288,6 @@ export class SideMenuView< this.updateStateFromMousePos(); }; - // private createSyntheticEvent(event: DragEvent) { - // const evt = new Event(event.type, event) as any; - // const editorBoundingBox = ( - // this.pmView.dom.firstChild as HTMLElement - // ).getBoundingClientRect(); - // evt.clientX = event.clientX; - // evt.clientY = event.clientY; - // if ( - // event.clientX < editorBoundingBox.left && - // event.clientX > - // editorBoundingBox.left - - // editorBoundingBox.width * - // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP - // ) { - // // when we're slightly left of the editor, we can drop to the side of the block - // evt.clientX = - // editorBoundingBox.left + - // (editorBoundingBox.width * - // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / - // 2; - // } else if ( - // event.clientX > editorBoundingBox.right && - // event.clientX < - // editorBoundingBox.right + - // editorBoundingBox.width * - // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP - // ) { - // // when we're slightly right of the editor, we can drop to the side of the block - // evt.clientX = - // editorBoundingBox.right - - // (editorBoundingBox.width * - // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / - // 2; - // } else if ( - // event.clientX < editorBoundingBox.left || - // event.clientX > editorBoundingBox.right - // ) { - // // when mouse is outside of the editor on x axis, drop it somewhere safe (but not to the side of a block) - // evt.clientX = - // editorBoundingBox.left + - // PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP * - // editorBoundingBox.width * - // 2; // put it somewhere in first block, but safe outside of the PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP margin - // } - // - // evt.clientY = Math.min( - // Math.max(event.clientY, editorBoundingBox.top), - // editorBoundingBox.top + editorBoundingBox.height - // ); - // - // evt.dataTransfer = event.dataTransfer; - // evt.preventDefault = () => event.preventDefault(); - // evt.synthetic = true; // prevent recursion - // return evt; - // } - // Needed in cases where the editor state updates without the mouse cursor // moving, as some state updates can require a side menu update. For example, // adding a button to the side menu which removes the block can cause the @@ -424,10 +312,6 @@ export class SideMenuView< this.onMouseMove as EventListener, true ); - this.pmView.root.removeEventListener( - "dragover", - this.onDragOver as EventListener - ); this.pmView.root.removeEventListener( "drop", diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts index 2c8a4bb5d..1dba4f462 100644 --- a/packages/core/src/extensions/SideMenu/dragging.ts +++ b/packages/core/src/extensions/SideMenu/dragging.ts @@ -202,6 +202,5 @@ export function dragStart< e.dataTransfer.setData("text/plain", plainText); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setDragImage(dragImageElement!, 0, 0); - view.dragging = { slice: selectedSlice, move: true }; } } From 67313c40e65c063a155a0bc7481f9844a6511414 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 10 Jan 2025 16:25:16 +0100 Subject: [PATCH 3/9] WIP: Added side menu detection area flag --- examples/01-basic/04-all-blocks/App.tsx | 3 +- packages/core/src/editor/BlockNoteEditor.ts | 10 + .../core/src/editor/BlockNoteExtensions.ts | 6 +- .../src/extensions/SideMenu/SideMenuPlugin.ts | 282 +++++++++++++++--- 4 files changed, 258 insertions(+), 43 deletions(-) diff --git a/examples/01-basic/04-all-blocks/App.tsx b/examples/01-basic/04-all-blocks/App.tsx index 110c2b418..5ae6d024f 100644 --- a/examples/01-basic/04-all-blocks/App.tsx +++ b/examples/01-basic/04-all-blocks/App.tsx @@ -179,6 +179,7 @@ const options = { type: "paragraph", }, ], + // sideMenuDetection: "editor", } satisfies Partial< BlockNoteEditorOptions< typeof schema.blockSchema, @@ -196,7 +197,7 @@ export default function App() { return (
- + {/**/}
); } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index f974af072..bbfc6388a 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -245,6 +245,15 @@ export type BlockNoteEditorOptions< @default "prefer-navigate-ui" */ tabBehavior: "prefer-navigate-ui" | "prefer-indent"; + + /** + * The detection mode for showing the side menu - "viewport" always shows the + * side menu for the block next to the mouse cursor, while "editor" only shows + * it when hovering the editor or the side menu itself. + * + * @default "viewport" + */ + sideMenuDetection: "viewport" | "editor"; }; const blockNoteTipTapOptions = { @@ -423,6 +432,7 @@ export class BlockNoteEditor< dropCursor: this.options.dropCursor ?? dropCursor, placeholders: newOptions.placeholders, tabBehavior: newOptions.tabBehavior, + sideMenuDetection: newOptions.sideMenuDetection || "viewport", }); // add extensions from _tiptapOptions diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts index 354ec85bd..ac826dce2 100644 --- a/packages/core/src/editor/BlockNoteExtensions.ts +++ b/packages/core/src/editor/BlockNoteExtensions.ts @@ -72,6 +72,7 @@ type ExtensionOptions< dropCursor: (opts: any) => Plugin; placeholders: Record; tabBehavior?: "prefer-navigate-ui" | "prefer-indent"; + sideMenuDetection: "viewport" | "editor"; }; /** @@ -97,7 +98,10 @@ export const getBlockNoteExtensions = < opts.editor ); ret["linkToolbar"] = new LinkToolbarProsemirrorPlugin(opts.editor); - ret["sideMenu"] = new SideMenuProsemirrorPlugin(opts.editor); + ret["sideMenu"] = new SideMenuProsemirrorPlugin( + opts.editor, + opts.sideMenuDetection + ); ret["suggestionMenus"] = new SuggestionMenuProseMirrorPlugin(opts.editor); ret["filePanel"] = new FilePanelProsemirrorPlugin(opts.editor as any); ret["placeholder"] = new PlaceholderPlugin(opts.editor, opts.placeholders); diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index feb663682..2bab5f3df 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -26,13 +26,19 @@ export type SideMenuState< block: Block; }; +const PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP = 0.1; + function getBlockFromCoords( view: EditorView, coords: { left: number; top: number }, + sideMenuDetection: "viewport" | "editor", adjustForColumns = true ) { - // bit hacky - offset x position to right to account for the width of sidemenu itself - const elements = view.root.elementsFromPoint(coords.left + 50, coords.top); + const elements = view.root.elementsFromPoint( + // bit hacky - offset x position to right to account for the width of sidemenu itself + coords.left + (sideMenuDetection === "editor" ? 50 : 0), + coords.top + ); for (const element of elements) { if (!view.dom.contains(element)) { @@ -45,9 +51,10 @@ function getBlockFromCoords( return getBlockFromCoords( view, { - left: coords.left, + left: coords.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself top: coords.top, }, + sideMenuDetection, false ); } @@ -62,14 +69,62 @@ function getBlockFromMousePos( x: number; y: number; }, - view: EditorView + view: EditorView, + sideMenuDetection: "viewport" | "editor" ): { node: HTMLElement; id: string } | undefined { + // Editor itself may have padding or other styling which affects + // size/position, so we get the boundingRect of the first child (i.e. the + // blockGroup that wraps all blocks in the editor) for more accurate side + // menu placement. + if (!view.dom.firstChild) { + return; + } + + const editorBoundingBox = ( + view.dom.firstChild as HTMLElement + ).getBoundingClientRect(); + + // this.horizontalPosAnchor = editorBoundingBox.x; + + // Gets block at mouse cursor's position. const coords = { left: mousePos.x, top: mousePos.y, }; - return getBlockFromCoords(view, coords); + const mouseLeftOfEditor = coords.left < editorBoundingBox.left; + const mouseRightOfEditor = coords.left > editorBoundingBox.right; + + // Clamps the x position to the editor's bounding box. + if (sideMenuDetection === "viewport") { + if (mouseLeftOfEditor) { + coords.left = editorBoundingBox.left + 10; + } + + if (mouseRightOfEditor) { + coords.left = editorBoundingBox.right - 10; + } + } + + let block = getBlockFromCoords(view, coords, sideMenuDetection); + + if (!mouseRightOfEditor && block) { + // note: this case is not necessary when we're on the right side of the editor + + /* Now, because blocks can be nested + | BlockA | + x | BlockB y| + + hovering over position x (the "margin of block B") will return block A instead of block B. + to fix this, we get the block from the right side of block A (position y, which will fall in BlockB correctly) + */ + + const rect = block.node.getBoundingClientRect(); + coords.left = rect.right - 10; + block = getBlockFromCoords(view, coords, "viewport", false); + } + + return block; } /** @@ -92,6 +147,7 @@ export class SideMenuView< constructor( private readonly editor: BlockNoteEditor, + private readonly sideMenuDetection: "viewport" | "editor", private readonly pmView: EditorView, emitUpdate: (state: SideMenuState) => void ) { @@ -103,6 +159,10 @@ export class SideMenuView< emitUpdate(this.state); }; + this.pmView.root.addEventListener( + "dragstart", + this.onDragStart as EventListener + ); this.pmView.root.addEventListener( "drop", this.onDrop as EventListener, @@ -123,36 +183,12 @@ export class SideMenuView< this.onKeyDown as EventListener, true ); - this.pmView.root.addEventListener( - "dragstart", - this.dragStart as EventListener + "dragover", + this.onDragOver as EventListener ); } - dragStart = (event: DragEvent) => { - const html = event.dataTransfer?.getData("blocknote/html"); - - if (!html) { - return; - } - - const element = document.createElement("div"); - element.innerHTML = html; - - const parser = DOMParser.fromSchema(this.pmView.state.schema); - const node = parser.parse(element, { - topNode: this.pmView.state.schema.nodes["blockGroup"].create(), - }); - console.log(html); - console.log(node); - - this.pmView.dragging = { - slice: new Slice(node.content, 0, 0), - move: true, - }; - }; - updateState = (state: SideMenuState) => { this.state = state; this.emitUpdate(this.state); @@ -163,7 +199,11 @@ export class SideMenuView< return; } - const block = getBlockFromMousePos(this.mousePos, this.pmView); + const block = getBlockFromMousePos( + this.mousePos, + this.pmView, + this.sideMenuDetection + ); // Closes the menu if the mouse cursor is beyond the editor vertically. if (!block || !this.editor.isEditable) { @@ -223,17 +263,110 @@ export class SideMenuView< } }; + /** + * If the event is outside the editor contents, + * we dispatch a fake event, so that we can still drop the content + * when dragging / dropping to the side of the editor + */ onDrop = (event: DragEvent) => { + this.editor._tiptapEditor.commands.blur(); + + // ProseMirror doesn't remove the dragged content if it's dropped outside + // the editor (e.g. to other editors), so we need to do it manually. Since + // the dragged content is the same as the selected content, we can just + // delete the selection. if ( - this.pmView.dragging && - this.pmView.dom.contains(event.target as Node) + // TODO + event.dataTransfer?.types.includes("blocknote/html") && + !this.pmView.dom.contains(event.target as Node) + ) { + this.pmView.dispatch(this.pmView.state.tr.deleteSelection()); + } + + if ( + this.sideMenuDetection === "editor" || + (event as any).synthetic || + !event.dataTransfer?.types.includes("blocknote/html") ) { return; } - this.pmView.dispatch(this.pmView.state.tr.deleteSelection()); + const pos = this.pmView.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); - return; + if (!pos || pos.inside === -1) { + const evt = this.createSyntheticEvent(event); + // console.log("dispatch fake drop"); + this.pmView.dom.dispatchEvent(evt); + } + }; + + /** + * If a block is being dragged, ProseMirror usually gets the context of what's + * being dragged from `view.dragging`, which is automatically set when a + * `dragstart` event fires in the editor. However, if the user tries to drag + * and drop blocks between multiple editors, only the one in which the drag + * began has that context, so we need to set it on the others manually. This + * ensures that PM always drops the blocks in between other blocks, and not + * inside them. + * + * After the `dragstart` event fires on the drag handle, it sets + * `blocknote/html` data on the clipboard. This handler fires right after, + * parsing the `blocknote/html` data into nodes and setting them on + * `view.dragging`. + * + * Note: Setting `view.dragging` on `dragover` would be better as the user + * could then drag between editors in different windows, but you can only + * access `dataTransfer` contents on `dragstart` and `drop` events. + */ + onDragStart = (event: DragEvent) => { + if (!this.pmView.dragging) { + const html = event.dataTransfer?.getData("blocknote/html"); + if (!html) { + return; + } + + const element = document.createElement("div"); + element.innerHTML = html; + + const parser = DOMParser.fromSchema(this.pmView.state.schema); + const node = parser.parse(element, { + topNode: this.pmView.state.schema.nodes["blockGroup"].create(), + }); + + this.pmView.dragging = { + slice: new Slice(node.content, 0, 0), + move: true, + }; + } + }; + + /** + * If the event is outside the editor contents, + * we dispatch a fake event, so that we can still drop the content + * when dragging / dropping to the side of the editor + */ + onDragOver = (event: DragEvent) => { + if ( + this.sideMenuDetection === "editor" || + (event as any).synthetic || + !event.dataTransfer?.types.includes("blocknote/html") + ) { + return; + } + + const pos = this.pmView.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (!pos || (pos.inside === -1 && this.pmView.dom.firstChild)) { + const evt = this.createSyntheticEvent(event); + // console.log("dispatch fake dragover"); + this.pmView.dom.dispatchEvent(evt); + } }; onKeyDown = (_event: KeyboardEvent) => { @@ -244,6 +377,62 @@ export class SideMenuView< } }; + private createSyntheticEvent(event: DragEvent) { + const evt = new Event(event.type, event) as any; + const editorBoundingBox = ( + this.pmView.dom.firstChild as HTMLElement + ).getBoundingClientRect(); + evt.clientX = event.clientX; + evt.clientY = event.clientY; + if ( + event.clientX < editorBoundingBox.left && + event.clientX > + editorBoundingBox.left - + editorBoundingBox.width * + PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP + ) { + // when we're slightly left of the editor, we can drop to the side of the block + evt.clientX = + editorBoundingBox.left + + (editorBoundingBox.width * + PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / + 2; + } else if ( + event.clientX > editorBoundingBox.right && + event.clientX < + editorBoundingBox.right + + editorBoundingBox.width * + PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP + ) { + // when we're slightly right of the editor, we can drop to the side of the block + evt.clientX = + editorBoundingBox.right - + (editorBoundingBox.width * + PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP) / + 2; + } else if ( + event.clientX < editorBoundingBox.left || + event.clientX > editorBoundingBox.right + ) { + // when mouse is outside of the editor on x axis, drop it somewhere safe (but not to the side of a block) + evt.clientX = + editorBoundingBox.left + + PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP * + editorBoundingBox.width * + 2; // put it somewhere in first block, but safe outside of the PERCENTAGE_OF_BLOCK_WIDTH_CONSIDERED_SIDE_DROP margin + } + + evt.clientY = Math.min( + Math.max(event.clientY, editorBoundingBox.top), + editorBoundingBox.top + editorBoundingBox.height + ); + + evt.dataTransfer = event.dataTransfer; + evt.preventDefault = () => event.preventDefault(); + evt.synthetic = true; // prevent recursion + return evt; + } + onMouseMove = (event: MouseEvent) => { if (this.menuFrozen) { return; @@ -312,7 +501,10 @@ export class SideMenuView< this.onMouseMove as EventListener, true ); - + this.pmView.root.removeEventListener( + "dragstart", + this.onDragStart as EventListener + ); this.pmView.root.removeEventListener( "drop", this.onDrop as EventListener, @@ -336,14 +528,22 @@ export class SideMenuProsemirrorPlugin< public view: SideMenuView | undefined; public readonly plugin: Plugin; - constructor(private readonly editor: BlockNoteEditor) { + constructor( + private readonly editor: BlockNoteEditor, + sideMenuDetection: "viewport" | "editor" + ) { super(); this.plugin = new Plugin({ key: sideMenuPluginKey, view: (editorView) => { - this.view = new SideMenuView(editor, editorView, (state) => { - this.emit("update", state); - }); + this.view = new SideMenuView( + editor, + sideMenuDetection, + editorView, + (state) => { + this.emit("update", state); + } + ); return this.view; }, }); From 1773a9d44619af57b901d9857128abdbd7dd4121 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 10 Jan 2025 16:31:27 +0100 Subject: [PATCH 4/9] Small changes --- .../src/extensions/SideMenu/SideMenuPlugin.ts | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 2bab5f3df..8dd7129bb 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -163,6 +163,10 @@ export class SideMenuView< "dragstart", this.onDragStart as EventListener ); + this.pmView.root.addEventListener( + "dragover", + this.onDragOver as EventListener + ); this.pmView.root.addEventListener( "drop", this.onDrop as EventListener, @@ -183,10 +187,6 @@ export class SideMenuView< this.onKeyDown as EventListener, true ); - this.pmView.root.addEventListener( - "dragover", - this.onDragOver as EventListener - ); } updateState = (state: SideMenuState) => { @@ -377,6 +377,50 @@ export class SideMenuView< } }; + onMouseMove = (event: MouseEvent) => { + if (this.menuFrozen) { + return; + } + + this.mousePos = { x: event.clientX, y: event.clientY }; + + // We want the full area of the editor to check if the cursor is hovering + // above it though. + const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect(); + const cursorWithinEditor = + this.mousePos.x > editorOuterBoundingBox.left && + this.mousePos.x < editorOuterBoundingBox.right && + this.mousePos.y > editorOuterBoundingBox.top && + this.mousePos.y < editorOuterBoundingBox.bottom; + + // TODO: remove parentElement, but then we need to remove padding from boundingbox or find a different solution + const editorWrapper = this.pmView.dom!.parentElement!; + + // Doesn't update if the mouse hovers an element that's over the editor but + // isn't a part of it or the side menu. + if ( + // Cursor is within the editor area + cursorWithinEditor && + // An element is hovered + event && + event.target && + // Element is outside the editor + !( + editorWrapper === event.target || + editorWrapper.contains(event.target as HTMLElement) + ) + ) { + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); + } + + return; + } + + this.updateStateFromMousePos(); + }; + private createSyntheticEvent(event: DragEvent) { const evt = new Event(event.type, event) as any; const editorBoundingBox = ( @@ -433,50 +477,6 @@ export class SideMenuView< return evt; } - onMouseMove = (event: MouseEvent) => { - if (this.menuFrozen) { - return; - } - - this.mousePos = { x: event.clientX, y: event.clientY }; - - // We want the full area of the editor to check if the cursor is hovering - // above it though. - const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect(); - const cursorWithinEditor = - this.mousePos.x > editorOuterBoundingBox.left && - this.mousePos.x < editorOuterBoundingBox.right && - this.mousePos.y > editorOuterBoundingBox.top && - this.mousePos.y < editorOuterBoundingBox.bottom; - - // TODO: remove parentElement, but then we need to remove padding from boundingbox or find a different solution - const editorWrapper = this.pmView.dom!.parentElement!; - - // Doesn't update if the mouse hovers an element that's over the editor but - // isn't a part of it or the side menu. - if ( - // Cursor is within the editor area - cursorWithinEditor && - // An element is hovered - event && - event.target && - // Element is outside the editor - !( - editorWrapper === event.target || - editorWrapper.contains(event.target as HTMLElement) - ) - ) { - if (this.state?.show) { - this.state.show = false; - this.emitUpdate(this.state); - } - - return; - } - - this.updateStateFromMousePos(); - }; - // Needed in cases where the editor state updates without the mouse cursor // moving, as some state updates can require a side menu update. For example, // adding a button to the side menu which removes the block can cause the From 3230091a9d8dbc612cc7baff61b051da4731b4f8 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 10 Jan 2025 17:17:35 +0100 Subject: [PATCH 5/9] Source blocks are now deleted correctly & fixed error sometimes being thrown after drag & drop --- .../CodeBlockContent/CodeBlockContent.ts | 1 + .../HeadingBlockContent.ts | 4 +++- .../CheckListItemBlockContent.ts | 11 +++++++++ .../NumberedListItemBlockContent.ts | 11 +++++++++ .../src/extensions/SideMenu/SideMenuPlugin.ts | 23 +++++++++++++++---- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts index ce71bba1b..1a9c863d6 100644 --- a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts +++ b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts @@ -93,6 +93,7 @@ const CodeBlockContent = createStronglyTypedTiptapNode({ ); }, renderHTML: (attributes) => { + // TODO: Use `data-language="..."` instead for easier parsing return attributes.language && attributes.language !== "text" ? { class: `language-${attributes.language}`, diff --git a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts index 2bf825dd6..fcaedd879 100644 --- a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts @@ -130,7 +130,9 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ } return { - level: element.getAttribute("data-level"), + level: + element.getAttribute("data-level") || + headingPropSchema.level.default, }; }, }, diff --git a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts index 628df671a..93ce2fe1c 100644 --- a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts @@ -119,6 +119,17 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({ return [ { tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + return { + checked: + element.getAttribute("data-index") || + checkListItemPropSchema.checked.default, + }; + }, }, // Checkbox only. { diff --git a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index f4de5a8ac..a67cd4bc4 100644 --- a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -107,6 +107,17 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ return [ { tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + return { + index: + element.getAttribute("data-index") || + numberedListItemPropSchema.start.default, + }; + }, }, // Case for regular HTML list structure. // (e.g.: when pasting from other apps) diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 8dd7129bb..6ebf5c1f0 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -145,6 +145,8 @@ export class SideMenuView< public menuFrozen = false; + public isDragOrigin = false; + constructor( private readonly editor: BlockNoteEditor, private readonly sideMenuDetection: "viewport" | "editor", @@ -275,11 +277,7 @@ export class SideMenuView< // the editor (e.g. to other editors), so we need to do it manually. Since // the dragged content is the same as the selected content, we can just // delete the selection. - if ( - // TODO - event.dataTransfer?.types.includes("blocknote/html") && - !this.pmView.dom.contains(event.target as Node) - ) { + if (this.isDragOrigin && !this.pmView.dom.contains(event.target as Node)) { this.pmView.dispatch(this.pmView.state.tr.deleteSelection()); } @@ -336,6 +334,9 @@ export class SideMenuView< topNode: this.pmView.state.schema.nodes["blockGroup"].create(), }); + console.log(html); + console.log(node); + this.pmView.dragging = { slice: new Slice(node.content, 0, 0), move: true, @@ -505,6 +506,10 @@ export class SideMenuView< "dragstart", this.onDragStart as EventListener ); + this.pmView.root.removeEventListener( + "dragover", + this.onDragOver as EventListener + ); this.pmView.root.removeEventListener( "drop", this.onDrop as EventListener, @@ -563,6 +568,10 @@ export class SideMenuProsemirrorPlugin< }, block: Block ) => { + if (this.view) { + this.view.isDragOrigin = true; + } + dragStart(event, block, this.editor); }; @@ -573,6 +582,10 @@ export class SideMenuProsemirrorPlugin< if (this.editor.prosemirrorView) { unsetDragImage(this.editor.prosemirrorView.root); } + + if (this.view) { + this.view.isDragOrigin = false; + } }; /** * Freezes the side menu. When frozen, the side menu will stay From c9746778cab32e8ef1d43a37e748f500c53bcd72 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 10 Jan 2025 17:30:31 +0100 Subject: [PATCH 6/9] Removed logs --- packages/core/src/extensions/SideMenu/SideMenuPlugin.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 6ebf5c1f0..215aa2223 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -334,9 +334,6 @@ export class SideMenuView< topNode: this.pmView.state.schema.nodes["blockGroup"].create(), }); - console.log(html); - console.log(node); - this.pmView.dragging = { slice: new Slice(node.content, 0, 0), move: true, From 934a632574fcca74a72e1732f09e419ad54061c9 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Mon, 13 Jan 2025 17:34:11 +0100 Subject: [PATCH 7/9] Implemented PR feedback --- .../parse-blocknote-html-with-props.json | 212 ++++++++++++++++++ .../__snapshots__/parse-blocknote-html.json | 211 +++++++++++++++++ .../src/api/parsers/html/parseHTML.test.ts | 196 ++++++++++++++++ .../CodeBlockContent/CodeBlockContent.ts | 2 + .../HeadingBlockContent.ts | 11 - .../BulletListItemBlockContent.ts | 2 +- .../CheckListItemBlockContent.ts | 13 +- .../NumberedListItemBlockContent.ts | 13 +- .../src/extensions/SideMenu/SideMenuPlugin.ts | 8 +- 9 files changed, 627 insertions(+), 41 deletions(-) create mode 100644 packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json create mode 100644 packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json b/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json new file mode 100644 index 000000000..45d71ca3b --- /dev/null +++ b/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json @@ -0,0 +1,212 @@ +[ + { + "id": "907bc887-c78e-47cd-a538-6a2d86564f8e", + "type": "paragraph", + "props": { + "textColor": "red", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "d40579fe-1e17-4a97-9d87-fc61f2ddff98", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Heading", + "styles": {} + } + ], + "children": [] + }, + { + "id": "374ea05b-2ae0-4b57-a141-910ca0d56203", + "type": "numberedListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "start": 2 + }, + "content": [ + { + "type": "text", + "text": "Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2d285dd8-9f38-4191-913b-b6ae547713ff", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "red", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2ad5bdaf-31c9-47c2-ba02-cf6827b4c858", + "type": "checkListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "checked": true + }, + "content": [ + { + "type": "text", + "text": "Check List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "979258b0-0df5-4b7c-8a38-a7576d4a1de2", + "type": "codeBlock", + "props": { + "language": "typescript" + }, + "content": [ + { + "type": "text", + "text": "console.log(\"Hello World\");", + "styles": {} + } + ], + "children": [] + }, + { + "id": "af6fc0ee-b997-4c0c-b454-ebb2b43b4d43", + "type": "table", + "props": { + "textColor": "default" + }, + "content": { + "type": "tableContent", + "columnWidths": [ + null, + null, + null + ], + "rows": [ + { + "cells": [ + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ] + ] + }, + { + "cells": [ + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ] + ] + }, + { + "cells": [ + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ] + ] + } + ] + }, + "children": [] + }, + { + "id": "1fd363b8-b843-4a69-982f-acaf24f25f7f", + "type": "image", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "name": "1280px-Placeholder_view_vector.svg.png", + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/1280px-Placeholder_view_vector.svg.png", + "caption": "", + "showPreview": true, + "previewWidth": 512 + }, + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json b/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json new file mode 100644 index 000000000..94bbcf972 --- /dev/null +++ b/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json @@ -0,0 +1,211 @@ +[ + { + "id": "907bc887-c78e-47cd-a538-6a2d86564f8e", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "d40579fe-1e17-4a97-9d87-fc61f2ddff98", + "type": "heading", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Heading", + "styles": {} + } + ], + "children": [] + }, + { + "id": "374ea05b-2ae0-4b57-a141-910ca0d56203", + "type": "numberedListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2d285dd8-9f38-4191-913b-b6ae547713ff", + "type": "bulletListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "2ad5bdaf-31c9-47c2-ba02-cf6827b4c858", + "type": "checkListItem", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left", + "checked": false + }, + "content": [ + { + "type": "text", + "text": "Check List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "979258b0-0df5-4b7c-8a38-a7576d4a1de2", + "type": "codeBlock", + "props": { + "language": "javascript" + }, + "content": [ + { + "type": "text", + "text": "console.log(\"Hello World\");", + "styles": {} + } + ], + "children": [] + }, + { + "id": "af6fc0ee-b997-4c0c-b454-ebb2b43b4d43", + "type": "table", + "props": { + "textColor": "default" + }, + "content": { + "type": "tableContent", + "columnWidths": [ + null, + null, + null + ], + "rows": [ + { + "cells": [ + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ] + ] + }, + { + "cells": [ + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ] + ] + }, + { + "cells": [ + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ], + [ + { + "type": "text", + "text": "Table Cell", + "styles": {} + } + ] + ] + } + ] + }, + "children": [] + }, + { + "id": "1fd363b8-b843-4a69-982f-acaf24f25f7f", + "type": "image", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "name": "", + "url": "", + "caption": "", + "showPreview": true, + "previewWidth": 512 + }, + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index fe41fe41c..faf3420eb 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -59,6 +59,202 @@ async function parseHTMLAndCompareSnapshots( } describe("Parse HTML", () => { + it("Parse BlockNote HTML", async () => { + const html = `
+
+
+
+

Paragraph

+
+
+
+
+
+
+

Heading

+
+
+
+
+
+
+

Numbered List Item

+
+
+
+
+
+
+

Bullet List Item

+
+
+
+
+
+
+ +

Check List Item

+
+
+
+
+
+
+
console.log("Hello World");
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+
+
+
+
+
+
+
+
+
+
+
`; + + await parseHTMLAndCompareSnapshots(html, "parse-blocknote-html"); + }); + + it("Parse BlockNote HTML with props", async () => { + const html = `
+
+
+
+

Paragraph

+
+
+
+
+
+
+

Heading

+
+
+
+
+
+
+

Numbered List Item

+
+
+
+
+
+
+

Bullet List Item

+
+
+
+
+
+
+ +

Check List Item

+
+
+
+
+
+
+
console.log("Hello World");
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+

Table Cell

+
+
+
+
+
+
+
+
+
+
+
+
`; + + await parseHTMLAndCompareSnapshots(html, "parse-blocknote-html-with-props"); + }); + it("Parse basic block types", async () => { const html = `

Heading 1

Heading 2

diff --git a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts index 1a9c863d6..9d9460167 100644 --- a/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts +++ b/packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts @@ -107,9 +107,11 @@ const CodeBlockContent = createStronglyTypedTiptapNode({ return [ { tag: "div[data-content-type=" + this.name + "]", + contentElement: "code", }, { tag: "pre", + contentElement: "code", preserveWhitespace: "full", }, ]; diff --git a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts index fcaedd879..f20e0b419 100644 --- a/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/blocks/HeadingBlockContent/HeadingBlockContent.ts @@ -124,17 +124,6 @@ const HeadingBlockContent = createStronglyTypedTiptapNode({ return [ { tag: "div[data-content-type=" + this.name + "]", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - return { - level: - element.getAttribute("data-level") || - headingPropSchema.level.default, - }; - }, }, { tag: "h1", diff --git a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index 06259794f..e373c9524 100644 --- a/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -79,7 +79,7 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({ return [ // Case for regular HTML list structure. { - tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this + tag: "div[data-content-type=" + this.name + "]", }, { tag: "li", diff --git a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts index 93ce2fe1c..3c2b2a209 100644 --- a/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts @@ -118,18 +118,7 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({ parseHTML() { return [ { - tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - return { - checked: - element.getAttribute("data-index") || - checkListItemPropSchema.checked.default, - }; - }, + tag: "div[data-content-type=" + this.name + "]", }, // Checkbox only. { diff --git a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index a67cd4bc4..d63712272 100644 --- a/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -106,18 +106,7 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ parseHTML() { return [ { - tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - return { - index: - element.getAttribute("data-index") || - numberedListItemPropSchema.start.default, - }; - }, + tag: "div[data-content-type=" + this.name + "]", }, // Case for regular HTML list structure. // (e.g.: when pasting from other apps) diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 215aa2223..62a1541c8 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -1,6 +1,6 @@ -import { PluginView } from "@tiptap/pm/state"; -import { EditorState, Plugin, PluginKey } from "prosemirror-state"; -import { EditorView } from "prosemirror-view"; +import { DOMParser, Slice } from "@tiptap/pm/model"; +import { EditorState, Plugin, PluginKey, PluginView } from "@tiptap/pm/state"; +import { EditorView } from "@tiptap/pm/view"; import { Block } from "../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; @@ -14,8 +14,6 @@ import { EventEmitter } from "../../util/EventEmitter.js"; import { initializeESMDependencies } from "../../util/esmDependencies.js"; import { getDraggableBlockFromElement } from "../getDraggableBlockFromElement.js"; import { dragStart, unsetDragImage } from "./dragging.js"; -import { DOMParser } from "prosemirror-model"; -import { Slice } from "@tiptap/pm/model"; export type SideMenuState< BSchema extends BlockSchema, From 534b8464a2894ca9bde41a480109e6d0c080265a Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Mon, 13 Jan 2025 17:58:04 +0100 Subject: [PATCH 8/9] Updated test snapshot --- .../headings-json-chromium-linux.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json b/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json index a777ca116..efc45cfa6 100644 --- a/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json +++ b/tests/src/end-to-end/copypaste/copypaste.test.ts-snapshots/headings-json-chromium-linux.json @@ -101,7 +101,7 @@ "type": "heading", "attrs": { "textAlignment": "left", - "level": null + "level": 1 }, "content": [ { From 10323832b32c60523ec7b1636db57fda60fa2107 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 14 Jan 2025 12:05:22 +0100 Subject: [PATCH 9/9] Moved/refactored tests --- .../__snapshots__/internal/basicBlocks.html | 1 + .../internal/basicBlocksWithProps.html | 1 + .../api/clipboard/clipboardInternal.test.ts | 126 +++++++++++ .../parse-blocknote-html-with-props.json | 212 ------------------ .../__snapshots__/parse-blocknote-html.json | 211 ----------------- .../src/api/parsers/html/parseHTML.test.ts | 196 ---------------- 6 files changed, 128 insertions(+), 619 deletions(-) create mode 100644 packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html delete mode 100644 packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json delete mode 100644 packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json diff --git a/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html new file mode 100644 index 000000000..8c7757e46 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocks.html @@ -0,0 +1 @@ +

Paragraph

Heading

  1. Numbered List Item

  • Bullet List Item

  • Check List Item

console.log("Hello World");

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Add image

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html new file mode 100644 index 000000000..439741382 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/internal/basicBlocksWithProps.html @@ -0,0 +1 @@ +

Paragraph

Heading

  1. Numbered List Item

  • Bullet List Item

  • Check List Item

console.log("Hello World");

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

Table Cell

1280px-Placeholder_view_vector.svg.png
Placeholder

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/clipboardInternal.test.ts b/packages/core/src/api/clipboard/clipboardInternal.test.ts index 29b5393f0..0fb136575 100644 --- a/packages/core/src/api/clipboard/clipboardInternal.test.ts +++ b/packages/core/src/api/clipboard/clipboardInternal.test.ts @@ -148,6 +148,122 @@ describe("Test ProseMirror selection clipboard HTML", () => { type: "customParagraph", content: "Paragraph", }, + { + type: "paragraph", + content: "Paragraph", + }, + { + type: "heading", + content: "Heading", + }, + { + type: "numberedListItem", + content: "Numbered List Item", + }, + { + type: "bulletListItem", + content: "Bullet List Item", + }, + { + type: "checkListItem", + content: "Check List Item", + }, + { + type: "codeBlock", + content: 'console.log("Hello World");', + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]], + }, + { + cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]], + }, + { + cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]], + }, + ], + }, + }, + { + type: "image", + }, + { + type: "paragraph", + props: { + textColor: "red", + }, + content: "Paragraph", + }, + { + type: "heading", + props: { + level: 2, + }, + content: "Heading", + }, + { + type: "numberedListItem", + props: { + start: 2, + }, + content: "Numbered List Item", + }, + { + type: "bulletListItem", + props: { + backgroundColor: "red", + }, + content: "Bullet List Item", + }, + { + type: "checkListItem", + props: { + checked: true, + }, + content: "Check List Item", + }, + { + type: "codeBlock", + props: { + language: "typescript", + }, + content: 'console.log("Hello World");', + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]], + }, + { + cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]], + }, + { + cells: [["Table Cell"], ["Table Cell"], ["Table Cell"]], + }, + ], + }, + }, + { + type: "image", + props: { + name: "1280px-Placeholder_view_vector.svg.png", + url: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/1280px-Placeholder_view_vector.svg.png", + caption: "Placeholder", + showPreview: true, + previewWidth: 256, + }, + }, + { + type: "paragraph", + }, ]; let editor: BlockNoteEditor; @@ -299,6 +415,16 @@ describe("Test ProseMirror selection clipboard HTML", () => { createCopySelection: (doc) => TextSelection.create(doc, 277, 286), createPasteSelection: (doc) => TextSelection.create(doc, 290, 299), }, + // Copy/paste basic blocks. + { + testName: "basicBlocks", + createCopySelection: (doc) => TextSelection.create(doc, 303, 558), + }, + // Copy/paste basic blocks with props. + { + testName: "basicBlocksWithProps", + createCopySelection: (doc) => TextSelection.create(doc, 558, 813), + }, ]; for (const testCase of testCases) { diff --git a/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json b/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json deleted file mode 100644 index 45d71ca3b..000000000 --- a/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html-with-props.json +++ /dev/null @@ -1,212 +0,0 @@ -[ - { - "id": "907bc887-c78e-47cd-a538-6a2d86564f8e", - "type": "paragraph", - "props": { - "textColor": "red", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph", - "styles": {} - } - ], - "children": [] - }, - { - "id": "d40579fe-1e17-4a97-9d87-fc61f2ddff98", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 2 - }, - "content": [ - { - "type": "text", - "text": "Heading", - "styles": {} - } - ], - "children": [] - }, - { - "id": "374ea05b-2ae0-4b57-a141-910ca0d56203", - "type": "numberedListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "start": 2 - }, - "content": [ - { - "type": "text", - "text": "Numbered List Item", - "styles": {} - } - ], - "children": [] - }, - { - "id": "2d285dd8-9f38-4191-913b-b6ae547713ff", - "type": "bulletListItem", - "props": { - "textColor": "default", - "backgroundColor": "red", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Bullet List Item", - "styles": {} - } - ], - "children": [] - }, - { - "id": "2ad5bdaf-31c9-47c2-ba02-cf6827b4c858", - "type": "checkListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "checked": true - }, - "content": [ - { - "type": "text", - "text": "Check List Item", - "styles": {} - } - ], - "children": [] - }, - { - "id": "979258b0-0df5-4b7c-8a38-a7576d4a1de2", - "type": "codeBlock", - "props": { - "language": "typescript" - }, - "content": [ - { - "type": "text", - "text": "console.log(\"Hello World\");", - "styles": {} - } - ], - "children": [] - }, - { - "id": "af6fc0ee-b997-4c0c-b454-ebb2b43b4d43", - "type": "table", - "props": { - "textColor": "default" - }, - "content": { - "type": "tableContent", - "columnWidths": [ - null, - null, - null - ], - "rows": [ - { - "cells": [ - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ] - ] - }, - { - "cells": [ - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ] - ] - }, - { - "cells": [ - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ] - ] - } - ] - }, - "children": [] - }, - { - "id": "1fd363b8-b843-4a69-982f-acaf24f25f7f", - "type": "image", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "name": "1280px-Placeholder_view_vector.svg.png", - "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Placeholder_view_vector.svg/1280px-Placeholder_view_vector.svg.png", - "caption": "", - "showPreview": true, - "previewWidth": 512 - }, - "children": [] - } -] \ No newline at end of file diff --git a/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json b/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json deleted file mode 100644 index 94bbcf972..000000000 --- a/packages/core/src/api/parsers/html/__snapshots__/parse-blocknote-html.json +++ /dev/null @@ -1,211 +0,0 @@ -[ - { - "id": "907bc887-c78e-47cd-a538-6a2d86564f8e", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph", - "styles": {} - } - ], - "children": [] - }, - { - "id": "d40579fe-1e17-4a97-9d87-fc61f2ddff98", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 1 - }, - "content": [ - { - "type": "text", - "text": "Heading", - "styles": {} - } - ], - "children": [] - }, - { - "id": "374ea05b-2ae0-4b57-a141-910ca0d56203", - "type": "numberedListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Numbered List Item", - "styles": {} - } - ], - "children": [] - }, - { - "id": "2d285dd8-9f38-4191-913b-b6ae547713ff", - "type": "bulletListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Bullet List Item", - "styles": {} - } - ], - "children": [] - }, - { - "id": "2ad5bdaf-31c9-47c2-ba02-cf6827b4c858", - "type": "checkListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "checked": false - }, - "content": [ - { - "type": "text", - "text": "Check List Item", - "styles": {} - } - ], - "children": [] - }, - { - "id": "979258b0-0df5-4b7c-8a38-a7576d4a1de2", - "type": "codeBlock", - "props": { - "language": "javascript" - }, - "content": [ - { - "type": "text", - "text": "console.log(\"Hello World\");", - "styles": {} - } - ], - "children": [] - }, - { - "id": "af6fc0ee-b997-4c0c-b454-ebb2b43b4d43", - "type": "table", - "props": { - "textColor": "default" - }, - "content": { - "type": "tableContent", - "columnWidths": [ - null, - null, - null - ], - "rows": [ - { - "cells": [ - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ] - ] - }, - { - "cells": [ - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ] - ] - }, - { - "cells": [ - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Table Cell", - "styles": {} - } - ] - ] - } - ] - }, - "children": [] - }, - { - "id": "1fd363b8-b843-4a69-982f-acaf24f25f7f", - "type": "image", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "name": "", - "url": "", - "caption": "", - "showPreview": true, - "previewWidth": 512 - }, - "children": [] - } -] \ No newline at end of file diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index faf3420eb..fe41fe41c 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -59,202 +59,6 @@ async function parseHTMLAndCompareSnapshots( } describe("Parse HTML", () => { - it("Parse BlockNote HTML", async () => { - const html = `
-
-
-
-

Paragraph

-
-
-
-
-
-
-

Heading

-
-
-
-
-
-
-

Numbered List Item

-
-
-
-
-
-
-

Bullet List Item

-
-
-
-
-
-
- -

Check List Item

-
-
-
-
-
-
-
console.log("Hello World");
-
-
-
-
-
-
- - - - - - - - - - - - - - - - -
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-
-
-
-
-
-
-
-
-
-
-
`; - - await parseHTMLAndCompareSnapshots(html, "parse-blocknote-html"); - }); - - it("Parse BlockNote HTML with props", async () => { - const html = `
-
-
-
-

Paragraph

-
-
-
-
-
-
-

Heading

-
-
-
-
-
-
-

Numbered List Item

-
-
-
-
-
-
-

Bullet List Item

-
-
-
-
-
-
- -

Check List Item

-
-
-
-
-
-
-
console.log("Hello World");
-
-
-
-
-
-
- - - - - - - - - - - - - - - - -
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-

Table Cell

-
-
-
-
-
-
-
-
-
-
-
-
`; - - await parseHTMLAndCompareSnapshots(html, "parse-blocknote-html-with-props"); - }); - it("Parse basic block types", async () => { const html = `

Heading 1

Heading 2