Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vite-plus/test";

import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
import { handleFileInsertion } from "./handleFileInsertion.js";

describe("File insertion", () => {
it.each(["drop", "paste"] as const)(
"uploads and inserts all files in order after %s data becomes unavailable",
async (eventType) => {
let finishUpload!: () => void;
const uploadPending = new Promise<void>((resolve) => {
finishUpload = resolve;
});
const uploadFile = vi.fn(async (file: File) => {
await uploadPending;
return `https://example.com/${file.name}`;
});
const editor = BlockNoteEditor.create({
initialContent: [{ type: "paragraph" }],
trailingBlock: false,
uploadFile,
});

try {
// Resolve drops to the first block without mounting a view.
Object.defineProperty(editor, "prosemirrorView", {
value: {
posAtCoords: () => ({ pos: 1, inside: -1 }),
} satisfies Pick<BlockNoteEditor["prosemirrorView"], "posAtCoords">,
});
const files = [
new File(["first"], "first.png", { type: "image/png" }),
new File(["second"], "second.pdf", { type: "application/pdf" }),
];
let protectedData = false;
const items = files.map((file) => ({
kind: "file",
type: file.type,
getAsFile: () => (protectedData ? null : file),
}));
const event =
eventType === "drop"
? new DragEvent("drop", { cancelable: true })
: new ClipboardEvent("paste", { cancelable: true });
Object.defineProperty(
event,
eventType === "drop" ? "dataTransfer" : "clipboardData",
{ value: { types: ["Files"], items } },
);
const target = new EventTarget();
let insertion = Promise.resolve();
target.addEventListener(eventType, () => {
insertion = handleFileInsertion(event, editor);
});
target.dispatchEvent(event);
// Native event data is only available during synchronous dispatch.
protectedData = true;
items.length = 0;
expect(uploadFile).toHaveBeenCalledTimes(1);
finishUpload();
await insertion;

expect.soft(uploadFile).toHaveBeenCalledTimes(2);
expect(
editor.document.map((block) => ({
type: block.type,
props: block.props,
})),
).toMatchObject([
{
type: "image",
props: {
name: "first.png",
url: "https://example.com/first.png",
},
},
{
type: "file",
props: {
name: "second.pdf",
url: "https://example.com/second.pdf",
},
},
]);
expect(uploadFile.mock.calls.map(([file]) => file)).toEqual(files);
} finally {
editor._tiptapEditor.destroy();
}
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,24 @@ export async function handleFileInsertion<

event.preventDefault();

for (let i = 0; i < items.length; i++) {
// Capture files before awaiting uploads, while event data is still available.
const files = Array.from(items, (item) => ({
file: item.getAsFile(),
type: item.type,
}));

for (const { file, type } of files) {
// Gets file block corresponding to MIME type.
let fileBlockType = "file";
for (const blockSpec of Object.values(editor.schema.blockSpecs)) {
for (const mimeType of blockSpec.implementation.meta.fileBlockAccept ||
[]) {
const isFileExtension = mimeType.startsWith(".");
const file = items[i].getAsFile();

if (file) {
if (
(!isFileExtension &&
file.type &&
checkMIMETypesMatch(items[i].type, mimeType)) ||
checkMIMETypesMatch(type, mimeType)) ||
(isFileExtension &&
checkFileExtensionsMatch(
"." + file.name.split(".").pop(),
Expand All @@ -132,7 +136,6 @@ export async function handleFileInsertion<
}
}

const file = items[i].getAsFile();
if (file) {
const fileBlock = {
type: fileBlockType,
Expand Down
Loading