File
Handle file pasting, dropping and uploading in the editor.
It’s common to upload files to a remote server when pasting or dropping files into the editor. Here are two approaches to implement this functionality:
The simplest method is to update the document after uploading the file and receiving the file URL:
import {
} from 'prosekit/core' import { insertNode function insertNode(options: InsertNodeOptions): Command
Returns a command that inserts the given node at the current selection or at the given position., defineFileDropHandler function defineFileDropHandler(handler: FileDropHandler): PlainExtension
, } from 'prosekit/extensions/file' import type { ImageAttrs } from 'prosekit/extensions/image' import type { defineFilePasteHandler function defineFilePasteHandler(handler: FilePasteHandler): PlainExtension
} from 'prosekit/pm/state' import type { Command type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
Commands are functions that take a state and a an optional transaction dispatch function and... - determine whether they apply to this state - if not, return false - if `dispatch` was passed, perform their effect, possibly by passing a transaction to `dispatch` - return true In some cases, the editor view is passed as a third argument.} from 'prosekit/pm/view' async function EditorView class EditorView
An editor view manages the DOM structure that represents an editable document. Its state and behavior are determined by its [props](https://prosemirror.net/docs/ref/#view.DirectEditorProps).( handleFile function handleFile(view: EditorView, file: File, pos?: number): Promise<boolean>
: view view: EditorView
, EditorView class EditorView
An editor view manages the DOM structure that represents an editable document. Its state and behavior are determined by its [props](https://prosemirror.net/docs/ref/#view.DirectEditorProps).: File, file file: File
?: number) { // Upload the file to a remote server and get the URL const pos pos: number | undefined
: string = await url const url: string
( myUploader function myUploader(file: File): Promise<string>
) // Attributes for the image node const file file: File
: ImageAttrs = { attrs const attrs: ImageAttrs
: src ImageAttrs.src?: string | null | undefined
} // For paste, insert the image node at current text cursor position. // For drop, insert the image node at the drop position. const url const url: string
: command const command: Command
= Command type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
Commands are functions that take a state and a an optional transaction dispatch function and... - determine whether they apply to this state - if not, return false - if `dispatch` was passed, perform their effect, possibly by passing a transaction to `dispatch` - return true In some cases, the editor view is passed as a third argument.? pos pos: number | undefined
({ insertNode function insertNode(options: InsertNodeOptions): Command
Returns a command that inserts the given node at the current selection or at the given position.: 'image', type InsertNodeOptions.type?: string | NodeType | undefined
The type of the node to insert. Either this or `node` must be provided., attrs InsertNodeOptions.attrs?: Attrs | undefined
When `type` is provided, the attributes of the node to insert.}) : pos InsertNodeOptions.pos?: number | undefined
The position to insert the node at. By default it will be the anchor position of current selection.({ insertNode function insertNode(options: InsertNodeOptions): Command
Returns a command that inserts the given node at the current selection or at the given position.: 'image', type InsertNodeOptions.type?: string | NodeType | undefined
The type of the node to insert. Either this or `node` must be provided.}) return attrs InsertNodeOptions.attrs?: Attrs | undefined
When `type` is provided, the attributes of the node to insert.( command const command: (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean
. view view: EditorView
, state EditorView.state: EditorState
The view's current [state](https://prosemirror.net/docs/ref/#state.EditorState).. view view: EditorView
, dispatch EditorView.dispatch(tr: Transaction): void
Dispatch a transaction. Will call [`dispatchTransaction`](https://prosemirror.net/docs/ref/#view.DirectEditorProps.dispatchTransaction) when given, and otherwise defaults to applying the transaction to the current state and calling [`updateState`](https://prosemirror.net/docs/ref/#view.EditorView.updateState) with the result. This method is bound to the view instance, so that it can be easily passed around.) } const view view: EditorView
= imagePasteExtension const imagePasteExtension: PlainExtension
(({ defineFilePasteHandler function defineFilePasteHandler(handler: FilePasteHandler): PlainExtension
, view view: EditorView
The editor view.}) => { if (! file file: File
The file that was pasted.. file file: File
The file that was pasted.. type Blob.type: string
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type)('image/')) return false void startsWith String.startsWith(searchString: string, position?: number): boolean
Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.( handleFile function handleFile(view: EditorView, file: File, pos?: number): Promise<boolean>
, view view: EditorView
The editor view.) return true }) const file file: File
The file that was pasted.= imageDropExtension const imageDropExtension: PlainExtension
(({ defineFileDropHandler function defineFileDropHandler(handler: FileDropHandler): PlainExtension
, view view: EditorView
The editor view., file file: File
The file that was dropped.}) => { if (! pos pos: number
The position of the document where the file was dropped.. file file: File
The file that was dropped.. type Blob.type: string
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type)('image/')) return false void startsWith String.startsWith(searchString: string, position?: number): boolean
Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.( handleFile function handleFile(view: EditorView, file: File, pos?: number): Promise<boolean>
, view view: EditorView
The editor view., file file: File
The file that was dropped.) return true }) pos pos: number
The position of the document where the file was dropped.
Advanced Approach: Immediate Display with UploadTask
Section titled “Advanced Approach: Immediate Display with UploadTask”To improve user experience, especially with slow uploads, you can insert the node immediately and update it once the upload is complete:
First, create an UploadTask
instance on paste or drop:
upload-file.ts
import { insertNode, union, } from 'prosekit/core' import { defineFileDropHandler, defineFilePasteHandler, UploadTask, type Uploader, } from 'prosekit/extensions/file' /** * Returns an extension that handles image file uploads when pasting or dropping * images into the editor. */ export function defineImageFileHandlers() { return union( defineFilePasteHandler(({ view, file }) => { // Only handle image files if (!file.type.startsWith('image/')) { return false } // Upload the image to https://tmpfiles.org/ const uploadTask = new UploadTask({ file, uploader: tmpfilesUploader, }) // Insert the image node at the current text selection position const command = insertNode({ type: 'image', attrs: { src: uploadTask.objectURL }, }) return command(view.state, view.dispatch, view) }), defineFileDropHandler(({ view, file, pos }) => { // Only handle image files if (!file.type.startsWith('image/')) { return false } // Upload the image to https://tmpfiles.org/ const uploadTask = new UploadTask({ file, uploader: tmpfilesUploader, }) // Insert the image node at the drop position const command = insertNode({ type: 'image', attrs: { src: uploadTask.objectURL }, pos, }) return command(view.state, view.dispatch, view) }), ) } /** * Uploads the given file to https://tmpfiles.org/ and returns the URL of the * uploaded file. * * This function is only for demonstration purposes. All uploaded files will be * deleted after 1 hour. */ const tmpfilesUploader: Uploader<string> = ({ file, onProgress, }): Promise<string> => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() const formData = new FormData() formData.append('file', file) xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable && onProgress) { onProgress({ loaded: event.loaded, total: event.total, }) } }) xhr.addEventListener('load', () => { if (xhr.status === 200) { try { const json = JSON.parse(xhr.responseText) const url: string = (json.data.url as string).replace( 'tmpfiles.org/', 'tmpfiles.org/dl/', ) // Simulate a larger delay setTimeout(() => resolve(url), 1000) } catch (error) { reject(new Error('Failed to parse response', { cause: error })) } } else { reject(new Error(`Upload failed with status ${xhr.status}`)) } }) xhr.addEventListener('error', () => { reject(new Error('Upload failed')) }) xhr.open('POST', 'https://tmpfiles.org/api/v1/upload', true) xhr.send(formData) }) }
Then, use the UploadTask
in your node view. When the upload is complete, update the node’s attrs
with the new file URL:
You can check the full example here.