Repository navigation
feat(muya): restore drag-and-drop image insertion (PG4) - #4413
Conversation
There was a problem hiding this comment.
Pull request overview
Restores engine-level drag-and-drop image insertion in @muyajs/core to close parity gap PG4 introduced during the muyajs → core migration, adding both the handler and unit coverage while keeping the engine embedder-agnostic via new option hooks.
Changes:
- Add drag/drop handlers to insert images dropped as either a web-link (
text/uri-list) or a local file (DataTransfer.files), with optional persistence viaimageAction. - Extend
IMuyaOptionswithimageActionandgetPathForFilehooks to avoid Electron-specific dependencies in the engine. - Add happy-dom unit tests and update parity docs/scoreboard to reflect automated coverage of the engine half of PG4.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/muya/src/types.ts | Adds new optional engine hooks (imageAction, getPathForFile) to support embedder-controlled image persistence and file-path resolution. |
| packages/muya/src/editor/index.ts | Wires drag/drop image handlers into Editor.init() lifecycle. |
| packages/muya/src/editor/dragDropImage.ts | Implements drag/drop detection, drop indicator “ghost”, and insertion/persistence flows for dropped images. |
| packages/muya/src/editor/tests/dragDropImage.spec.ts | Adds happy-dom regression tests driving the real handler via synthetic DataTransfer. |
| packages/desktop/test/PARITY_SCOREBOARD.md | Updates PG4 status to “engine fixed” and adjusts remaining gap count. |
| packages/desktop/test/PARITY_QA.md | Clarifies what is now automated vs what remains manual, and documents the required desktop wave-2 wiring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Object.assign(ghost.style, { | ||
| width: `${rect.width}px`, | ||
| left: `${rect.left}px`, | ||
| top: | ||
| target.position === 'up' | ||
| ? `${rect.top - GHOST_HEIGHT}px` | ||
| : `${rect.top + rect.height}px`, | ||
| }); |
There was a problem hiding this comment.
Fixed in b0d2696 — added the #mu-dragover-ghost rule (position: absolute; height: 3px; background: var(--highlight-color)) to src/assets/styles/index.css. The new engine had dropped the legacy rule, so the indicator was invisible.
| const { name } = image; | ||
| const loadingId = `loading-${getUniqueId()}`; | ||
| insertImageParagraph(muya, target, ``); | ||
|
|
||
| void persistDroppedImage(muya, path, name, loadingId); | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Good catch — fixed in b0d2696. When options.imageAction is absent, handleFileImage now inserts a clean  with the raw path verbatim (matching the documented contract and imageEditTool's direct-replacement), instead of leaving a permanent loading-* placeholder. Added a regression test for it.
| const hasImageFile | ||
| = dataTransfer.items.length === 1 | ||
| && dataTransfer.items[0].type.includes('image'); | ||
| const hasUriList = dataTransfer.types.includes('text/uri-list'); | ||
| if (!hasImageFile && !hasUriList) | ||
| return; |
There was a problem hiding this comment.
Fixed in b0d2696. Added isWebImageDrag(dataTransfer) mirroring the legacy heuristic — only intercept text/uri-list drags that also carry text/html and do NOT carry text/plain. dragover now uses it, so a plain hyperlink drag is left to the browser and not preventDefault()-ed.
| // Try the file path first (a dropped image file also exposes a | ||
| // synthetic `text/uri-list`, but the file branch is the intended one). | ||
| const inserted | ||
| = handleFileImage(muya, dragEvent, target) | ||
| || handleWebLinkImage(muya, dragEvent, target); | ||
|
|
There was a problem hiding this comment.
Fixed in b0d2696. dropHandler now gates the web-link branch behind the same isWebImageDrag heuristic, so it won't suppress default drop for a non-image uri-list drag even if dragover was cancelled elsewhere.
| const dt = new DataTransfer(); | ||
| dt.items.add('https://example.com/pic.png', 'text/uri-list'); | ||
|
|
There was a problem hiding this comment.
Updated in b0d2696 — web-link DnD tests now build the realistic browser payload (text/uri-list + text/html) via a webImageDataTransfer helper, and there's a new case asserting a plain-hyperlink drag (uri-list + text/plain) is ignored.
| const dt = new DataTransfer(); | ||
| dt.items.add('not-a-url', 'text/uri-list'); | ||
|
|
There was a problem hiding this comment.
Done in b0d2696 — the non-image case now uses the full (uri-list + html) signature with a non-image URL, so it still asserts no insert under the gated handler (the content-type sniff fails and nothing is inserted).
|
Build artifacts for PR #4413: Run: https://github.com/marktext/marktext/actions/runs/27156469291
|
…eb-image gating) Copilot review on #4413: - Add the `#mu-dragover-ghost` CSS rule (position/height/background) to `assets/styles/index.css`. The new engine had no ghost style, so the drop indicator was invisible — legacy muyajs shipped this rule. - When no `imageAction` hook is configured, insert a clean `` with the raw path verbatim instead of a `` placeholder that would never be swapped (it persists only when imageAction resolves). Matches the documented `imageAction` contract and imageEditTool's direct-replacement behaviour. - Gate the web-link path on the legacy "image dragged from a browser" signature — `text/uri-list` + `text/html` and NO `text/plain` — in both `dragover` and `drop`. A plain hyperlink drag (uri-list + text/plain) is now left to the browser instead of being intercepted and swallowed by `preventDefault()`. Tests updated: web-link drags use the realistic (uri-list + html) payload, plus new cases for the no-imageAction clean insert and the plain-hyperlink pass-through. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The @muyajs/core rewrite (#4406) shipped without any DnD handler, so dropping an image into the document was a no-op — parity gap PG4. Port the legacy muyajs dragDrop/dragDropCtrl behaviour as an engine-level, embedder-agnostic handler. `attachDragDropImageHandlers(muya)` binds dragstart/dragover/drop on the editor container (and dragleave for the ghost) via `eventCenter.attachDOMEvent`, so cleanup rides on `muya.destroy() → detachAllDomEvents()`. It is wired in `Editor.init()` alongside `attachLinkMouseHandlers`. Two drop paths mirror the legacy controller: - web-link image (`text/uri-list`): verify it is an image (extension or content-type sniff) then insert ``. - local image FILE (`dataTransfer.files`): resolve the file to a path via the new embedder `getPathForFile` hook, insert a `` placeholder, persist it through the new `imageAction` option (the same `{ src, alt, title }` contract the imageEditTool plugin consumes), then swap in the returned src. Two optional `IMuyaOptions` hooks are added (mirroring the existing `clipboardFilePath`): `imageAction` and `getPathForFile`. The engine stays free of `window.electron`; the desktop wires these in wave 2. Tested in `dragDropImage.spec.ts`: happy-dom provides a fully working `DataTransfer` (items.add / getAsString / files) and fires getAsString synchronously, so a synthetic `drop` event drives the real handler end-to-end — asserting both drop paths and the no-op-off-target case. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
PG4 (drag-drop image insertion) was a manual-QA-only entry because real drag gestures are hard headless. The engine handler now has a synthetic- DataTransfer unit test, so update the scoreboard and QA checklist: - PARITY_QA.md § PG4: describe what is now automated (both drop paths via the live handler), keep the OS-integration steps manual, and document the desktop wave-2 wiring needed for the local-file persistence path (pass `imageAction` / `getPathForFile` into the Muya constructor). - PARITY_SCOREBOARD.md: point PG4 at the new spec, flag the engine half fixed, and drop the remaining-gaps count 15 → 14. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…eb-image gating) Copilot review on #4413: - Add the `#mu-dragover-ghost` CSS rule (position/height/background) to `assets/styles/index.css`. The new engine had no ghost style, so the drop indicator was invisible — legacy muyajs shipped this rule. - When no `imageAction` hook is configured, insert a clean `` with the raw path verbatim instead of a `` placeholder that would never be swapped (it persists only when imageAction resolves). Matches the documented `imageAction` contract and imageEditTool's direct-replacement behaviour. - Gate the web-link path on the legacy "image dragged from a browser" signature — `text/uri-list` + `text/html` and NO `text/plain` — in both `dragover` and `drop`. A plain hyperlink drag (uri-list + text/plain) is now left to the browser instead of being intercepted and swallowed by `preventDefault()`. Tests updated: web-link drags use the realistic (uri-list + html) payload, plus new cases for the no-imageAction clean insert and the plain-hyperlink pass-through. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
b0d2696 to
22020cb
Compare
Summary
Parity follow-up to #4406 (the muyajs →
@muyajs/coremigration). The new engine shipped without any drag-and-drop handler, so dropping an image into the document was a no-op — parity gap PG4. This restores the legacydragDrop/dragDropCtrlbehaviour as an engine-level, embedder-agnostic handler. ENGINE-ONLY: no desktopeditor.vuechange here.What changed
packages/muya/src/editor/dragDropImage.ts(new) —attachDragDropImageHandlers(muya)bindsdragstart/dragover/dropon the editor container (+dragleavefor the drop-indicator ghost) viaeventCenter.attachDOMEvent, so cleanup rides onmuya.destroy() → detachAllDomEvents(). Wired inEditor.init()next toattachLinkMouseHandlers.text/uri-list): verify it is an image (extension or content-type sniff), insert.dataTransfer.files): resolve to a path via the newgetPathForFilehook, insert aplaceholder, persist through the newimageActionoption (same{ src, alt, title }contract theimageEditToolplugin already consumes), then swap in the returned src.packages/muya/src/types.ts— two optionalIMuyaOptionshooks (mirroring the existingclipboardFilePath):imageAction({ src, alt, title }) => Promise<string>andgetPathForFile(file) => string. The engine stays free ofwindow.electron.packages/muya/src/editor/__tests__/dragDropImage.spec.ts(new) — 5 unit tests driving the real handler with a syntheticDataTransfer.Test-first / automated vs manual
PG4 was a manual-QA-only entry because real drag gestures are hard headless. It turns out happy-dom provides a fully working
DataTransfer(items.add/getAsString/files) and firesgetAsStringsynchronously, so a syntheticdropevent drives the handler end-to-end. The spec asserts both drop paths against the live handler plus the no-op-off-target case (mutation-verified: removing theimageActioncall fails the test).PARITY_QA.md§ PG4 andPARITY_SCOREBOARD.mdare updated: the engine half is now automated, the remaining-gaps count drops 15 → 14, and the OS-integration steps stay manual.Wave-2 desktop change (follow-up, not in this PR)
The local-file persistence path needs
editor.vueto pass the two new engine options into theMuyaconstructoroptions:imageAction: muyaImageAction(already defined ineditor.vuefor the imageEditTool plugin)getPathForFile: (file) => window.electron.webUtils.getPathForFile(file)Without it, the web-link drop path works immediately, but a dropped local file inserts the raw path verbatim and the insert-action preference is ignored. Documented in
PARITY_QA.md§ PG4.Verification
pnpm -C packages/muyalint✅ ·lint:types✅ ·check-circular✅ (no new cycles) ·test✅ (517 pass / 20 xfail) ·test:spec✅ (1347, conformance unchanged).lint:cssfails on a pre-existing issue inblockSyntax.css/sequence-diagram.cssthat is already red onorigin/developand untouched here.🤖 Generated with Claude Code