diff --git a/docs/app/demo/_components/utils.ts b/docs/app/demo/_components/utils.ts index c69c63e3e8..ab0f394886 100644 --- a/docs/app/demo/_components/utils.ts +++ b/docs/app/demo/_components/utils.ts @@ -1,3 +1,5 @@ +import { uploadFile_DEV_ONLY } from "@blocknote/core"; + export const HARDCODED_USERS = [ { id: "user-1", @@ -64,17 +66,9 @@ export async function resolveUsers(userIds: string[]) { return HARDCODED_USERS.filter((user) => userIds.includes(user.id)); } -// Uploads a file to tmpfiles.org and returns the URL to the uploaded file. +// "Uploads" a file using BlockNote's dev-only helper, which encodes it as a +// base64 data URL. In a real app you'd replace this with an upload to your own +// backend that returns a URL to the stored file. export 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 uploadFile_DEV_ONLY(file); } diff --git a/docs/content/docs/react/components/image-toolbar.mdx b/docs/content/docs/react/components/image-toolbar.mdx index e76c47a1a7..ec75dc9056 100644 --- a/docs/content/docs/react/components/image-toolbar.mdx +++ b/docs/content/docs/react/components/image-toolbar.mdx @@ -29,7 +29,7 @@ type uploadFile = (file: File) => Promise; `returns:` A `Promise`, which resolves to the URL that the image can be accessed at. -You can use the provided `uploadToTempFilesOrg` function to as a starting point, which uploads files to [tmpfiles.org](https://tmpfiles.org/). However, it's not recommended to use this in a production environment - you should use your own backend: +You can use the provided `uploadFile_DEV_ONLY` function as a starting point, which encodes files as base64 data URLs. However, it's only meant for development - in production you should use your own backend: diff --git a/examples/01-basic/testing/src/App.tsx b/examples/01-basic/testing/src/App.tsx index 353e445e91..97de4d4a79 100644 --- a/examples/01-basic/testing/src/App.tsx +++ b/examples/01-basic/testing/src/App.tsx @@ -1,4 +1,4 @@ -import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core"; +import { uploadFile_DEV_ONLY } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; @@ -7,7 +7,7 @@ import { useCreateBlockNote } from "@blocknote/react"; export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ - uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + uploadFile: uploadFile_DEV_ONLY, }); // Renders the editor instance using a React component. diff --git a/examples/02-backend/01-file-uploading/README.md b/examples/02-backend/01-file-uploading/README.md index 7c471cf83a..3c3c0ede8b 100644 --- a/examples/02-backend/01-file-uploading/README.md +++ b/examples/02-backend/01-file-uploading/README.md @@ -1,6 +1,6 @@ # Upload Files -This example allows users to upload files and use them in the editor. The files are uploaded to [/TMP/Files](https://tmpfiles.org/), and can be used for File, Image, Video, and Audio blocks. +This example allows users to upload files and use them in the editor. For simplicity, files are encoded as data URLs rather than uploaded to a server, but you'd swap the `uploadFile` function for an upload to your own backend. The uploaded files can be used for File, Image, Video, and Audio blocks. **Try it out:** Click the "Add Image" button and see there's now an "Upload" tab in the toolbar! diff --git a/examples/02-backend/01-file-uploading/src/App.tsx b/examples/02-backend/01-file-uploading/src/App.tsx index 982d0f37a6..830bb0159a 100644 --- a/examples/02-backend/01-file-uploading/src/App.tsx +++ b/examples/02-backend/01-file-uploading/src/App.tsx @@ -1,21 +1,16 @@ +import { uploadFile_DEV_ONLY } from "@blocknote/core"; import "@blocknote/core/fonts/inter.css"; 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. +// "Uploads" a file using BlockNote's dev-only helper, which encodes it as a +// base64 data URL. We add a short delay first to simulate the latency of a real +// server upload. In a real app you'd replace this with an upload to your own +// backend that returns a URL to the stored file. 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/", - ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return uploadFile_DEV_ONLY(file); } export default function App() { diff --git a/examples/03-ui-components/11-uppy-file-panel/src/UppyFilePanel.tsx b/examples/03-ui-components/11-uppy-file-panel/src/UppyFilePanel.tsx index 6d8ad0a067..f5060ded99 100644 --- a/examples/03-ui-components/11-uppy-file-panel/src/UppyFilePanel.tsx +++ b/examples/03-ui-components/11-uppy-file-panel/src/UppyFilePanel.tsx @@ -1,9 +1,9 @@ +import { uploadFile_DEV_ONLY } from "@blocknote/core"; import { FilePanelProps, useBlockNoteEditor } from "@blocknote/react"; -import Uppy, { UploadSuccessCallback } from "@uppy/core"; +import Uppy, { UploadCompleteCallback } from "@uppy/core"; import "@uppy/core/dist/style.min.css"; import "@uppy/dashboard/dist/style.min.css"; import { Dashboard } from "@uppy/react"; -import XHR from "@uppy/xhr-upload"; import { useEffect } from "react"; // Image editor plugin @@ -25,58 +25,38 @@ const uppy = new Uppy() // Instagram Dropbox etc. .use(Webcam) .use(ScreenCapture) - .use(ImageEditor) + .use(ImageEditor); - // Uses an XHR upload plugin to upload files to tmpfiles.org. - // You want to replace this with your own upload endpoint or Uppy Companion - // server. - .use(XHR, { - endpoint: "https://tmpfiles.org/api/v1/upload", - getResponseData(text, _resp) { - return { - url: JSON.parse(text).data.url.replace( - "tmpfiles.org/", - "tmpfiles.org/dl/", - ), - }; - }, - }); +// No uploader plugin is registered: for this demo we "upload" files with +// BlockNote's dev-only helper, which encodes them as base64 data URLs. In a real +// app you'd add an uploader like `@uppy/xhr-upload` pointing at your own backend +// or Uppy Companion server. export function UppyFilePanel(props: FilePanelProps) { const { blockId } = props; const editor = useBlockNoteEditor(); useEffect(() => { - // Listen for successful tippy uploads, and then update the Block with the - // uploaded URL. - const handler: UploadSuccessCallback> = ( - file, - response, + // Listen for completed Dashboard uploads, then update the Block with the + // uploaded file's URL. + const handler: UploadCompleteCallback> = async ( + result, ) => { - if (!file) { - return; - } - - if (file.source === "uploadFile") { - // Didn't originate from Dashboard, should be handled by `uploadFile` - return; - } - if (response.status === 200) { - const updateData = { + for (const file of result.successful) { + editor.updateBlock(blockId, { props: { - name: file?.name, - url: response.uploadURL, + name: file.name, + url: await uploadFile_DEV_ONLY(file.data as File), }, - }; - editor.updateBlock(blockId, updateData); + }); // File should be removed from the Uppy instance after upload. uppy.removeFile(file.id); } }; - uppy.on("upload-success", handler); + uppy.on("complete", handler); return () => { - uppy.off("upload-success", handler); + uppy.off("complete", handler); }; }, [blockId, editor]); @@ -87,18 +67,5 @@ export function UppyFilePanel(props: FilePanelProps) { // Implementation for the BlockNote `uploadFile` function. // This function is used when for example, files are dropped into the editor. export async function uploadFile(file: File) { - const id = uppy.addFile({ - id: file.name, - name: file.name, - type: file.type, - data: file, - source: "uploadFile", - }); - - try { - const result = await uppy.upload(); - return result.successful[0].response!.uploadURL!; - } finally { - uppy.removeFile(id); - } + return uploadFile_DEV_ONLY(file); } diff --git a/packages/core/src/blocks/File/helpers/uploadFile_DEV_ONLY.ts b/packages/core/src/blocks/File/helpers/uploadFile_DEV_ONLY.ts new file mode 100644 index 0000000000..b70b63937c --- /dev/null +++ b/packages/core/src/blocks/File/helpers/uploadFile_DEV_ONLY.ts @@ -0,0 +1,15 @@ +/** + * Encodes a file as a base64 data URL and returns it. This keeps the demo + * self-contained (no external upload host, so nothing can rate-limit or break + * it), at the cost of embedding the file directly in the document. + * + * @warning This function should only be used for development purposes, replace with your own backend! + */ +export const uploadFile_DEV_ONLY = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); +}; diff --git a/packages/core/src/blocks/File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.ts b/packages/core/src/blocks/File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.ts deleted file mode 100644 index ab0a686e82..0000000000 --- a/packages/core/src/blocks/File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Uploads a file to tmpfiles.org and returns the URL to the uploaded file. - * - * @warning This function should only be used for development purposes, replace with your own backend! - */ -export const uploadToTmpFilesDotOrg_DEV_ONLY = async ( - file: File, -): Promise => { - 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/", - ); -}; diff --git a/packages/core/src/blocks/index.ts b/packages/core/src/blocks/index.ts index d40bba055c..2fbfbeb6c2 100644 --- a/packages/core/src/blocks/index.ts +++ b/packages/core/src/blocks/index.ts @@ -20,7 +20,7 @@ export * from "./Code/helpers/parse/parsePreCode.js"; export * from "./Code/helpers/render/createCodeBlock.js"; export * from "./Code/helpers/toExternalHTML/createPreCode.js"; export * from "./ToggleWrapper/createToggleWrapper.js"; -export * from "./File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.js"; +export * from "./File/helpers/uploadFile_DEV_ONLY.js"; export * from "./PageBreak/getPageBreakSlashMenuItems.js"; export * from "./BlockNoteSchema.js"; diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 155a460786..fc95039f22 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -353,7 +353,7 @@ export const examples = { slug: "backend", }, readme: - 'This example allows users to upload files and use them in the editor. The files are uploaded to [/TMP/Files](https://tmpfiles.org/), and can be used for File, Image, Video, and Audio blocks.\n\n**Try it out:** Click the "Add Image" button and see there\'s now an "Upload" tab in the toolbar!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [File Block](/docs/features/blocks/embeds#file)', + 'This example allows users to upload files and use them in the editor. For simplicity, files are encoded as data URLs rather than uploaded to a server, but you\'d swap the `uploadFile` function for an upload to your own backend. The uploaded files can be used for File, Image, Video, and Audio blocks.\n\n**Try it out:** Click the "Add Image" button and see there\'s now an "Upload" tab in the toolbar!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [File Block](/docs/features/blocks/embeds#file)', }, { projectSlug: "saving-loading", diff --git a/tests/src/end-to-end/images/images.test.tsx b/tests/src/end-to-end/images/images.test.tsx index 67ded55285..52bc073ac2 100644 --- a/tests/src/end-to-end/images/images.test.tsx +++ b/tests/src/end-to-end/images/images.test.tsx @@ -40,7 +40,7 @@ describe("Check Image Block and Toolbar functionality", () => { type: "image/png", }); await userEvent.upload(uploadInput, file); - await waitForSelector(`img[src^="https://tmpfiles.org/"]`); + await waitForSelector(`img[src^="data:"]`); await sleep(500); await userEvent.click(await waitForSelector(`img`)); diff --git a/tests/src/unit/core/createTestEditor.ts b/tests/src/unit/core/createTestEditor.ts index 26c9324f91..b9c37d8f70 100644 --- a/tests/src/unit/core/createTestEditor.ts +++ b/tests/src/unit/core/createTestEditor.ts @@ -5,7 +5,7 @@ import { createCodeBlockSpec, InlineContentSchema, StyleSchema, - uploadToTmpFilesDotOrg_DEV_ONLY, + uploadFile_DEV_ONLY, } from "@blocknote/core"; import { afterAll, beforeAll } from "vite-plus/test"; @@ -57,7 +57,7 @@ export const createTestEditor = < headers: true, }, trailingBlock: false, - uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY, + uploadFile: uploadFile_DEV_ONLY, }) as any; editor.mount(div); });