import Image from "@tiptap/extension-image"; import { ImageOptions as DefaultImageOptions } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { mergeAttributes, Range } from "@tiptap/core"; export interface ImageOptions extends DefaultImageOptions { view: any; } export interface ImageAttributes { src?: string; alt?: string; title?: string; align?: string; attachmentId?: string; size?: number; width?: number; aspectRatio?: number; placeholderId?: string; } declare module "@tiptap/core" { interface Commands { imageBlock: { setImage: (attributes: ImageAttributes) => ReturnType; setImageAt: ( attributes: ImageAttributes & { pos: number | Range }, ) => ReturnType; setImageAlign: (align: "left" | "center" | "right") => ReturnType; setImageWidth: (width: number) => ReturnType; }; } } export const TiptapImage = Image.extend({ name: "image", inline: false, group: "block", isolating: true, atom: true, defining: true, addOptions() { return { ...this.parent?.(), view: null, }; }, addAttributes() { return { src: { default: "", parseHTML: (element) => element.getAttribute("src"), renderHTML: (attributes) => ({ src: attributes.src, }), }, width: { default: "100%", parseHTML: (element) => element.getAttribute("width"), renderHTML: (attributes: ImageAttributes) => ({ width: attributes.width, }), }, align: { default: "center", parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: ImageAttributes) => ({ "data-align": attributes.align, }), }, alt: { default: undefined, parseHTML: (element) => element.getAttribute("alt"), renderHTML: (attributes: ImageAttributes) => ({ alt: attributes.alt, }), }, attachmentId: { default: undefined, parseHTML: (element) => element.getAttribute("data-attachment-id"), renderHTML: (attributes: ImageAttributes) => ({ "data-attachment-id": attributes.attachmentId, }), }, size: { default: null, parseHTML: (element) => element.getAttribute("data-size"), renderHTML: (attributes: ImageAttributes) => ({ "data-size": attributes.size, }), }, placeholderId: { default: null, rendered: false, }, aspectRatio: { default: null, rendered: false, }, }; }, renderHTML({ HTMLAttributes }) { return [ "img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ]; }, addCommands() { return { setImage: (attrs: ImageAttributes) => ({ commands }) => { return commands.insertContent({ type: "image", attrs: attrs, }); }, setImageAt: (attrs) => ({ commands }) => { return commands.insertContentAt(attrs.pos, { type: "image", attrs: attrs, }); }, setImageAlign: (align) => ({ commands }) => commands.updateAttributes("image", { align }), setImageWidth: (width) => ({ commands }) => commands.updateAttributes("image", { width: `${Math.max(0, Math.min(100, width))}%`, }), }; }, addNodeView() { // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) this.editor.isInitialized = true; return ReactNodeViewRenderer(this.options.view); }, });