Repository navigation
feat(muya): add ImagePathPicker floating autocomplete UI - #4386
Conversation
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>
There was a problem hiding this comment.
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 amuya-image-pickerevent. - Extended
ImageEditToolwith animagePathAutoCompleteoption and keyboard wiring to show/drive the floating picker. - Exported the new plugin and suggestion interface from
packages/muya/src/index.tsand 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
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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; |
There was a problem hiding this comment.
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.
| 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 }); |
There was a problem hiding this comment.
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.
| .mu-image-picker-wrapper .mu-list-picker .item { | ||
| display: flex; | ||
| align-items: center; | ||
| height: 28px; | ||
| padding: 0 10px; | ||
|
|
There was a problem hiding this comment.
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.
| 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, | ||
| }, |
There was a problem hiding this comment.
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].
There was a problem hiding this comment.
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>

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'ssrc(legacy:packages/muyajs/lib/ui/imagePicker/index.js, wired frompackages/muyajs/lib/ui/imageSelector). The newpackages/muya(@muyajs/core) had no equivalent floating UI — only a one-shotimagePathPickerfile-dialog callback on the ImageEditTool, and noimagePathAutoCompletehook. This PR closes that gap as part of the desktop migration off legacy muya.What's added
ImagePathPickerplugin —packages/muya/src/ui/imagePicker/A
BaseScrollFloatsubclass (static pluginName = 'imagePathPicker') that faithfully mirrors the legacy UX:muya-image-pickerevent ({ reference, list, cb }).{ text, iconClass?, type?, … }).BaseScrollFloat).I-prefixed interfaces,_-prefixed privates, snabbdomh/patch,query).Exported from
packages/muya/src/index.ts(withIImagePathSuggestion), placed in path-sorted order betweenImageEditToolandImageResizeBar.imagePathAutoCompletehook + ImageEditTool wiringOptionstype (declared alongside the existingimagePathPicker/imageAction):imagePathAutoComplete?: (src: string) => Promise<IImagePathSuggestion[]>.imagePathAutoComplete(currentSrc)and dispatchesmuya-image-pickerso the floating picker refreshes (navigation keys are skipped to avoid re-fetching mid-navigation). The chosen path is written back asdirectory-portion + basename, matching the legacy autocomplete behavior.imagePathPicker,imageActionupload flow, direct replace, file-protocol normalization) is unchanged.The cross-plugin handle to the open picker is resolved via
muya.ui.shownFloat(the same registrytableColumnToolbaralready uses to find the format picker) — no edits topackages/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 fromiconClass. The new engine ships no inline SVG assets for UI plugins (icons are font-icon CSS classes, e.g.codeBlockLanguageSelector). So the picker rendersiconClassdirectly as aspan.<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
footnoteToolUI-plugin test pattern):packages/muya/src/ui/imagePicker/__tests__/imagePicker.spec.ts— stablepluginName; 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 callsimagePathAutoCompletewith the current value and dispatchesmuya-image-pickerwith 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).Remaining integration note
The wiring is complete on the muya side. The desktop renderer must, when registering plugins, pass an
imagePathAutoCompleteimplementation (a filesystem directory listing returning{ text, iconClass, type }[]) alongside the existingimagePathPickeroption — the same data source the legacy app fed to itsimagePathAutoComplete. Without it, the ImageEditTool behaves exactly as before (no picker).🤖 Generated with Claude Code