Files
multica/packages/views/editor/extensions/image-view.tsx
Naiyuan Qing 0dbe9f0a8f Move caret after inserted image uploads (#3796)
* fix editor image upload caret placement

Co-authored-by: multica-agent <github@multica.ai>

* feat(editor): reserve image box via intrinsic dimensions to kill paste layout shift (#3803)

Capture an image's intrinsic width/height on upload and render them as
<img width height> so the browser reserves the box before the image
decodes. Removes the layout shift that pushed the caret out of view after
a pasted-image insert, making the post-insert scrollIntoView correct.

- Add width/height node attrs to ImageExtension (render-only; not
  serialized to markdown, so round-trips stay clean).
- Measure dimensions off-thread via createImageBitmap and patch the node
  after insert. Fire-and-forget so the synchronous-insert contract
  (instant preview) is preserved; degrades to no-box when the API is
  unavailable (jsdom). The src swap keeps width/height via attr spread.
- Thread width/height through ImageView -> Attachment -> <img>.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:48:36 +08:00

47 lines
1.5 KiB
TypeScript

"use client";
/**
* ImageView — Tiptap NodeView for the image node.
*
* Thin wrapper around the unified `<Attachment>` dispatcher. All rendering
* (figure, hover toolbar, lightbox/preview) lives there. The NodeView only
* forwards Tiptap's editor-context hints (editable, selected, deleteNode).
*/
import { NodeViewWrapper } from "@tiptap/react";
import type { NodeViewProps } from "@tiptap/react";
import { Attachment } from "../attachment";
function ImageView({ node, editor, selected, deleteNode }: NodeViewProps) {
const src = (node.attrs.src as string) || "";
const alt = (node.attrs.alt as string) || "";
const uploading = node.attrs.uploading as boolean;
const width = (node.attrs.width as number | null) ?? undefined;
const height = (node.attrs.height as number | null) ?? undefined;
// <Attachment> emits its own .image-node wrapper, so the NodeViewWrapper
// stays unclassed — no double image-node.
return (
<NodeViewWrapper>
<Attachment
attachment={{
kind: "url",
url: src,
filename: alt,
uploading,
// Intrinsic dimensions reserve the <img> box pre-decode (no shift).
width,
height,
// Tiptap image node is structurally an image regardless of alt.
forceKind: "image",
}}
editable={editor.isEditable}
selected={selected}
onDelete={() => deleteNode()}
/>
</NodeViewWrapper>
);
}
export { ImageView };