feat(muya): add ImagePathPicker floating autocomplete UI by Jocs · Pull Request #4386 · marktext/marktext · GitHub
Skip to content

feat(muya): add ImagePathPicker floating autocomplete UI - #4386

Merged
Jocs merged 2 commits into
developfrom
feat/muya-image-path-picker
Jun 8, 2026
Merged

Jocs merged 2 commits into
developfrom
feat/muya-image-path-picker

Conversation

@Jocs

@Jocs Jocs commented Jun 8, 2026

Copy link
Copy Markdown
Member

Parity gap

The legacy engine (packages/muyajs) ships an ImagePathPicker — a floating autocomplete dropdown that suggests local image file paths as the user edits an image's src (legacy: packages/muyajs/lib/ui/imagePicker/index.js, wired from packages/muyajs/lib/ui/imageSelector). The new packages/muya (@muyajs/core) had no equivalent floating UI — only a one-shot imagePathPicker file-dialog callback on the ImageEditTool, and no imagePathAutoComplete hook. This PR closes that gap as part of the desktop migration off legacy muya.

What's added

ImagePathPicker plugin — packages/muya/src/ui/imagePicker/

A BaseScrollFloat subclass (static pluginName = 'imagePathPicker') that faithfully mirrors the legacy UX:

  • Subscribes to the muya-image-picker event ({ reference, list, cb }).
  • Renders a scrollable, filtered list of suggestions ({ text, iconClass?, type?, … }).
  • Arrow-Up / Arrow-Down / Tab navigation + Enter / click to choose (inherited from BaseScrollFloat).
  • Reports the chosen item via the callback supplied in the event payload.
  • Follows new-muya conventions exactly (4-space indent, semicolons, I-prefixed interfaces, _-prefixed privates, snabbdom h/patch, query).

Exported from packages/muya/src/index.ts (with IImagePathSuggestion), placed in path-sorted order between ImageEditTool and ImageResizeBar.

imagePathAutoComplete hook + ImageEditTool wiring

  • New option on the ImageEditTool Options type (declared alongside the existing imagePathPicker / imageAction):
    imagePathAutoComplete?: (src: string) => Promise<IImagePathSuggestion[]>.
  • On keyup in the src input, the tool calls imagePathAutoComplete(currentSrc) and dispatches muya-image-picker so the floating picker refreshes (navigation keys are skipped to avoid re-fetching mid-navigation). The chosen path is written back as directory-portion + basename, matching the legacy autocomplete behavior.
  • On keydown, when the picker is open, Arrow/Tab/Enter drive the picker (navigate + choose) instead of confirming; otherwise Enter confirms as before.
  • The picker is dismissed when the tool hides, so confirm/close never leaves a dangling dropdown.
  • All existing ImageEditTool behavior (file-dialog imagePathPicker, imageAction upload flow, direct replace, file-protocol normalization) is unchanged.

The cross-plugin handle to the open picker is resolved via muya.ui.shownFloat (the same registry tableColumnToolbar already uses to find the format picker) — no edits to packages/muya/src/muya.ts (which is being changed in parallel).

Placeholder note (icons)

The legacy plugin rendered inline SVG assets (folder.svg / image.svg / upload.svg) mapped from iconClass. The new engine ships no inline SVG assets for UI plugins (icons are font-icon CSS classes, e.g. codeBlockLanguageSelector). So the picker renders iconClass directly as a span.<iconClass> font icon and omits the icon when none is supplied. A host wanting the exact legacy folder/image/upload glyphs should provide the corresponding icon-font classes (or a follow-up can add dedicated assets).

Tests

happy-dom vitest specs (following the existing footnoteTool UI-plugin test pattern):

  • packages/muya/src/ui/imagePicker/__tests__/imagePicker.spec.ts — stable pluginName; renders one item per suggestion (first active, icon class applied); hides on empty list; click invokes the callback; step('next') + select drives the active item.
  • packages/muya/src/ui/imageEditTool/__tests__/imageEditToolAutocomplete.spec.ts — typing in the src input calls imagePathAutoComplete with the current value and dispatches muya-image-picker with the result + reference; no dispatch when the hook is absent.

Verify (all green)

pnpm -C packages/muya: lint (0 errors), lint:types, check-circular, test (395 passed), test:spec (1347 passed).

Note: lint:css is unaffected by this change — it crashes on the pre-existing assets/styles/exportStyle.css via a stylelint-order plugin version incompatibility, independent of the new CSS (which mirrors codeBlockLanguageSelector/index.css ordering). It is not part of the required verify set.

Remaining integration note

The wiring is complete on the muya side. The desktop renderer must, when registering plugins, pass an imagePathAutoComplete implementation (a filesystem directory listing returning { text, iconClass, type }[]) alongside the existing imagePathPicker option — the same data source the legacy app fed to its imagePathAutoComplete. Without it, the ImageEditTool behaves exactly as before (no picker).

🤖 Generated with Claude Code

Port the legacy `imagePicker` plugin (packages/muyajs/lib/ui/imagePicker)
to the new TypeScript engine so the desktop migration off packages/muyajs
retains the local image-path autocomplete dropdown.

- Add `ImagePathPicker` (BaseScrollFloat subclass, pluginName
  "imagePathPicker") that renders a scrollable, keyboard-navigable list of
  path suggestions on the `muya-image-picker` event and reports the choice
  through the supplied callback.
- Wire it into ImageEditTool: the src input now calls the new
  `imagePathAutoComplete(src)` option on keyup and dispatches
  `muya-image-picker`; keydown routes arrows/Tab/Enter to the open picker.
  The picker is dismissed when the tool hides. All existing ImageEditTool
  behavior is preserved.
- Export `ImagePathPicker` + `IImagePathSuggestion` from the package entry.
- Add happy-dom vitest specs for the picker render/navigation/selection and
  for the ImageEditTool autocomplete wiring.

Icons are rendered via font-icon CSS classes from `iconClass` (the new
engine ships no inline SVG assets); see PR notes for the placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 8, 2026 04:20

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

This PR closes the feature gap between legacy packages/muyajs and new packages/muya by adding a floating ImagePathPicker autocomplete UI for local image src editing, plus an imagePathAutoComplete hook wired into ImageEditTool and accompanying happy-dom Vitest coverage.

Changes:

  • Added ImagePathPicker (BaseScrollFloat) plugin that renders and navigates a suggestion list driven by a muya-image-picker event.
  • Extended ImageEditTool with an imagePathAutoComplete option and keyboard wiring to show/drive the floating picker.
  • Exported the new plugin and suggestion interface from packages/muya/src/index.ts and added UI-level tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/muya/src/ui/imagePicker/index.ts New floating picker plugin implementation + suggestion interface.
packages/muya/src/ui/imagePicker/index.css Picker-specific styling for the floating dropdown.
packages/muya/src/ui/imagePicker/tests/imagePicker.spec.ts Happy-dom integration tests for picker render/selection behavior.
packages/muya/src/ui/imageEditTool/index.ts Adds imagePathAutoComplete hook + keydown/keyup behavior to show/drive picker.
packages/muya/src/ui/imageEditTool/tests/imageEditToolAutocomplete.spec.ts Tests ImageEditTool’s autocomplete hook dispatch behavior.
packages/muya/src/index.ts Exports ImagePathPicker and IImagePathSuggestion.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +207 to +217
switch (event.key) {
case EVENT_KEYS.ArrowUp:
event.preventDefault();
picker.step('previous');
break;

case EVENT_KEYS.ArrowDown:
case EVENT_KEYS.Tab:
event.preventDefault();
picker.step('next');
break;

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 8bf1edf. Added event.stopPropagation() to the Arrow/Tab cases (the Enter case already did this), so the editor's BaseScrollFloat keydown handler bound on muya.domNode no longer also steps the picker. No more double-advance per keypress.

Comment on lines +268 to +283
const { text } = item;
let basePath = '';
const pathSep = value.match(/(?:\/|\\)[^/\\]*$/);
if (pathSep && pathSep[0])
basePath = value.substring(0, pathSep.index! + 1);

const newValue = basePath + text;
const len = newValue.length;
reference.value = newValue;
this._state.src = newValue;
reference.focus();
reference.setSelectionRange(len, len);
};

const list = value ? await this.options.imagePathAutoComplete(value) : [];
eventCenter.emit('muya-image-picker', { reference, list, cb });

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 8bf1edf. Added a monotonic _autoCompleteSeq counter and drop the response if a newer keystroke superseded it (no more stale suggestions on fast typing). The selection callback now derives the directory prefix from the current input value (reference.value) at selection time rather than the value captured on keyup.

Comment on lines +21 to +26
.mu-image-picker-wrapper .mu-list-picker .item {
display: flex;
align-items: center;
height: 28px;
padding: 0 10px;

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 8bf1edf. Added justify-content: flex-start to .mu-image-picker-wrapper .mu-list-picker .item to override codeBlockLanguageSelector's global .mu-list-picker .item { justify-content: space-between }, so icon/text are no longer pushed apart.

Comment on lines +85 to +101
const children = renderArray.map((item) => {
const { text, iconClass } = item;
// Icons are font-icon classes (parity placeholder for the legacy
// inline SVGs — see PR notes). When the host omits `iconClass` we
// simply render the text without an icon.
const iconContent = iconClass
? [h('div.icon-wrapper', h(`span.${iconClass}`))]
: [];
const textEle = h('div.text', text);
const selector = activeItem === item ? 'li.item.active' : 'li.item';

return h(
selector,
{
dataset: {
label: text,
},

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 8bf1edf. Items are now keyed by a stable data-index (the map index) instead of data-label/text, so file names containing quotes/brackets no longer break the selector and duplicate basenames no longer collide. Test updated to query [data-index].

Comment on lines +122 to +126

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 8bf1edf. getItemElement now looks up via renderArray.indexOf(item) + [data-index="${index}"] (returning null when not found), matching the index-based attribute added in render — no more text-based selector injection / duplicate mismatches.

Five fixes from the Copilot review:
- imageEditTool keydown: stopPropagation on Arrow/Tab so the editor's
  BaseScrollFloat keydown handler (bound on muya.domNode) does not also
  step the picker, which advanced the active item twice per keypress
  (the Enter case already stopped propagation).
- imageEditTool autocomplete: guard against out-of-order resolution with a
  monotonic sequence counter (fast typing could emit stale suggestions),
  and derive the selected path's directory prefix from the CURRENT input
  value rather than the value captured on keyup.
- imagePicker CSS: explicitly set justify-content: flex-start to override
  codeBlockLanguageSelector's global `.mu-list-picker .item`
  justify-content: space-between, which spaced icon/text apart.
- imagePicker render/getItemElement: key items by a stable data-index
  instead of data-label (file names can contain quotes/brackets — unsafe in
  an attribute selector — and duplicate basenames collided). Test updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Jocs
Jocs merged commit f8b8638 into develop Jun 8, 2026
17 checks passed
@Jocs
Jocs deleted the feat/muya-image-path-picker branch June 8, 2026 04:45
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