fix(muya): take the image references from the live DOM after selectin… · marktext/marktext@82e5201 · GitHub
Skip to content

Commit 82e5201

Browse files
Jocsclaude
andauthored
fix(muya): take the image references from the live DOM after selecting (#5391) (#5534)
Clicking an image emitted `muya-transformer` (and the image toolbar's rect) from the nodes that were in the document at click time, and only then called `selectImage`. That call nulls `editor.activeContentBlock`, which runs `Format.blurHandler` -> `checkNeedRender()` -> `update()`, and the inline renderer's `patch` reassigns the paragraph's `innerHTML`. Every node inside it, the clicked image's wrapper and container included, is replaced. `checkNeedRender()` is true whenever the caret sits within one character of a token's range, so a caret anywhere on the image's own line always triggers it -- the state a document that opens on an image is already in, since `Editor.init()` focuses the first block at offset 0. `ImageResizeBar` renders on a `setTimeout`, so by then it held a detached container: an all-zero bounding rect, drawing both handles at `left: -5px; top: -25px` instead of on the image's edges, where only a sliver shows and dragging cannot resize. `autoUpdate` never recovers, because a detached node never moves. A second click found the block already blurred, hence no re-render and correct handles. Select first, then read both references off the live DOM. The clicked image is re-found by its position among the paragraph's images, which survives the re-render -- ids do not identify it (images sharing a src and paragraph offset render with the same id). Taking the toolbar's rect afterwards too fixes the second manifestation: when the caret revealed markers of another token in the paragraph, hiding them on blur reflows the line and shifts the image sideways, so the pre-render rect left the toolbar behind by that offset. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 69f93e4 commit 82e5201

3 files changed

Lines changed: 218 additions & 20 deletions

File tree

Lines changed: 81 additions & 0 deletions

‎packages/muya/src/selection/ImageSelection.ts‎

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ class ImageSelection {
8383
}
8484
}
8585

86+
private _imageWrappers(contentDom: HTMLElement): HTMLElement[] {
87+
return Array.from(
88+
contentDom.querySelectorAll<HTMLElement>(`.${CLASS_NAMES.MU_INLINE_IMAGE}`),
89+
);
90+
}
91+
8692
private _handleClickInlineImage(event: Event, imageWrapper: HTMLElement) {
8793
event.preventDefault();
8894
event.stopPropagation();
@@ -130,38 +136,44 @@ class ImageSelection {
130136
}
131137
}
132138

133-
const rect = imageWrapper
134-
.querySelector(`.${CLASS_NAMES.MU_IMAGE_CONTAINER}`)
135-
?.getBoundingClientRect();
136-
const reference = {
137-
getBoundingClientRect: () => rect,
138-
width: imageWrapper.offsetWidth,
139-
height: imageWrapper.offsetHeight,
140-
};
139+
// Identify the clicked image by its position among the paragraph's
140+
// images, which survives the re-render below — the node does not,
141+
// and ids are not unique (images sharing a src and paragraph offset
142+
// render with the same id, so a `#id` lookup finds the first one).
143+
const imageIndex = this._imageWrappers(contentDom).indexOf(imageWrapper);
144+
145+
// Selecting the image blurs the paragraph, and `Format.blurHandler`
146+
// re-renders it whenever the caret sat on a token's markers — which
147+
// a caret on the image's own line always does. The re-render
148+
// reassigns the paragraph's `innerHTML`, detaching `imageWrapper`,
149+
// and the markers it hides can shift the image sideways. So select
150+
// first and take both references off the live DOM afterwards: from
151+
// the detached node the resize bar reads a zero rect and draws its
152+
// handles in the window's corner, and the toolbar a stale one (#5391).
153+
this._selection.selectImage(Object.assign({}, imageInfo, { block: contentBlock }));
154+
155+
const liveWrapper = this._imageWrappers(contentDom)[imageIndex] ?? imageWrapper;
156+
const imageContainer = liveWrapper.querySelector(
157+
`.${CLASS_NAMES.MU_IMAGE_CONTAINER}`,
158+
);
159+
const rect = imageContainer?.getBoundingClientRect();
141160

142161
eventCenter.emit('muya-image-toolbar', {
143162
block: contentBlock,
144-
reference,
163+
reference: {
164+
getBoundingClientRect: () => rect,
165+
width: liveWrapper.offsetWidth,
166+
height: liveWrapper.offsetHeight,
167+
},
145168
imageInfo,
146169
});
147170

148-
// Resolve the image container from the clicked wrapper directly.
149-
// Images that share the same src (and paragraph offset) render with
150-
// duplicate DOM ids, so a `document.querySelector('#id ...')` lookup
151-
// would resolve to the first occurrence and place the resize bar on
152-
// the wrong image.
153-
const imageContainer = imageWrapper.querySelector(
154-
`.${CLASS_NAMES.MU_IMAGE_CONTAINER}`,
155-
);
156-
157171
eventCenter.emit('muya-transformer', {
158172
block: contentBlock,
159173
reference: imageContainer,
160174
imageInfo,
161175
});
162176

163-
this._selection.selectImage(Object.assign({}, imageInfo, { block: contentBlock }));
164-
165177
return;
166178
}
167179

Lines changed: 105 additions & 0 deletions

0 commit comments

Comments
 (0)