diff --git a/examples/02-backend/01-file-uploading/src/App.tsx b/examples/02-backend/01-file-uploading/src/App.tsx index c5805f0c40..52d774c468 100644 --- a/examples/02-backend/01-file-uploading/src/App.tsx +++ b/examples/02-backend/01-file-uploading/src/App.tsx @@ -3,19 +3,9 @@ import { useCreateBlockNote } from "@blocknote/react"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; -// Uploads a file to tmpfiles.org and returns the URL to the uploaded file. +// Creates a local blob URL for the file (for testing without an upload server). async function uploadFile(file: File) { - const body = new FormData(); - body.append("file", file); - - const ret = await fetch("https://tmpfiles.org/api/v1/upload", { - method: "POST", - body: body, - }); - return (await ret.json()).data.url.replace( - "tmpfiles.org/", - "tmpfiles.org/dl/", - ); + return URL.createObjectURL(file); } export default function App() { diff --git a/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts index f602ef4a2d..f11ab3fa8c 100644 --- a/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts +++ b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts @@ -2,6 +2,7 @@ import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; +import { FilePanelExtension } from "../../../extensions/FilePanel/FilePanel.js"; import { BlockSchema, InlineContentSchema, @@ -41,6 +42,14 @@ export const createDropFileExtension = < } if (format === "Files") { + // If the file panel is open, its React drop handler + // manages the upload — don't insert a duplicate block. + if ( + editor.getExtension(FilePanelExtension)?.store.state + ) { + return true; + } + handleFileInsertion(event, editor); return true; } diff --git a/packages/react/src/components/FilePanel/FilePanel.tsx b/packages/react/src/components/FilePanel/FilePanel.tsx index 9365571025..0262844d3b 100644 --- a/packages/react/src/components/FilePanel/FilePanel.tsx +++ b/packages/react/src/components/FilePanel/FilePanel.tsx @@ -4,9 +4,10 @@ import { DefaultInlineContentSchema, DefaultStyleSchema, InlineContentSchema, + PartialBlock, StyleSchema, } from "@blocknote/core"; -import { useState } from "react"; +import { DragEvent, useCallback, useState } from "react"; import { ComponentProps, @@ -40,6 +41,43 @@ export const FilePanel = < const [loading, setLoading] = useState(false); + const handleDragOver = useCallback((e: DragEvent) => { + e.preventDefault(); + }, []); + + const handleDrop = useCallback( + (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const file = e.dataTransfer.files?.[0]; + if (!file || !editor.uploadFile) { + return; + } + + (async () => { + setLoading(true); + try { + let updateData = await editor.uploadFile!(file, props.blockId); + if (typeof updateData === "string") { + updateData = { + props: { + name: file.name, + url: updateData, + }, + } as PartialBlock; + } + editor.updateBlock(props.blockId, updateData); + } catch { + // Leave panel open so the user can retry. + } finally { + setLoading(false); + } + })(); + }, + [editor, props.blockId], + ); + const tabs: PanelProps["tabs"] = props.tabs ?? [ ...(editor.uploadFile !== undefined ? [ @@ -62,13 +100,15 @@ export const FilePanel = < ); return ( - +
+ +
); };