Skip to content

Dragging an image into the inline editor navigates the browser away, discarding unsaved edits — InlinePortableTextEditor registers Image but defines no handleDrop #2425

Description

@virafb

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:

  • :15import Image from "@tiptap/extension-image";
  • :1951Image.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-urlPOST /_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

  1. Open an article in the inline editor.
  2. Type a few words without blurring the editor.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions