diff --git a/packages/core/src/api/clipboard/__snapshots__/childToParent.html b/packages/core/src/api/clipboard/__snapshots__/childToParent.html
new file mode 100644
index 000000000..a899fb60f
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/childToParent.html
@@ -0,0 +1 @@
+
Heading 1
Nested Paragraph 1
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html
new file mode 100644
index 000000000..851b8973d
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html
@@ -0,0 +1 @@
+Nested Paragraph 1
Nested Paragraph 2
Nested Paragraph 3
Heading 2
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html
new file mode 100644
index 000000000..ba81a3a20
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html
@@ -0,0 +1 @@
+Nested Paragraph 1
Nested Paragraph 2
Nested Paragraph 3
Heading 2
Nested Paragraph 1
Nested Paragraph 2
Nested Paragraph 3
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/image.html b/packages/core/src/api/clipboard/__snapshots__/image.html
new file mode 100644
index 000000000..09248e592
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/image.html
@@ -0,0 +1 @@
+Nested Paragraph
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionWithinBlockChildren.html b/packages/core/src/api/clipboard/__snapshots__/multipleChildren.html
similarity index 100%
rename from packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionWithinBlockChildren.html
rename to packages/core/src/api/clipboard/__snapshots__/multipleChildren.html
diff --git a/packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html b/packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html
new file mode 100644
index 000000000..a9991463b
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html
@@ -0,0 +1 @@
+BoldItalicRegular
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/nestedImage.html b/packages/core/src/api/clipboard/__snapshots__/nestedImage.html
new file mode 100644
index 000000000..4e181d251
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/nestedImage.html
@@ -0,0 +1 @@
+BoldItalicRegular
Nested Paragraph
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html b/packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html
new file mode 100644
index 000000000..f4d82fbae
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html
@@ -0,0 +1 @@
+ding 1
Nested
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/styledText.html b/packages/core/src/api/clipboard/__snapshots__/styledText.html
new file mode 100644
index 000000000..03d564343
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/styledText.html
@@ -0,0 +1 @@
+Italic
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html b/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html
new file mode 100644
index 000000000..1de956c17
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html
@@ -0,0 +1 @@
+Table Cell | Table Cell |
Table Cell | Table Cell |
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/tableCell.html b/packages/core/src/api/clipboard/__snapshots__/tableCell.html
new file mode 100644
index 000000000..b7fa75d30
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/tableCell.html
@@ -0,0 +1 @@
+Table Cell |
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/tableCellText.html b/packages/core/src/api/clipboard/__snapshots__/tableCellText.html
new file mode 100644
index 000000000..cd55158ac
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/tableCellText.html
@@ -0,0 +1 @@
+Table Cell
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/tableRow.html b/packages/core/src/api/clipboard/__snapshots__/tableRow.html
new file mode 100644
index 000000000..a7d0f18df
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/tableRow.html
@@ -0,0 +1 @@
+Table Cell | Table Cell |
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/__snapshots__/unstyledText.html b/packages/core/src/api/clipboard/__snapshots__/unstyledText.html
new file mode 100644
index 000000000..ea9503c08
--- /dev/null
+++ b/packages/core/src/api/clipboard/__snapshots__/unstyledText.html
@@ -0,0 +1 @@
+Regular
\ No newline at end of file
diff --git a/packages/core/src/api/clipboard/clipboard.test.ts b/packages/core/src/api/clipboard/clipboard.test.ts
new file mode 100644
index 000000000..58cf9e892
--- /dev/null
+++ b/packages/core/src/api/clipboard/clipboard.test.ts
@@ -0,0 +1,284 @@
+import { Node } from "prosemirror-model";
+import { NodeSelection, Selection, TextSelection } from "prosemirror-state";
+import { CellSelection } from "prosemirror-tables";
+import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
+
+import { PartialBlock } from "../../blocks/defaultBlocks";
+import { BlockNoteEditor } from "../../editor/BlockNoteEditor";
+import { doPaste } from "../testUtil/paste";
+import { initializeESMDependencies } from "../../util/esmDependencies";
+import { selectedFragmentToHTML } from "./toClipboard/copyExtension";
+
+type SelectionTestCase = {
+ testName: string;
+ createSelection: (doc: Node) => Selection;
+};
+
+// These tests are meant to test the copying of user selections in the editor.
+// The test cases used for the other HTML conversion tests are not suitable here
+// as they are represented in the BlockNote API, whereas here we want to test
+// ProseMirror/TipTap selections directly.
+describe("Test ProseMirror selection clipboard HTML", () => {
+ const initialContent: PartialBlock[] = [
+ {
+ type: "heading",
+ props: {
+ level: 2,
+ textColor: "red",
+ },
+ content: "Heading 1",
+ children: [
+ {
+ type: "paragraph",
+ content: "Nested Paragraph 1",
+ },
+ {
+ type: "paragraph",
+ content: "Nested Paragraph 2",
+ },
+ {
+ type: "paragraph",
+ content: "Nested Paragraph 3",
+ },
+ ],
+ },
+ {
+ type: "heading",
+ props: {
+ level: 2,
+ textColor: "red",
+ },
+ content: "Heading 2",
+ children: [
+ {
+ type: "paragraph",
+ content: "Nested Paragraph 1",
+ },
+ {
+ type: "paragraph",
+ content: "Nested Paragraph 2",
+ },
+ {
+ type: "paragraph",
+ content: "Nested Paragraph 3",
+ },
+ ],
+ },
+ {
+ type: "heading",
+ props: {
+ level: 2,
+ textColor: "red",
+ },
+ content: [
+ {
+ type: "text",
+ text: "Bold",
+ styles: {
+ bold: true,
+ },
+ },
+ {
+ type: "text",
+ text: "Italic",
+ styles: {
+ italic: true,
+ },
+ },
+ {
+ type: "text",
+ text: "Regular",
+ styles: {},
+ },
+ ],
+ children: [
+ {
+ type: "image",
+ props: {
+ url: "https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg",
+ },
+ children: [
+ {
+ type: "paragraph",
+ content: "Nested Paragraph",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: ["Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell"],
+ },
+ ],
+ },
+ // Not needed as selections starting in table cells will get snapped to
+ // the table boundaries.
+ // children: [
+ // {
+ // type: "table",
+ // content: {
+ // type: "tableContent",
+ // rows: [
+ // {
+ // cells: ["Table Cell", "Table Cell"],
+ // },
+ // {
+ // cells: ["Table Cell", "Table Cell"],
+ // },
+ // ],
+ // },
+ // },
+ // ],
+ },
+ ];
+
+ let editor: BlockNoteEditor;
+ const div = document.createElement("div");
+
+ beforeEach(() => {
+ editor.replaceBlocks(editor.document, initialContent);
+ });
+
+ beforeAll(async () => {
+ (window as any).__TEST_OPTIONS = (window as any).__TEST_OPTIONS || {};
+
+ editor = BlockNoteEditor.create();
+ editor.mount(div);
+
+ await initializeESMDependencies();
+ });
+
+ afterAll(() => {
+ editor.mount(undefined);
+ editor._tiptapEditor.destroy();
+ editor = undefined as any;
+
+ delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS;
+ });
+
+ // Sets the editor selection to the given start and end positions, then
+ // exports the selected content to HTML and compares it to a snapshot.
+ async function testSelection(testCase: SelectionTestCase) {
+ editor.dispatch(
+ editor._tiptapEditor.state.tr.setSelection(
+ testCase.createSelection(editor._tiptapEditor.view.state.doc)
+ )
+ );
+
+ const { clipboardHTML, externalHTML } = await selectedFragmentToHTML(
+ editor._tiptapEditor.view,
+ editor
+ );
+
+ expect(externalHTML).toMatchFileSnapshot(
+ `./__snapshots__/${testCase.testName}.html`
+ );
+
+ const originalDocument = editor.document;
+ doPaste(
+ editor._tiptapEditor.view,
+ "text",
+ clipboardHTML,
+ false,
+ new ClipboardEvent("paste")
+ );
+ const newDocument = editor.document;
+
+ expect(newDocument).toStrictEqual(originalDocument);
+ }
+
+ const testCases: SelectionTestCase[] = [
+ // TODO: Consider adding test cases for nested blocks & double nested blocks.
+ // Selection spans all of first heading's children.
+ {
+ testName: "multipleChildren",
+ createSelection: (doc) => TextSelection.create(doc, 16, 78),
+ },
+ // Selection spans from start of first heading to end of its first child.
+ {
+ testName: "childToParent",
+ createSelection: (doc) => TextSelection.create(doc, 3, 34),
+ },
+ // Selection spans from middle of first heading to the middle of its first
+ // child.
+ {
+ testName: "partialChildToParent",
+ createSelection: (doc) => TextSelection.create(doc, 6, 23),
+ },
+ // Selection spans from start of first heading's first child to end of
+ // second heading's content (does not include second heading's children).
+ {
+ testName: "childrenToNextParent",
+ createSelection: (doc) => TextSelection.create(doc, 16, 93),
+ },
+ // Selection spans from start of first heading's first child to end of
+ // second heading's last child.
+ {
+ testName: "childrenToNextParentsChildren",
+ createSelection: (doc) => TextSelection.create(doc, 16, 159),
+ },
+ // Selection spans "Regular" text inside third heading.
+ {
+ testName: "unstyledText",
+ createSelection: (doc) => TextSelection.create(doc, 175, 182),
+ },
+ // Selection spans "Italic" text inside third heading.
+ {
+ testName: "styledText",
+ createSelection: (doc) => TextSelection.create(doc, 169, 175),
+ },
+ // Selection spans third heading's content (does not include third heading's
+ // children).
+ {
+ testName: "multipleStyledText",
+ createSelection: (doc) => TextSelection.create(doc, 165, 182),
+ },
+ // Selection spans the image block content.
+ {
+ testName: "image",
+ createSelection: (doc) => NodeSelection.create(doc, 185),
+ },
+ // Selection spans from start of third heading to end of it's last
+ // descendant.
+ {
+ testName: "nestedImage",
+ createSelection: (doc) => TextSelection.create(doc, 165, 205),
+ },
+ // Selection spans text in first cell of the table.
+ {
+ testName: "tableCellText",
+ createSelection: (doc) => TextSelection.create(doc, 216, 226),
+ },
+ // Selection spans first cell of the table.
+ // TODO: External HTML is wrapped in unnecessary `tr` element.
+ {
+ testName: "tableCell",
+ createSelection: (doc) => CellSelection.create(doc, 214),
+ },
+ // Selection spans first row of the table.
+ {
+ testName: "tableRow",
+ createSelection: (doc) => CellSelection.create(doc, 214, 228),
+ },
+ // Selection spans all cells of the table.
+ // TODO: External HTML is wrapped in unnecessary `blockContent` element.
+ {
+ testName: "tableAllCells",
+ createSelection: (doc) => CellSelection.create(doc, 214, 258),
+ },
+ ];
+
+ for (const testCase of testCases) {
+ it(`${testCase.testName}`, async () => {
+ await testSelection(testCase);
+ });
+ }
+});
diff --git a/packages/core/src/api/parsers/acceptedMIMETypes.ts b/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
similarity index 100%
rename from packages/core/src/api/parsers/acceptedMIMETypes.ts
rename to packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
diff --git a/packages/core/src/api/parsers/fileDropExtension.ts b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts
similarity index 94%
rename from packages/core/src/api/parsers/fileDropExtension.ts
rename to packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts
index 9964b1b0c..657e77527 100644
--- a/packages/core/src/api/parsers/fileDropExtension.ts
+++ b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts
@@ -1,8 +1,8 @@
import { Extension } from "@tiptap/core";
import { Plugin } from "prosemirror-state";
-import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
-import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
import { handleFileInsertion } from "./handleFileInsertion";
import { acceptedMIMETypes } from "./acceptedMIMETypes";
diff --git a/packages/core/src/api/parsers/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts
similarity index 94%
rename from packages/core/src/api/parsers/handleFileInsertion.ts
rename to packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts
index b68bc4e0d..d060c0656 100644
--- a/packages/core/src/api/parsers/handleFileInsertion.ts
+++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts
@@ -1,12 +1,12 @@
-import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
-import { PartialBlock } from "../../blocks/defaultBlocks";
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import { PartialBlock } from "../../../blocks/defaultBlocks";
import {
BlockSchema,
FileBlockConfig,
InlineContentSchema,
StyleSchema,
-} from "../../schema";
-import { getBlockInfoFromPos } from "../getBlockInfoFromPos";
+} from "../../../schema";
+import { getBlockInfoFromPos } from "../../getBlockInfoFromPos";
import { acceptedMIMETypes } from "./acceptedMIMETypes";
function checkFileExtensionsMatch(
diff --git a/packages/core/src/api/parsers/pasteExtension.ts b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
similarity index 76%
rename from packages/core/src/api/parsers/pasteExtension.ts
rename to packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
index 97cfd751c..8b9161b89 100644
--- a/packages/core/src/api/parsers/pasteExtension.ts
+++ b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
@@ -1,11 +1,11 @@
import { Extension } from "@tiptap/core";
import { Plugin } from "prosemirror-state";
-import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
-import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
-import { handleFileInsertion } from "./handleFileInsertion";
-import { nestedListsToBlockNoteStructure } from "./html/util/nestedLists";
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
+import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists";
import { acceptedMIMETypes } from "./acceptedMIMETypes";
+import { handleFileInsertion } from "./handleFileInsertion";
export const createPasteFromClipboardExtension = <
BSchema extends BlockSchema,
@@ -28,14 +28,14 @@ export const createPasteFromClipboardExtension = <
return;
}
- let format: (typeof acceptedMIMETypes)[number] | null = null;
+ let format: (typeof acceptedMIMETypes)[number] | undefined;
for (const mimeType of acceptedMIMETypes) {
if (event.clipboardData!.types.includes(mimeType)) {
format = mimeType;
break;
}
}
- if (format === null) {
+ if (!format) {
return true;
}
@@ -46,12 +46,19 @@ export const createPasteFromClipboardExtension = <
let data = event.clipboardData!.getData(format);
+ if (format === "blocknote/html") {
+ editor._tiptapEditor.view.pasteHTML(data);
+ return true;
+ }
+
if (format === "text/html") {
const htmlNode = nestedListsToBlockNoteStructure(data.trim());
data = htmlNode.innerHTML;
+ editor._tiptapEditor.view.pasteHTML(data);
+ return true;
}
- editor._tiptapEditor.view.pasteHTML(data);
+ editor._tiptapEditor.view.pasteText(data);
return true;
},
diff --git a/packages/core/src/api/exporters/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
similarity index 65%
rename from packages/core/src/api/exporters/copyExtension.ts
rename to packages/core/src/api/clipboard/toClipboard/copyExtension.ts
index 8a168387a..ed3446ac3 100644
--- a/packages/core/src/api/exporters/copyExtension.ts
+++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
@@ -1,16 +1,17 @@
import { Extension } from "@tiptap/core";
import { Node } from "prosemirror-model";
import { NodeSelection, Plugin } from "prosemirror-state";
+import { CellSelection } from "prosemirror-tables";
+import * as pmView from "prosemirror-view";
import { EditorView } from "prosemirror-view";
-import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
-import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
-import { initializeESMDependencies } from "../../util/esmDependencies";
-import { createExternalHTMLExporter } from "./html/externalHTMLExporter";
-import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer";
-import { cleanHTMLToMarkdown } from "./markdown/markdownExporter";
-
-async function selectedFragmentToHTML<
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema";
+import { initializeESMDependencies } from "../../../util/esmDependencies";
+import { createExternalHTMLExporter } from "../../exporters/html/externalHTMLExporter";
+import { cleanHTMLToMarkdown } from "../../exporters/markdown/markdownExporter";
+
+export async function selectedFragmentToHTML<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
@@ -18,20 +19,61 @@ async function selectedFragmentToHTML<
view: EditorView,
editor: BlockNoteEditor
): Promise<{
- internalHTML: string;
+ clipboardHTML: string;
externalHTML: string;
- plainText: string;
+ markdown: string;
}> {
- const selectedFragment = view.state.selection.content().content;
+ // Checks if a `blockContent` node is being copied and expands
+ // the selection to the parent `blockContainer` node. This is
+ // for the use-case in which only a block without content is
+ // selected, e.g. an image block.
+ if (
+ "node" in view.state.selection &&
+ (view.state.selection.node as Node).type.spec.group === "blockContent"
+ ) {
+ editor.dispatch(
+ editor._tiptapEditor.state.tr.setSelection(
+ new NodeSelection(view.state.doc.resolve(view.state.selection.from - 1))
+ )
+ );
+ }
- const internalHTMLSerializer = createInternalHTMLSerializer(
- view.state.schema,
- editor
- );
- const internalHTML = internalHTMLSerializer.serializeProseMirrorFragment(
- selectedFragment,
- {}
- );
+ // Uses default ProseMirror clipboard serialization.
+ const clipboardHTML: string = (pmView as any).__serializeForClipboard(
+ view,
+ view.state.selection.content()
+ ).dom.innerHTML;
+
+ let selectedFragment = view.state.selection.content().content;
+
+ // Checks whether block ancestry should be included when creating external
+ // HTML. If the selection is within a block content node, the block ancestry
+ // is excluded as we only care about the inline content.
+ let isWithinBlockContent = false;
+ const isWithinTable = view.state.selection instanceof CellSelection;
+ if (!isWithinTable) {
+ const fragmentWithoutParents = view.state.doc.slice(
+ view.state.selection.from,
+ view.state.selection.to,
+ false
+ ).content;
+
+ const children = [];
+ for (let i = 0; i < fragmentWithoutParents.childCount; i++) {
+ children.push(fragmentWithoutParents.child(i));
+ }
+
+ isWithinBlockContent =
+ children.find(
+ (child) =>
+ child.type.name === "blockContainer" ||
+ child.type.name === "blockGroup" ||
+ child.type.spec.group === "blockContent"
+ ) === undefined;
+ if (isWithinBlockContent) {
+ selectedFragment = fragmentWithoutParents;
+ }
+ }
await initializeESMDependencies();
const externalHTMLExporter = createExternalHTMLExporter(
@@ -40,12 +82,12 @@ async function selectedFragmentToHTML<
);
const externalHTML = externalHTMLExporter.exportProseMirrorFragment(
selectedFragment,
- {}
+ { simplifyBlocks: !isWithinBlockContent && !isWithinTable }
);
- const plainText = await cleanHTMLToMarkdown(externalHTML);
+ const markdown = cleanHTMLToMarkdown(externalHTML);
- return { internalHTML, externalHTML, plainText };
+ return { clipboardHTML, externalHTML, markdown };
}
const copyToClipboard = <
@@ -61,30 +103,15 @@ const copyToClipboard = <
event.preventDefault();
event.clipboardData!.clearData();
- // Checks if a `blockContent` node is being copied and expands
- // the selection to the parent `blockContainer` node. This is
- // for the use-case in which only a block without content is
- // selected, e.g. an image block.
- if (
- "node" in view.state.selection &&
- (view.state.selection.node as Node).type.spec.group === "blockContent"
- ) {
- editor.dispatch(
- editor._tiptapEditor.state.tr.setSelection(
- new NodeSelection(view.state.doc.resolve(view.state.selection.from - 1))
- )
- );
- }
-
(async () => {
- const { internalHTML, externalHTML, plainText } =
+ const { clipboardHTML, externalHTML, markdown } =
await selectedFragmentToHTML(view, editor);
// TODO: Writing to other MIME types not working in Safari for
// some reason.
- event.clipboardData!.setData("blocknote/html", internalHTML);
+ event.clipboardData!.setData("blocknote/html", clipboardHTML);
event.clipboardData!.setData("text/html", externalHTML);
- event.clipboardData!.setData("text/plain", plainText);
+ event.clipboardData!.setData("text/plain", markdown);
})();
};
@@ -144,14 +171,14 @@ export const createCopyToClipboardExtension = <
event.dataTransfer!.clearData();
(async () => {
- const { internalHTML, externalHTML, plainText } =
+ const { clipboardHTML, externalHTML, markdown } =
await selectedFragmentToHTML(view, editor);
// TODO: Writing to other MIME types not working in Safari for
// some reason.
- event.dataTransfer!.setData("blocknote/html", internalHTML);
+ event.dataTransfer!.setData("blocknote/html", clipboardHTML);
event.dataTransfer!.setData("text/html", externalHTML);
- event.dataTransfer!.setData("text/plain", plainText);
+ event.dataTransfer!.setData("text/plain", markdown);
})();
// Prevent default PM handler to be called
return true;
diff --git a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html b/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html
deleted file mode 100644
index 5bad4edb4..000000000
--- a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html
+++ /dev/null
@@ -1 +0,0 @@
-Nested Paragraph 1
Nested Paragraph 2
Nested Paragraph 3
Paragraph 2
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html b/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html
deleted file mode 100644
index 5a9420782..000000000
--- a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html
+++ /dev/null
@@ -1 +0,0 @@
-Nested Paragraph 1
Nested Paragraph 2
Nested Paragraph 3
Paragraph 2
Nested Paragraph 1
Nested Paragraph 2
Nested Paragraph 3
\ No newline at end of file
diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
index beb81487c..2d6a859d9 100644
--- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts
+++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
@@ -41,7 +41,7 @@ export interface ExternalHTMLExporter<
) => string;
exportProseMirrorFragment: (
fragment: Fragment,
- options: { document?: Document }
+ options: { document?: Document; simplifyBlocks?: boolean }
) => string;
}
@@ -63,14 +63,18 @@ export const createExternalHTMLExporter = <
);
}
- const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & {
+ // TODO: maybe cache this serializer (default prosemirror serializer is cached)?
+ const serializer = new DOMSerializer(
+ DOMSerializer.nodesFromSchema(schema),
+ DOMSerializer.marksFromSchema(schema)
+ ) as DOMSerializer & {
serializeNodeInner: (
node: Node,
options: { document?: Document }
) => HTMLElement;
exportProseMirrorFragment: (
fragment: Fragment,
- options: { document?: Document }
+ options: { document?: Document; simplifyBlocks?: boolean }
) => string;
exportBlocks: (
blocks: PartialBlock[],
@@ -87,16 +91,19 @@ export const createExternalHTMLExporter = <
// but additionally runs it through the `simplifyBlocks` rehype plugin to
// convert the internal HTML to external.
serializer.exportProseMirrorFragment = (fragment, options) => {
- const externalHTML = deps.unified
+ let externalHTML: any = deps.unified
.unified()
- .use(deps.rehypeParse.default, { fragment: true })
- .use(simplifyBlocks, {
+ .use(deps.rehypeParse.default, { fragment: true });
+ if (options.simplifyBlocks !== false) {
+ externalHTML = externalHTML.use(simplifyBlocks, {
orderedListItemBlockTypes: new Set(["numberedListItem"]),
unorderedListItemBlockTypes: new Set([
"bulletListItem",
"checkListItem",
]),
- })
+ });
+ }
+ externalHTML = externalHTML
.use(deps.rehypeStringify.default)
.processSync(serializeProseMirrorFragment(fragment, serializer, options));
diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts
index a08367f00..850158902 100644
--- a/packages/core/src/api/exporters/html/htmlConversion.test.ts
+++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts
@@ -1,12 +1,11 @@
-import { TextSelection } from "prosemirror-state";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
-import { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
import { addIdsToBlocks, partialBlocksToBlocksForTesting } from "../../..";
import { PartialBlock } from "../../../blocks/defaultBlocks";
-import { BlockSchema } from "../../../schema/blocks/types";
-import { InlineContentSchema } from "../../../schema/inlineContent/types";
-import { StyleSchema } from "../../../schema/styles/types";
+import { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import { BlockSchema } from "../../../schema";
+import { InlineContentSchema } from "../../../schema";
+import { StyleSchema } from "../../../schema";
import { initializeESMDependencies } from "../../../util/esmDependencies";
import { customBlocksTestCases } from "../../testUtil/cases/customBlocks";
import { customInlineContentTestCases } from "../../testUtil/cases/customInlineContent";
@@ -106,145 +105,3 @@ describe("Test HTML conversion", () => {
});
}
});
-
-// Fragments created from ProseMirror selections don't always conform to the
-// schema. This is because ProseMirror preserves the full ancestry of selected
-// nodes, but not the siblings of ancestor nodes. These tests are to verify that
-// Fragments like this are exported to HTML properly, as they can't be created
-// from Block objects like all the other test cases (Block object conversions
-// always conform to the schema).
-describe("Test ProseMirror fragment edge case conversion", () => {
- let editor: BlockNoteEditor;
- const div = document.createElement("div");
- beforeEach(() => {
- editor = BlockNoteEditor.create();
- editor.mount(div);
- });
-
- afterEach(() => {
- editor.mount(undefined);
- editor._tiptapEditor.destroy();
- editor = undefined as any;
-
- delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS;
- });
-
- // When the selection starts in a nested block, the Fragment from it omits the
- // `blockContent` node of the parent `blockContainer` if it's not also
- // included in the selection. In the schema, `blockContainer` nodes should
- // contain a single `blockContent` node, so this edge case needs to be tested.
- describe("No block content", () => {
- const blocks: PartialBlock[] = [
- {
- type: "paragraph",
- content: "Paragraph 1",
- children: [
- {
- type: "paragraph",
- content: "Nested Paragraph 1",
- },
- {
- type: "paragraph",
- content: "Nested Paragraph 2",
- },
- {
- type: "paragraph",
- content: "Nested Paragraph 3",
- },
- ],
- },
- {
- type: "paragraph",
- content: "Paragraph 2",
- children: [
- {
- type: "paragraph",
- content: "Nested Paragraph 1",
- },
- {
- type: "paragraph",
- content: "Nested Paragraph 2",
- },
- {
- type: "paragraph",
- content: "Nested Paragraph 3",
- },
- ],
- },
- ];
-
- beforeEach(() => {
- editor.replaceBlocks(editor.document, blocks);
- });
-
- it("Selection within a block's children", async () => {
- // Selection starts and ends within the first block's children.
- editor.dispatch(
- editor._tiptapEditor.state.tr.setSelection(
- TextSelection.create(editor._tiptapEditor.state.doc, 18, 80)
- )
- );
-
- const copiedFragment =
- editor._tiptapEditor.state.selection.content().content;
-
- await initializeESMDependencies();
- const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
- const externalHTML = exporter.exportProseMirrorFragment(
- copiedFragment,
- {}
- );
- expect(externalHTML).toMatchFileSnapshot(
- "./__snapshots_fragment_edge_cases__/" +
- "selectionWithinBlockChildren.html"
- );
- });
-
- it("Selection leaves a block's children", async () => {
- // Selection starts and ends within the first block's children and ends
- // outside, at a shallower nesting level in the second block.
- editor.dispatch(
- editor._tiptapEditor.state.tr.setSelection(
- TextSelection.create(editor._tiptapEditor.state.doc, 18, 97)
- )
- );
-
- const copiedFragment =
- editor._tiptapEditor.state.selection.content().content;
-
- await initializeESMDependencies();
- const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
- const externalHTML = exporter.exportProseMirrorFragment(
- copiedFragment,
- {}
- );
- expect(externalHTML).toMatchFileSnapshot(
- "./__snapshots_fragment_edge_cases__/" +
- "selectionLeavesBlockChildren.html"
- );
- });
-
- it("Selection spans multiple blocks' children", async () => {
- // Selection starts and ends within the first block's children and ends
- // within the second block's children.
- editor.dispatch(
- editor._tiptapEditor.state.tr.setSelection(
- TextSelection.create(editor._tiptapEditor.state.doc, 18, 163)
- )
- );
-
- const copiedFragment =
- editor._tiptapEditor.state.selection.content().content;
- await initializeESMDependencies();
- const exporter = createExternalHTMLExporter(editor.pmSchema, editor);
- const externalHTML = exporter.exportProseMirrorFragment(
- copiedFragment,
- {}
- );
- expect(externalHTML).toMatchFileSnapshot(
- "./__snapshots_fragment_edge_cases__/" +
- "selectionSpansBlocksChildren.html"
- );
- });
- });
-});
diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
index d1357774b..923319067 100644
--- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
+++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
@@ -7,7 +7,6 @@ import {
serializeNodeInner,
serializeProseMirrorFragment,
} from "./util/sharedHTMLConversion";
-
// Used to serialize BlockNote blocks and ProseMirror nodes to HTML without
// losing data. Blocks are exported using the `toInternalHTML` method in their
// `blockSpec`.
@@ -48,7 +47,11 @@ export const createInternalHTMLSerializer = <
schema: Schema,
editor: BlockNoteEditor
): InternalHTMLSerializer => {
- const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & {
+ // TODO: maybe cache this serializer (default prosemirror serializer is cached)?
+ const serializer = new DOMSerializer(
+ DOMSerializer.nodesFromSchema(schema),
+ DOMSerializer.marksFromSchema(schema)
+ ) as DOMSerializer & {
serializeNodeInner: (
node: Node,
options: { document?: Document }
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json b/packages/core/src/api/parsers/html/__snapshots__/list-test.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json
rename to packages/core/src/api/parsers/html/__snapshots__/list-test.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json b/packages/core/src/api/parsers/html/__snapshots__/parse-basic-block-types.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-basic-block-types.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json b/packages/core/src/api/parsers/html/__snapshots__/parse-div-with-inline-content.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-div-with-inline-content.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json b/packages/core/src/api/parsers/html/__snapshots__/parse-divs.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-divs.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json b/packages/core/src/api/parsers/html/__snapshots__/parse-fake-image-caption.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-fake-image-caption.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-image-in-paragraph.json b/packages/core/src/api/parsers/html/__snapshots__/parse-image-in-paragraph.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-image-in-paragraph.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-image-in-paragraph.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/parse-mixed-nested-lists.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-mixed-nested-lists.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json b/packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists-with-paragraphs.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists-with-paragraphs.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-notion-html.json b/packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-notion-html.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json b/packages/core/src/api/parsers/html/__snapshots__/parse-two-divs.json
similarity index 100%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json
rename to packages/core/src/api/parsers/html/__snapshots__/parse-two-divs.json
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json
deleted file mode 100644
index c45e54ef9..000000000
--- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json
+++ /dev/null
@@ -1,476 +0,0 @@
-[
- {
- "id": "1",
- "type": "heading",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left",
- "level": 1
- },
- "content": [
- {
- "type": "text",
- "text": "Heading 1",
- "styles": {
- "bold": true
- }
- }
- ],
- "children": []
- },
- {
- "id": "2",
- "type": "heading",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left",
- "level": 2
- },
- "content": [
- {
- "type": "text",
- "text": "Heading 2",
- "styles": {
- "bold": true
- }
- }
- ],
- "children": []
- },
- {
- "id": "3",
- "type": "heading",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left",
- "level": 3
- },
- "content": [
- {
- "type": "text",
- "text": "Heading 3",
- "styles": {
- "bold": true
- }
- }
- ],
- "children": []
- },
- {
- "id": "4",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Paragraph 1",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "5",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Paragraph 2",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "6",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Paragraph 3",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "7",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Paragraph With \nHard Break",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "8",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Bold",
- "styles": {
- "bold": true
- }
- },
- {
- "type": "text",
- "text": " ",
- "styles": {}
- },
- {
- "type": "text",
- "text": "Italic",
- "styles": {
- "italic": true
- }
- },
- {
- "type": "text",
- "text": " Underline ",
- "styles": {}
- },
- {
- "type": "text",
- "text": "Strikethrough",
- "styles": {
- "strike": true
- }
- },
- {
- "type": "text",
- "text": " ",
- "styles": {}
- },
- {
- "type": "text",
- "text": "All",
- "styles": {
- "bold": true,
- "italic": true,
- "strike": true
- }
- }
- ],
- "children": []
- },
- {
- "id": "9",
- "type": "bulletListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Bullet List Item 1",
- "styles": {}
- }
- ],
- "children": [
- {
- "id": "10",
- "type": "bulletListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Nested Bullet List Item 1",
- "styles": {}
- }
- ],
- "children": [
- {
- "id": "11",
- "type": "numberedListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Nested Numbered List Item 1",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "12",
- "type": "numberedListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Nested Numbered List Item 2",
- "styles": {}
- }
- ],
- "children": []
- }
- ]
- },
- {
- "id": "13",
- "type": "bulletListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Nested Bullet List Item 2",
- "styles": {}
- }
- ],
- "children": []
- }
- ]
- },
- {
- "id": "14",
- "type": "bulletListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Bullet List Item 2",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "15",
- "type": "numberedListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Numbered List Item 1",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "16",
- "type": "numberedListItem",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Numbered List Item 2",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "17",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [],
- "children": []
- },
- {
- "id": "18",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "\n",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "19",
- "type": "table",
- "props": {
- "textColor": "default",
- "backgroundColor": "default"
- },
- "content": {
- "type": "tableContent",
- "rows": [
- {
- "cells": [
- [
- {
- "type": "text",
- "text": "Cell 1",
- "styles": {}
- }
- ],
- [
- {
- "type": "text",
- "text": "Cell 2",
- "styles": {}
- }
- ],
- [
- {
- "type": "text",
- "text": "Cell 3",
- "styles": {}
- }
- ]
- ]
- },
- {
- "cells": [
- [
- {
- "type": "text",
- "text": "Cell 4",
- "styles": {}
- }
- ],
- [
- {
- "type": "text",
- "text": "Cell 5",
- "styles": {}
- }
- ],
- [
- {
- "type": "text",
- "text": "Cell 6",
- "styles": {}
- }
- ]
- ]
- },
- {
- "cells": [
- [
- {
- "type": "text",
- "text": "Cell 7",
- "styles": {}
- }
- ],
- [
- {
- "type": "text",
- "text": "Cell 8",
- "styles": {}
- }
- ],
- [
- {
- "type": "text",
- "text": "Cell 9",
- "styles": {}
- }
- ]
- ]
- }
- ]
- },
- "children": []
- },
- {
- "id": "20",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "Paragraph",
- "styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "21",
- "type": "paragraph",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left"
- },
- "content": [
- {
- "type": "text",
- "text": "\n",
- "styles": {}
- }
- ],
- "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 06ff826ac..389d1b745 100644
--- a/packages/core/src/api/parsers/html/parseHTML.test.ts
+++ b/packages/core/src/api/parsers/html/parseHTML.test.ts
@@ -1,3 +1,4 @@
+import * as pmView from "prosemirror-view";
import { describe, expect, it } from "vitest";
import { BlockNoteEditor } from "../../..";
import { nestedListsToBlockNoteStructure } from "./util/nestedLists";
@@ -6,16 +7,12 @@ async function parseHTMLAndCompareSnapshots(
html: string,
snapshotName: string
) {
- // use a dynamic import because we want to access
- // __parseFromClipboard which is not exposed in types
- const view: any = await import("prosemirror-view");
-
const editor = BlockNoteEditor.create();
const div = document.createElement("div");
editor.mount(div);
const blocks = await editor.tryParseHTMLToBlocks(html);
- const snapshotPath = "./__snapshots__/paste/" + snapshotName + ".json";
+ const snapshotPath = "./__snapshots__/" + snapshotName + ".json";
expect(JSON.stringify(blocks, undefined, 2)).toMatchFileSnapshot(
snapshotPath
);
@@ -34,7 +31,7 @@ async function parseHTMLAndCompareSnapshots(
(window as any).__TEST_OPTIONS.mockID = 0; // reset id counter
const htmlNode = nestedListsToBlockNoteStructure(html);
- const slice = view.__parseFromClipboard(
+ const slice = (pmView as any).__parseFromClipboard(
editor.prosemirrorView,
"",
htmlNode.innerHTML,
diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json
new file mode 100644
index 000000000..e0d619a9e
--- /dev/null
+++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json
@@ -0,0 +1,319 @@
+[
+ {
+ "id": "19",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "# Heading 1",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "20",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "## Heading 2",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "21",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "### Heading 3",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "22",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Paragraph",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "23",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "P**ara***grap*h",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "24",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "P*ara*~~grap~~h",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "25",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "* Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "26",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "* Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "27",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " * Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "28",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " * Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "29",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " Paragraph",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "30",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " 1. Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "31",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " 2. Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "32",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " 3. Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "33",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " 1. Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "34",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " * Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "35",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " * Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "36",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "* Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "37",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [],
+ "children": []
+ }
+]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json
new file mode 100644
index 000000000..3c17433c1
--- /dev/null
+++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json
@@ -0,0 +1,81 @@
+[
+ {
+ "id": "5",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "- 📝 item1",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "6",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "- ⚙️ item2",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "7",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "- 🔗 item3",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "8",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "# h1",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "9",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [],
+ "children": []
+ }
+]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json
similarity index 51%
rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json
rename to packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json
index ae11e36cb..3ee19cf3e 100644
--- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json
+++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json
@@ -1,6 +1,6 @@
[
{
- "id": "1",
+ "id": "9",
"type": "paragraph",
"props": {
"textColor": "default",
@@ -10,14 +10,14 @@
"content": [
{
"type": "text",
- "text": "Outer 1 Div Before",
+ "text": "* a",
"styles": {}
}
],
"children": []
},
{
- "id": "2",
+ "id": "10",
"type": "paragraph",
"props": {
"textColor": "default",
@@ -27,14 +27,14 @@
"content": [
{
"type": "text",
- "text": " Outer 2 Div Before",
+ "text": "* b",
"styles": {}
}
],
"children": []
},
{
- "id": "3",
+ "id": "11",
"type": "paragraph",
"props": {
"textColor": "default",
@@ -44,14 +44,14 @@
"content": [
{
"type": "text",
- "text": " Outer 3 Div Before",
+ "text": "* c",
"styles": {}
}
],
"children": []
},
{
- "id": "4",
+ "id": "12",
"type": "paragraph",
"props": {
"textColor": "default",
@@ -61,68 +61,64 @@
"content": [
{
"type": "text",
- "text": " Outer 4 Div Before",
+ "text": "* d",
"styles": {}
}
],
"children": []
},
{
- "id": "5",
- "type": "heading",
+ "id": "13",
+ "type": "paragraph",
"props": {
"textColor": "default",
"backgroundColor": "default",
- "textAlignment": "left",
- "level": 1
+ "textAlignment": "left"
},
"content": [
{
"type": "text",
- "text": "Heading 1",
+ "text": "anything",
"styles": {}
}
],
"children": []
},
{
- "id": "6",
- "type": "heading",
+ "id": "14",
+ "type": "paragraph",
"props": {
"textColor": "default",
"backgroundColor": "default",
- "textAlignment": "left",
- "level": 2
+ "textAlignment": "left"
},
"content": [
{
"type": "text",
- "text": "Heading 2",
+ "text": "[a link](",
"styles": {}
- }
- ],
- "children": []
- },
- {
- "id": "7",
- "type": "heading",
- "props": {
- "textColor": "default",
- "backgroundColor": "default",
- "textAlignment": "left",
- "level": 3
- },
- "content": [
+ },
+ {
+ "type": "link",
+ "href": "http://example.com",
+ "content": [
+ {
+ "type": "text",
+ "text": "http://example.com",
+ "styles": {}
+ }
+ ]
+ },
{
"type": "text",
- "text": "Heading 3",
+ "text": ")",
"styles": {}
}
],
"children": []
},
{
- "id": "8",
+ "id": "15",
"type": "paragraph",
"props": {
"textColor": "default",
@@ -132,26 +128,14 @@
"content": [
{
"type": "text",
- "text": "Paragraph",
+ "text": "* another",
"styles": {}
}
],
"children": []
},
{
- "id": "9",
- "type": "image",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "url": "exampleURL",
- "caption": "Image Caption",
- "width": 512
- },
- "children": []
- },
- {
- "id": "10",
+ "id": "16",
"type": "paragraph",
"props": {
"textColor": "default",
@@ -161,80 +145,21 @@
"content": [
{
"type": "text",
- "text": "Bold",
- "styles": {
- "bold": true
- }
- },
- {
- "type": "text",
- "text": " ",
+ "text": "* list",
"styles": {}
- },
- {
- "type": "text",
- "text": "Italic",
- "styles": {
- "italic": true
- }
- },
- {
- "type": "text",
- "text": " ",
- "styles": {}
- },
- {
- "type": "text",
- "text": "Underline",
- "styles": {
- "underline": true
- }
- },
- {
- "type": "text",
- "text": " ",
- "styles": {}
- },
- {
- "type": "text",
- "text": "Strikethrough",
- "styles": {
- "strike": true
- }
- },
- {
- "type": "text",
- "text": " ",
- "styles": {}
- },
- {
- "type": "text",
- "text": "All",
- "styles": {
- "bold": true,
- "italic": true,
- "underline": true,
- "strike": true
- }
}
],
"children": []
},
{
- "id": "11",
+ "id": "17",
"type": "paragraph",
"props": {
"textColor": "default",
"backgroundColor": "default",
"textAlignment": "left"
},
- "content": [
- {
- "type": "text",
- "text": " Outer 4 Div After Outer 3 Div After Outer 2 Div After Outer 1 Div After",
- "styles": {}
- }
- ],
+ "content": [],
"children": []
}
]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json
new file mode 100644
index 000000000..84e352628
--- /dev/null
+++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json
@@ -0,0 +1,81 @@
+[
+ {
+ "id": "5",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "# Heading",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "6",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Paragraph",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "7",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "* Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "8",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": " 1. Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "9",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [],
+ "children": []
+ }
+]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json
new file mode 100644
index 000000000..f72ab28fa
--- /dev/null
+++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json
@@ -0,0 +1,81 @@
+[
+ {
+ "id": "5",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "# Heading",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "6",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Paragraph",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "7",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "* Bullet List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "8",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "1. Numbered List Item",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "9",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [],
+ "children": []
+ }
+]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json
new file mode 100644
index 000000000..5d3c67820
--- /dev/null
+++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json
@@ -0,0 +1,61 @@
+[
+ {
+ "id": "2",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [
+ {
+ "type": "text",
+ "text": "Bold",
+ "styles": {
+ "bold": true
+ }
+ },
+ {
+ "type": "text",
+ "text": " ",
+ "styles": {}
+ },
+ {
+ "type": "text",
+ "text": "Italic",
+ "styles": {
+ "italic": true
+ }
+ },
+ {
+ "type": "text",
+ "text": " ",
+ "styles": {}
+ },
+ {
+ "type": "text",
+ "text": "Strikethrough",
+ "styles": {
+ "strike": true
+ }
+ },
+ {
+ "type": "text",
+ "text": " ***Multiple***",
+ "styles": {}
+ }
+ ],
+ "children": []
+ },
+ {
+ "id": "3",
+ "type": "paragraph",
+ "props": {
+ "textColor": "default",
+ "backgroundColor": "default",
+ "textAlignment": "left"
+ },
+ "content": [],
+ "children": []
+ }
+]
\ No newline at end of file
diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts
index b8ef953af..16e12b0dc 100644
--- a/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts
+++ b/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { BlockNoteEditor } from "../../..";
+import { doPaste } from "../../testUtil/paste";
async function parseMarkdownAndCompareSnapshots(
md: string,
@@ -14,6 +15,20 @@ async function parseMarkdownAndCompareSnapshots(
expect(JSON.stringify(blocks, undefined, 2)).toMatchFileSnapshot(
snapshotPath
);
+
+ doPaste(
+ editor._tiptapEditor.view,
+ md,
+ null,
+ true,
+ new ClipboardEvent("paste")
+ );
+
+ const pastedSnapshotPath = "./__snapshots__/pasted/" + snapshotName + ".json";
+ expect(JSON.stringify(editor.document, undefined, 2)).toMatchFileSnapshot(
+ pastedSnapshotPath
+ );
+
editor.mount(undefined);
}
diff --git a/packages/core/src/api/testUtil/paste.ts b/packages/core/src/api/testUtil/paste.ts
new file mode 100644
index 000000000..a25874313
--- /dev/null
+++ b/packages/core/src/api/testUtil/paste.ts
@@ -0,0 +1,46 @@
+import { Slice } from "@tiptap/pm/model";
+import { EditorView } from "@tiptap/pm/view";
+import * as pmView from "@tiptap/pm/view";
+
+function sliceSingleNode(slice: Slice) {
+ return slice.openStart === 0 &&
+ slice.openEnd === 0 &&
+ slice.content.childCount === 1
+ ? slice.content.firstChild
+ : null;
+}
+
+// This function is a copy of the `doPaste` function from `@tiptap/pm/view`,
+// but made to work in a JSDOM environment. To do this, the `tr.scrollIntoView`
+// call has been removed.
+// https://github.com/ProseMirror/prosemirror-view/blob/17b508f618c944c54776f8ddac45edcb49970796/src/input.ts#L624
+export function doPaste(
+ view: EditorView,
+ text: string,
+ html: string | null,
+ preferPlain: boolean,
+ event: ClipboardEvent
+) {
+ const slice = (pmView as any).__parseFromClipboard(
+ view,
+ text,
+ html,
+ preferPlain,
+ view.state.selection.$from
+ );
+ if (
+ view.someProp("handlePaste", (f) => f(view, event, slice || Slice.empty))
+ ) {
+ return true;
+ }
+ if (!slice) {
+ return false;
+ }
+
+ const singleNode = sliceSingleNode(slice);
+ const tr = singleNode
+ ? view.state.tr.replaceSelectionWith(singleNode, preferPlain)
+ : view.state.tr.replaceSelection(slice);
+ view.dispatch(tr.setMeta("paste", true).setMeta("uiEvent", "paste"));
+ return true;
+}
diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts
index e47a62b2d..1e45066cf 100644
--- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts
+++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts
@@ -46,7 +46,6 @@ const TableParagraph = Node.create({
parseHTML() {
return [
- { tag: "td" },
{
tag: "p",
getAttrs: (element) => {
diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts
index fca81fb5a..e03163e53 100644
--- a/packages/core/src/editor/BlockNoteExtensions.ts
+++ b/packages/core/src/editor/BlockNoteExtensions.ts
@@ -11,9 +11,9 @@ import { History } from "@tiptap/extension-history";
import { Link } from "@tiptap/extension-link";
import { Text } from "@tiptap/extension-text";
import * as Y from "yjs";
-import { createCopyToClipboardExtension } from "../api/exporters/copyExtension";
-import { createDropFileExtension } from "../api/parsers/fileDropExtension";
-import { createPasteFromClipboardExtension } from "../api/parsers/pasteExtension";
+import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension";
+import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension";
+import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension";
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension";
import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension";
import { TextColorExtension } from "../extensions/TextColor/TextColorExtension";
diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts
index cea35c8e0..334a1aab8 100644
--- a/packages/core/src/editor/transformPasted.ts
+++ b/packages/core/src/editor/transformPasted.ts
@@ -1,4 +1,4 @@
-import { Fragment, Slice } from "@tiptap/pm/model";
+import { Fragment, Schema, Slice } from "@tiptap/pm/model";
import { EditorView } from "@tiptap/pm/view";
// helper function to remove a child from a fragment
@@ -12,6 +12,37 @@ function removeChild(node: Fragment, n: number) {
return Fragment.from(children);
}
+/**
+ * Wrap adjacent tableRow items in a table.
+ *
+ * This makes sure the content that we paste is always a table (and not a tableRow)
+ * A table works better for the remaing paste handling logic, as it's actually a blockContent node
+ */
+export function wrapTableRows(f: Fragment, schema: Schema) {
+ const newItems: any[] = [];
+ for (let i = 0; i < f.childCount; i++) {
+ if (f.child(i).type.name === "tableRow") {
+ if (
+ newItems.length > 0 &&
+ newItems[newItems.length - 1].type.name === "table"
+ ) {
+ // append to existing table
+ const prevTable = newItems[newItems.length - 1];
+ const newTable = prevTable.copy(prevTable.content.addToEnd(f.child(i)));
+ newItems[newItems.length - 1] = newTable;
+ } else {
+ // create new table to wrap tableRow with
+ const newTable = schema.nodes.table.create(undefined, f.child(i));
+ newItems.push(newTable);
+ }
+ } else {
+ newItems.push(f.child(i));
+ }
+ }
+ f = Fragment.from(newItems);
+ return f;
+}
+
/**
* fix for https://github.com/ProseMirror/prosemirror/issues/1430#issuecomment-1822570821
*
@@ -23,6 +54,8 @@ function removeChild(node: Fragment, n: number) {
*/
export function transformPasted(slice: Slice, view: EditorView) {
let f = Fragment.from(slice.content);
+ f = wrapTableRows(f, view.state.schema);
+
for (let i = 0; i < f.childCount; i++) {
if (f.child(i).type.spec.group === "blockContent") {
const content = [f.child(i)];
@@ -54,6 +87,5 @@ export function transformPasted(slice: Slice, view: EditorView) {
f = f.replaceChild(i, container);
}
}
-
return new Slice(f, slice.openStart, slice.openEnd);
}
diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
index 6bc5cc7cd..bc7b310b5 100644
--- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
+++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
@@ -2,9 +2,9 @@ import { PluginView } from "@tiptap/pm/state";
import { Node } from "prosemirror-model";
import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
+import * as pmView from "prosemirror-view";
import { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter";
-import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer";
import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter";
import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos";
import { Block } from "../../blocks/defaultBlocks";
@@ -227,11 +227,10 @@ function dragStart<
const selectedSlice = view.state.selection.content();
const schema = editor.pmSchema;
- const internalHTMLSerializer = createInternalHTMLSerializer(schema, editor);
- const internalHTML = internalHTMLSerializer.serializeProseMirrorFragment(
- selectedSlice.content,
- {}
- );
+ const clipboardHML = (pmView as any).__serializeForClipboard(
+ view,
+ selectedSlice
+ ).dom.innerHTML;
const externalHTMLExporter = createExternalHTMLExporter(schema, editor);
const externalHTML = externalHTMLExporter.exportProseMirrorFragment(
@@ -242,7 +241,7 @@ function dragStart<
const plainText = cleanHTMLToMarkdown(externalHTML);
e.dataTransfer.clearData();
- e.dataTransfer.setData("blocknote/html", internalHTML);
+ e.dataTransfer.setData("blocknote/html", clipboardHML);
e.dataTransfer.setData("text/html", externalHTML);
e.dataTransfer.setData("text/plain", plainText);
e.dataTransfer.effectAllowed = "move";
diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts
index 1dac32dd6..b68b90c65 100644
--- a/packages/core/src/schema/blocks/createSpec.ts
+++ b/packages/core/src/schema/blocks/createSpec.ts
@@ -162,15 +162,24 @@ export function createBlockSpec<
return getParseRules(blockConfig, blockImplementation.parse);
},
- renderHTML() {
- // renderHTML is not really used, as we always use a nodeView, and we use toExternalHTML / toInternalHTML for serialization
- // There's an edge case when this gets called nevertheless; before the nodeviews have been mounted
- // this is why we implement it with a temporary placeholder
+ renderHTML({ HTMLAttributes }) {
+ // renderHTML is used for copy/pasting content from the editor back into
+ // the editor, so we need to make sure the `blockContent` element is
+ // structured correctly as this is what's used for parsing blocks. We
+ // just render a placeholder div inside as the `blockContent` element
+ // already has all the information needed for proper parsing.
const div = document.createElement("div");
div.setAttribute("data-tmp-placeholder", "true");
- return {
- dom: div,
- };
+ return wrapInBlockStructure(
+ {
+ dom: div,
+ },
+ blockConfig.type,
+ {},
+ blockConfig.propSchema,
+ blockConfig.isFileBlock,
+ HTMLAttributes
+ );
},
addNodeView() {
diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx
index 687f93afe..1122527a0 100644
--- a/packages/react/src/schema/ReactBlockSpec.tsx
+++ b/packages/react/src/schema/ReactBlockSpec.tsx
@@ -17,6 +17,7 @@ import {
PropSchema,
propsToAttributes,
StyleSchema,
+ wrapInBlockStructure,
} from "@blocknote/core";
import {
NodeView,
@@ -130,15 +131,24 @@ export function createReactBlockSpec<
return getParseRules(blockConfig, blockImplementation.parse);
},
- renderHTML() {
- // renderHTML is not really used, as we always use a nodeView, and we use toExternalHTML / toInternalHTML for serialization
- // There's an edge case when this gets called nevertheless; before the nodeviews have been mounted
- // this is why we implement it with a temporary placeholder
+ renderHTML({ HTMLAttributes }) {
+ // renderHTML is used for copy/pasting content from the editor back into
+ // the editor, so we need to make sure the `blockContent` element is
+ // structured correctly as this is what's used for parsing blocks. We
+ // just render a placeholder div inside as the `blockContent` element
+ // already has all the information needed for proper parsing.
const div = document.createElement("div");
div.setAttribute("data-tmp-placeholder", "true");
- return {
- dom: div,
- };
+ return wrapInBlockStructure(
+ {
+ dom: div,
+ },
+ blockConfig.type,
+ {},
+ blockConfig.propSchema,
+ blockConfig.isFileBlock,
+ HTMLAttributes
+ );
},
addNodeView() {