Preserve user selection during rich text editor uploads
What does this MR do and why?
When an upload finished, the completion handlers moved the selection back to the uploaded node. If the user kept typing elsewhere during the upload, the caret was yanked onto the media node and the next keystroke replaced the just-uploaded image. Deleting the placeholder mid-upload crashed the handlers and dropped the failure alert.
Completion handlers now patch the node positionally and leave the user's selection alone. ProseMirror maps the selection through positional transactions, so it stays where the user left it. A selection still sitting on the uploading node is restored after the patch, which keeps the upload-complete signal unchanged. A deleted placeholder now discards the result on success and still shows the alert on failure. The attachment variant covers the whole link even when styling has split its text into several nodes, and the selection restore is occurrence-precise when copies of an uploading placeholder share a file id.
- Design intent: selecting the uploaded node on completion was a deliberate upload-complete signal, per the maintainer discussion in issue 390233. That behavior is unchanged whenever the user has not moved: a selection still sitting on the uploading node is restored after the patch. The restore is identity-based, checking the selection's node, never position-based, since a caret at the same position usually means the user is typing just before the image. The same restore covers the async dimension probe, which previously degraded the selection mid-upload even for an idle user.
- Scope: this fixes the mid-upload deletion and the crashes in issue 554669. The other half of that issue (media failing to persist when the form is submitted while an upload is still in flight) is a separate defect and remains open. Insert-time selection is also deliberately untouched: upload progress renders in the selection-gated bubble menu, so changing it is blocked on communication of upload progress (#410080). See also #428085.
Related to #554669 Related to #390233 (closed)
References
- Issue 554669 names the mechanism in its description: media "gets deleted mid-upload due to the editor returning focus to the upload position during active typing."
- User report of the deletion in practice: on a slow connection, "this causes me to delete images all the time ... Focus goes back to the image and it gets deleted accidentally and I often don't notice."
- Maintainer note on the intended completion signal, which this MR preserves for the unmoved case.
- Test evidence: 15 new Jest specs, the core ones failing on master: the deletion reproduced exactly (after completion with the caret moved elsewhere, a single keystroke replaced the entire image node), the placeholder-deleted crashes (unhandled
TypeError/RangeErrorand a lost error alert), a style-split placeholder link only partially patched or removed, and a duplicated placeholder yanking the selection. After the fix, both spec files pass 66/66 under Vue 2 and Vue 3, and the fullspec/frontend/content_editor/tree stays green (125 suites, 1173 tests, both variants). - The completion re-select dates to contextual menu options for images (!85761). Improve file upload experience (!119156) fixed the loud half of this class (typing while uploading failed the upload (#406718), severity::1) by re-finding the node at completion; the selection steal was the quiet residual.
- Accepted precedent for addressing uploads by file id instead of selection: fix dimension appending for attachments (!240055).
Screenshots or screen recordings
| Before | After |
|---|---|
![]() |
![]() |
Staged: typed a sentence, uploaded dk.png through a 5s server-side hold, clicked back into the sentence mid-upload and kept typing, kept typing after completion. Before: completion selects the just-uploaded image and the next keystrokes replace it; the trailing words land alone where the image was. After: the caret stays in the sentence and the image survives.
Recording
| Before (recording) | After (recording) |
|---|---|
![]() |
![]() |
The stills' staging re-captured as four frames (upload hold widened to 8s to fit the mid-upload frames): sentence typed and upload starts, typing mid-upload, upload completes, kept typing. Before: completion re-selects the image (the media menu pops over the sentence) and the next keystrokes replace it. After: the caret stays in the sentence and the image survives.
How to set up and validate locally
- Open any rich text editor surface, for example an issue comment box switched to rich text editing.
- In DevTools, Network tab, set throttling to Slow 3G so uploads take several seconds.
- Type a sentence, then paste or drag-and-drop an image. The placeholder appears with a progress indicator.
- Click back into the middle of the sentence immediately and keep typing until the upload completes.
- Before the fix: on completion the caret jumps and the image becomes selected, so the next keystroke silently replaces the image. After the fix: the caret never moves, typing lands where the caret is, and the image swaps to its final URL in place.
- Placeholder-deleted case: paste an image, then select all and press Delete while it uploads. Before: the console shows an unhandled
RangeError(media) orTypeError(attachments), and a failed upload shows no error alert. After: no console errors, the document stays as the user left it, and a failed upload still shows the alert. - Attachment variant: repeat with a
.zipfile (link placeholder). Before: completion selects the link text, and a failure yanks the caret to the deletion point. After: the caret stays put. - Unmoved case: paste an image and leave it alone. The image is selected when the upload completes, exactly as before this change. Repeat with a large image so the dimension probe fires: same result.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.



