mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* 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>
47 lines
1.5 KiB
TypeScript
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 };
|