Skip to content

Commit 31dd97f

Browse files
feat: Pasting & dropping files (#852)
* paste extension updates * updted utils file * Extended paste handling for all file block types and added file drag & drop handling * Implemented PR feedback --------- Co-authored-by: Matthew Lipski <[email protected]>
1 parent 4663318 commit 31dd97f

File tree

6 files changed

+175
-17
lines changed

6 files changed

+175
-17
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const acceptedMIMETypes = [
2+
"blocknote/html",
3+
"Files",
4+
"text/html",
5+
"text/plain",
6+
] as const;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Extension } from "@tiptap/core";
2+
import { Plugin } from "prosemirror-state";
3+
4+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
5+
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
6+
import { handleFileInsertion } from "./handleFileInsertion";
7+
import { acceptedMIMETypes } from "./acceptedMIMETypes";
8+
9+
export const createDropFileExtension = <
10+
BSchema extends BlockSchema,
11+
I extends InlineContentSchema,
12+
S extends StyleSchema
13+
>(
14+
editor: BlockNoteEditor<BSchema, I, S>
15+
) =>
16+
Extension.create<{ editor: BlockNoteEditor<BSchema, I, S> }, undefined>({
17+
name: "dropFile",
18+
addProseMirrorPlugins() {
19+
return [
20+
new Plugin({
21+
props: {
22+
handleDOMEvents: {
23+
drop(_view, event) {
24+
if (!editor.isEditable) {
25+
return;
26+
}
27+
28+
let format: (typeof acceptedMIMETypes)[number] | null = null;
29+
for (const mimeType of acceptedMIMETypes) {
30+
if (event.dataTransfer!.types.includes(mimeType)) {
31+
format = mimeType;
32+
break;
33+
}
34+
}
35+
if (format === null) {
36+
return true;
37+
}
38+
39+
if (format === "Files") {
40+
handleFileInsertion(event, editor);
41+
return true;
42+
}
43+
44+
return false;
45+
},
46+
},
47+
},
48+
}),
49+
];
50+
},
51+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
2+
import { PartialBlock } from "../../blocks/defaultBlocks";
3+
import { insertOrUpdateBlock } from "../../extensions/SuggestionMenu/getDefaultSlashMenuItems";
4+
import {
5+
BlockSchema,
6+
FileBlockConfig,
7+
InlineContentSchema,
8+
StyleSchema,
9+
} from "../../schema";
10+
import { acceptedMIMETypes } from "./acceptedMIMETypes";
11+
12+
function checkMIMETypesMatch(mimeType1: string, mimeType2: string) {
13+
const types1 = mimeType1.split("/");
14+
const types2 = mimeType2.split("/");
15+
16+
if (types1.length !== 2) {
17+
throw new Error(`The string ${mimeType1} is not a valid MIME type.`);
18+
}
19+
if (types2.length !== 2) {
20+
throw new Error(`The string ${mimeType2} is not a valid MIME type.`);
21+
}
22+
23+
if (types1[1] === "*" || types2[1] === "*") {
24+
return types1[0] === types2[0];
25+
}
26+
if (types1[0] === "*" || types2[0] === "*") {
27+
return types1[1] === types2[1];
28+
}
29+
30+
return types1[0] === types2[0] && types1[1] === types2[1];
31+
}
32+
33+
export async function handleFileInsertion<
34+
BSchema extends BlockSchema,
35+
I extends InlineContentSchema,
36+
S extends StyleSchema
37+
>(event: DragEvent | ClipboardEvent, editor: BlockNoteEditor<BSchema, I, S>) {
38+
if (!editor.uploadFile) {
39+
return;
40+
}
41+
42+
const dataTransfer =
43+
"dataTransfer" in event ? event.dataTransfer : event.clipboardData;
44+
if (dataTransfer === null) {
45+
return;
46+
}
47+
48+
let format: (typeof acceptedMIMETypes)[number] | null = null;
49+
for (const mimeType of acceptedMIMETypes) {
50+
if (dataTransfer.types.includes(mimeType)) {
51+
format = mimeType;
52+
break;
53+
}
54+
}
55+
if (format !== "Files") {
56+
return;
57+
}
58+
59+
const items = dataTransfer.items;
60+
if (!items) {
61+
return;
62+
}
63+
64+
event.preventDefault();
65+
66+
const fileBlockConfigs = Object.values(editor.schema.blockSchema).filter(
67+
(blockConfig) => blockConfig.isFileBlock
68+
) as FileBlockConfig[];
69+
70+
for (let i = 0; i < items.length; i++) {
71+
// Gets file block corresponding to MIME type.
72+
let fileBlockType = "file";
73+
for (const fileBlockConfig of fileBlockConfigs) {
74+
for (const mimeType of fileBlockConfig.fileBlockAcceptMimeTypes || []) {
75+
if (checkMIMETypesMatch(items[i].type, mimeType)) {
76+
fileBlockType = fileBlockConfig.type;
77+
break;
78+
}
79+
}
80+
}
81+
82+
const file = items[i].getAsFile();
83+
if (file) {
84+
const updateData = await editor.uploadFile(file);
85+
86+
if (typeof updateData === "string") {
87+
const fileBlock = {
88+
type: fileBlockType,
89+
props: {
90+
name: file.name,
91+
url: updateData,
92+
},
93+
} as PartialBlock<BSchema, I, S>;
94+
95+
insertOrUpdateBlock(editor, fileBlock);
96+
}
97+
}
98+
}
99+
}

packages/core/src/api/parsers/pasteExtension.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@ import { Plugin } from "prosemirror-state";
33

44
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
55
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
6+
import { handleFileInsertion } from "./handleFileInsertion";
67
import { nestedListsToBlockNoteStructure } from "./html/util/nestedLists";
7-
8-
const acceptedMIMETypes = [
9-
"blocknote/html",
10-
"text/html",
11-
"text/plain",
12-
] as const;
8+
import { acceptedMIMETypes } from "./acceptedMIMETypes";
139

1410
export const createPasteFromClipboardExtension = <
1511
BSchema extends BlockSchema,
@@ -33,26 +29,30 @@ export const createPasteFromClipboardExtension = <
3329
}
3430

3531
let format: (typeof acceptedMIMETypes)[number] | null = null;
36-
3732
for (const mimeType of acceptedMIMETypes) {
3833
if (event.clipboardData!.types.includes(mimeType)) {
3934
format = mimeType;
4035
break;
4136
}
4237
}
38+
if (format === null) {
39+
return true;
40+
}
4341

44-
if (format !== null) {
45-
let data = event.clipboardData!.getData(format);
46-
if (format === "text/html") {
47-
const htmlNode = nestedListsToBlockNoteStructure(
48-
data.trim()
49-
);
42+
if (format === "Files") {
43+
handleFileInsertion(event, editor);
44+
return true;
45+
}
5046

51-
data = htmlNode.innerHTML;
52-
}
53-
editor._tiptapEditor.view.pasteHTML(data);
47+
let data = event.clipboardData!.getData(format);
48+
49+
if (format === "text/html") {
50+
const htmlNode = nestedListsToBlockNoteStructure(data.trim());
51+
data = htmlNode.innerHTML;
5452
}
5553

54+
editor._tiptapEditor.view.pasteHTML(data);
55+
5656
return true;
5757
},
5858
},

packages/core/src/editor/BlockNoteExtensions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Text } from "@tiptap/extension-text";
1313
import * as Y from "yjs";
1414
import { createCopyToClipboardExtension } from "../api/exporters/copyExtension";
1515
import { createPasteFromClipboardExtension } from "../api/parsers/pasteExtension";
16+
import { createDropFileExtension } from "../api/parsers/fileDropExtension";
1617
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension";
1718
import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension";
1819
import { TextColorExtension } from "../extensions/TextColor/TextColorExtension";
@@ -146,6 +147,7 @@ export const getBlockNoteExtensions = <
146147
}),
147148
createCopyToClipboardExtension(opts.editor),
148149
createPasteFromClipboardExtension(opts.editor),
150+
createDropFileExtension(opts.editor),
149151

150152
Dropcursor.configure({ width: 5, color: "#ddeeff" }),
151153
// This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command),

packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
12
import { Block, PartialBlock } from "../../blocks/defaultBlocks";
23
import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards";
3-
import { BlockNoteEditor } from "../../editor/BlockNoteEditor";
44
import {
55
BlockSchema,
66
InlineContentSchema,

0 commit comments

Comments
 (0)