Files and drop

Images inline, every other file to the host, and a drop target as large as the composer.

A file reaches the editor two ways: dropped on it, or pasted into it. Each file goes to the first feature that takes it. The image feature takes images when the host wired onUploadImage: one placeholder per file appears at the caret, and each fills in as its upload lands. Every file no feature takes goes to onDropFiles, in one call per drop or paste, so the host can show it in an attachments tray.

Hello Camille,
Drop a PDF anywhere on this card and it joins the attachments below. Drop a photo and it lands in the text.

Drop a PDF anywhere on the card, the toolbar and the footer included: it joins the tray. Drop a photo: it lands in the text.

Where each file goes

The host wiredImagesOther files
onUploadImage and onDropFilesinline, uploadedonDropFiles
onDropFiles onlyonDropFilesonDropFiles
onUploadImage onlyinline, uploadedignored
neitherignoredignored

An ignored file is kept from the browser, which would otherwise open it in place of the page, and from Plate, which would try to place a file as text. Nothing is inserted and nothing is reported. That is the behaviour of an editor without onDropFiles, unchanged from before the option existed: a document editor has nowhere to put a PDF, and a composer that does says so by passing onDropFiles. A read-only editor takes no file.

Text drops and text pastes keep Plate's own handling.

The drop zone

The canvas alone is a small target in a composer whose toolbar, tray and footer take half the card. ContentEditor.DropZone makes everything it wraps a drop target, with an overlay while files are dragged over it. A file dropped on the zone goes where it would on the canvas: an image at the caret, the rest to onDropFiles.

const [attachments, setAttachments] = useState<ReadonlyArray<File>>([]);
 
<ContentEditor.Root
	features={FEATURES}
	value={body}
	onChange={setBody}
	onUploadImage={uploadImage}
	onDropFiles={(files) => setAttachments((current) => [...current, ...files])}
>
	<ContentEditor.DropZone className="rounded-xl border p-2">
		<ContentEditor.Layout>
			<ContentEditor.Toolbar />
			<ContentEditor.Canvas />
		</ContentEditor.Layout>
		<AttachmentTray files={attachments} />
	</ContentEditor.DropZone>
</ContentEditor.Root>

The zone sits inside ContentEditor.Root, since it hands files to the editor. It renders a div, positioned so the overlay covers it; pass render for another element. While files are over it, it carries data-dragging, and the overlay is [data-slot="content-editor-drop-overlay"], for a host that styles either. The overlay text is the chrome.dropToUpload label.

Without a zone, only the canvas takes files, with the same routing.

The tray is the host's: the editor does not hold attachments, since what happens to them (an upload, a size limit, a remove button) belongs to the product. The kit's Attachment parts draw one.

API

ContentEditor.DropZone

PropTypeDefault
renderComponentRenderFn<HTMLProps, {}> | ReactElement<unknown, string | JSXElementConstructor<any>>—Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

Plus every <div> prop. Source: content-editor/components/drop-zone.tsx.