Summary
Dragging an image file into an article body in the inline Portable Text editor does not insert it. With no ProseMirror drop handler registered, the drop falls through to the browser default: the tab navigates to the dropped file and the editor is unmounted, taking any edits made since the last save with it.
The gesture is an obvious one to try, the failure is silent, and the cost is unsaved work rather than a rejected insert.
Version: emdash@0.32.0.
Detail
src/components/InlinePortableTextEditor.tsx registers TipTap's image extension, so the schema can hold image nodes:
:15 — import Image from "@tiptap/extension-image";
:1951 — Image.extend({ … }), adding mediaId, provider, width, height, blurhash, dominantColor
So the editor is fully capable of holding an uploaded image node.
But the editor defines no handleDrop and no handlePaste. editorProps exists at :1990 and carries only presentation attributes:
editorProps: {
attributes: {
class: "prose prose-sm sm:prose-base dark:prose-invert max-w-none emdash-inline-editor",
dir: "auto",
},
},
Neither identifier appears anywhere in the file. Without a handler returning true, ProseMirror leaves the drop to the browser, which navigates to the file.
The unsaved-edit exposure comes from the save model in the same component. onUpdate marks the document dirty (:1996):
onUpdate: () => {
document.dispatchEvent(new CustomEvent("emdash:save", { detail: { state: "unsaved" } }));
},
and the docblock states the editor "Auto-saves on blur". A full-page navigation triggered by the drop is not a reliable save point, so anything typed since the last save is at risk. We observed the navigation directly; we did not instrument the precise ordering of blur, autosave and unload, so treat the loss window as "edits since last save" rather than a specific race.
The supported path works
Insertion via the block/slash menu is correct and we verified it end to end. The menu item is defined at :850:
{
id: "image",
title: "Image",
description: "Insert an image",
icon: "🖼",
aliases: ["img", "photo", "picture"],
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
// Signal the component to open the media picker
document.dispatchEvent(new CustomEvent("emdash:open-media-picker"));
},
}
End-to-end on our UAT instance: POST /_emdash/api/media/upload-url → POST /_emdash/api/media/<id>/confirm → publish. The published page carries the R2 URL, and fetching it with no authentication returns 200, image/png, 45,658 bytes. Nothing is wrong with the upload pipeline.
We note the component's own docblock — "Includes BubbleMenu for inline formatting (bold, italic, etc.) but no toolbar, no media picker, no section picker" — so the absence of a drop path may be deliberate scope rather than an oversight.
Reproduction
- Open an article in the inline editor.
- Type a few words without blurring the editor.
- Drag an image file from the desktop into the body.
Observed: the browser navigates to the image, the editor is gone, and the typing from step 2 is not in the saved document.
Expected: the drop either uploads and inserts the image, or is refused with a visible message.
Net effect
An author trying the most natural gesture for inserting an image can lose in-progress work with no warning and no error. Because the block menu is the only insert path and nothing signposts it, "there is no way to add an image here" is a reasonable conclusion for an author to reach — the body toolbar offers bold, italic, headings, lists, alignment and links, but no image control. That cost us a round of confusion during an upgrade rehearsal before we found the block menu.
Suggested fix
Even if a drop-to-upload path is out of scope for this component, the drop should never navigate. editorProps at :1990 is the natural home:
- Minimal: add
handleDrop (and handlePaste) that call preventDefault() and surface a short message pointing at the block menu — refuse visibly rather than navigate.
- Better: route the dropped file into the same path the slash command uses, by dispatching
emdash:open-media-picker (or invoking the upload directly) so the drop becomes the discoverable path rather than a trap.
The same reasoning applies to paste: an image on the clipboard has the same default-handling exposure.
Summary
Dragging an image file into an article body in the inline Portable Text editor does not insert it. With no ProseMirror drop handler registered, the drop falls through to the browser default: the tab navigates to the dropped file and the editor is unmounted, taking any edits made since the last save with it.
The gesture is an obvious one to try, the failure is silent, and the cost is unsaved work rather than a rejected insert.
Version:
emdash@0.32.0.Detail
src/components/InlinePortableTextEditor.tsxregisters TipTap's image extension, so the schema can hold image nodes::15—import Image from "@tiptap/extension-image";:1951—Image.extend({ … }), addingmediaId,provider,width,height,blurhash,dominantColorSo the editor is fully capable of holding an uploaded image node.
But the editor defines no
handleDropand nohandlePaste.editorPropsexists at:1990and carries only presentation attributes:Neither identifier appears anywhere in the file. Without a handler returning
true, ProseMirror leaves the drop to the browser, which navigates to the file.The unsaved-edit exposure comes from the save model in the same component.
onUpdatemarks the document dirty (:1996):and the docblock states the editor "Auto-saves on blur". A full-page navigation triggered by the drop is not a reliable save point, so anything typed since the last save is at risk. We observed the navigation directly; we did not instrument the precise ordering of blur, autosave and unload, so treat the loss window as "edits since last save" rather than a specific race.
The supported path works
Insertion via the block/slash menu is correct and we verified it end to end. The menu item is defined at
:850:End-to-end on our UAT instance:
POST /_emdash/api/media/upload-url→POST /_emdash/api/media/<id>/confirm→ publish. The published page carries the R2 URL, and fetching it with no authentication returns200,image/png, 45,658 bytes. Nothing is wrong with the upload pipeline.We note the component's own docblock — "Includes BubbleMenu for inline formatting (bold, italic, etc.) but no toolbar, no media picker, no section picker" — so the absence of a drop path may be deliberate scope rather than an oversight.
Reproduction
Observed: the browser navigates to the image, the editor is gone, and the typing from step 2 is not in the saved document.
Expected: the drop either uploads and inserts the image, or is refused with a visible message.
Net effect
An author trying the most natural gesture for inserting an image can lose in-progress work with no warning and no error. Because the block menu is the only insert path and nothing signposts it, "there is no way to add an image here" is a reasonable conclusion for an author to reach — the body toolbar offers bold, italic, headings, lists, alignment and links, but no image control. That cost us a round of confusion during an upgrade rehearsal before we found the block menu.
Suggested fix
Even if a drop-to-upload path is out of scope for this component, the drop should never navigate.
editorPropsat:1990is the natural home:handleDrop(andhandlePaste) that callpreventDefault()and surface a short message pointing at the block menu — refuse visibly rather than navigate.emdash:open-media-picker(or invoking the upload directly) so the drop becomes the discoverable path rather than a trap.The same reasoning applies to paste: an image on the clipboard has the same default-handling exposure.