Skip to content

feat(muya): restore drag-and-drop image insertion (PG4) - #4413

Merged
Jocs merged 3 commits into
developfrom
feat/muya-image-drag-drop
Jun 8, 2026
Merged

Jocs merged 3 commits into
developfrom
feat/muya-image-drag-drop

Conversation

@Jocs

@Jocs Jocs commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Parity follow-up to #4406 (the muyajs → @muyajs/core migration). 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 legacy dragDrop/dragDropCtrl behaviour as an engine-level, embedder-agnostic handler. ENGINE-ONLY: no desktop editor.vue change here.

What changed

  • packages/muya/src/editor/dragDropImage.ts (new) — attachDragDropImageHandlers(muya) binds dragstart/dragover/drop on the editor container (+ dragleave for the drop-indicator ghost) via eventCenter.attachDOMEvent, so cleanup rides on muya.destroy() → detachAllDomEvents(). Wired in Editor.init() next to attachLinkMouseHandlers.
    • web-link image (text/uri-list): verify it is an image (extension or content-type sniff), insert ![](url).
    • local image FILE (dataTransfer.files): resolve to a path via the new getPathForFile hook, insert a ![loading-id](path) placeholder, persist through the new imageAction option (same { src, alt, title } contract the imageEditTool plugin already consumes), then swap in the returned src.
  • packages/muya/src/types.ts — two optional IMuyaOptions hooks (mirroring the existing clipboardFilePath): imageAction({ src, alt, title }) => Promise<string> and getPathForFile(file) => string. The engine stays free of window.electron.
  • packages/muya/src/editor/__tests__/dragDropImage.spec.ts (new) — 5 unit tests driving the real handler with a synthetic DataTransfer.

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 fires getAsString synchronously, so a synthetic drop event 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 the imageAction call fails the test).

PARITY_QA.md § PG4 and PARITY_SCOREBOARD.md are 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.vue to pass the two new engine options into the Muya constructor options:

  • imageAction: muyaImageAction (already defined in editor.vue for 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/muya lint ✅ · lint:types ✅ · check-circular ✅ (no new cycles) · test ✅ (517 pass / 20 xfail) · test:spec ✅ (1347, conformance unchanged). lint:css fails on a pre-existing issue in blockSyntax.css / sequence-diagram.css that is already red on origin/develop and untouched here.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 8, 2026 17:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via imageAction.
  • Extend IMuyaOptions with imageAction and getPathForFile hooks 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.

Comment on lines +77 to +84
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`,
});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +189 to +195
const { name } = image;
const loadingId = `loading-${getUniqueId()}`;
insertImageParagraph(muya, target, `![${loadingId}](${path})`);

void persistDroppedImage(muya, path, name, loadingId);

return true;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in b0d2696. When options.imageAction is absent, handleFileImage now inserts a clean ![name](path) 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.

Comment on lines +214 to +219
const hasImageFile
= dataTransfer.items.length === 1
&& dataTransfer.items[0].type.includes('image');
const hasUriList = dataTransfer.types.includes('text/uri-list');
if (!hasImageFile && !hasUriList)
return;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +239 to +244
// 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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +177 to +179
const dt = new DataTransfer();
dt.items.add('https://example.com/pic.png', 'text/uri-list');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +194 to +196
const dt = new DataTransfer();
dt.items.add('not-a-url', 'text/uri-list');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@github-actions

github-actions Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Build artifacts for PR #4413:

Run: https://github.com/marktext/marktext/actions/runs/27156469291

Artifact Size Link
marktext-macos-x64 256.8 MB Download
marktext-windows-arm64 256.2 MB Download
marktext-linux 556.4 MB Download
marktext-windows-x64 257.5 MB Download
marktext-macos-arm64 246.6 MB Download

Jocs added a commit that referenced this pull request Jun 8, 2026
…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 `![name](path)`
  with the raw path verbatim instead of a `![loading-id](path)` 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]>
Jocs and others added 3 commits June 9, 2026 01:49
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 `![](url)`.
- local image FILE (`dataTransfer.files`): resolve the file to a path via
  the new embedder `getPathForFile` hook, insert a `![loading-id](path)`
  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 `![name](path)`
  with the raw path verbatim instead of a `![loading-id](path)` 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]>
@Jocs
Jocs force-pushed the feat/muya-image-drag-drop branch from b0d2696 to 22020cb Compare June 8, 2026 17:51
@Jocs
Jocs merged commit 410a7ca into develop Jun 8, 2026
15 checks passed
@Jocs
Jocs deleted the feat/muya-image-drag-drop branch June 10, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants