fix(editor): use ProseMirror schema for image upload state

Address code review feedback:
- Replace rAF + DOM query with Image extension `uploading` attribute
  managed by ProseMirror schema (no race conditions)
- Remove redundant removeAttribute call (setNodeMarkup rebuilds DOM)
- Restore pulse animation on img[data-uploading] for upload feedback
- Remove dev mock from use-file-upload.ts (was blocking real uploads)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-04-02 16:36:59 +08:00
parent fe975fb2bb
commit b8b4731602
2 changed files with 22 additions and 23 deletions

View file

@ -241,14 +241,14 @@
margin: 0.5rem 0;
}
/* Uploading image placeholder (blob: URLs = in-flight uploads) */
.rich-text-editor img[src^="blob:"] {
/* Uploading image placeholder — data-uploading attribute managed by ProseMirror schema */
.rich-text-editor img[data-uploading] {
opacity: 0.5;
border-radius: var(--radius);
animation: rte-pulse 1.5s ease-in-out infinite;
animation: rte-upload-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes rte-pulse {
@keyframes rte-upload-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 0.3; }
}

View file

@ -150,23 +150,13 @@ function createFileUploadExtension(
const isImage = file.type.startsWith("image/");
if (isImage) {
// Instant preview via blob URL, then replace with real URL after upload
// Instant preview via blob URL with uploading flag for CSS styling
const blobUrl = URL.createObjectURL(file);
const imgAttrs = { src: blobUrl, alt: file.name, uploading: true };
if (pos !== undefined) {
editor
.chain()
.focus()
.insertContentAt(pos, {
type: "image",
attrs: { src: blobUrl, alt: file.name },
})
.run();
editor.chain().focus().insertContentAt(pos, { type: "image", attrs: imgAttrs }).run();
} else {
editor
.chain()
.focus()
.setImage({ src: blobUrl, alt: file.name })
.run();
editor.chain().focus().setImage(imgAttrs).run();
}
try {
@ -174,14 +164,12 @@ function createFileUploadExtension(
if (result) {
const { tr } = editor.state;
editor.state.doc.descendants((node, nodePos) => {
if (
node.type.name === "image" &&
node.attrs.src === blobUrl
) {
if (node.type.name === "image" && node.attrs.src === blobUrl) {
tr.setNodeMarkup(nodePos, undefined, {
...node.attrs,
src: result.link,
alt: result.filename,
uploading: false,
});
}
});
@ -330,7 +318,18 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
LinkExtension,
Typography,
MentionExtension,
Image.configure({
Image.extend({
addAttributes() {
return {
...this.parent?.(),
uploading: {
default: false,
renderHTML: (attrs) => (attrs.uploading ? { "data-uploading": "" } : {}),
parseHTML: (el) => el.hasAttribute("data-uploading"),
},
};
},
}).configure({
inline: false,
allowBase64: false,
HTMLAttributes: { style: "max-width: 100%; height: auto;" },