Files
gitmost/packages/editor-ext/src/lib/image/image.ts
Philip Okugbe 657fdf8cb7 feat: Tiptap V3 migration (#1854)
* Tiptap3 migration - WIP

* fix collaboration

* remove unused code

* fix flicker

* disable duplicate extensions

* update tiptap version

* Switch to useEditorState
- Set shouldRerenderOnTransaction to false

* fix editable state

* add tippyoptions for reference

* merge main

* tiptap 3.6.1

* fix bubble menu

* fix converter

* fix menus

* fix collaboration caret css

* fix: Set `isInitialized` to force immediate react node view rendering

* feat: Migrate tippy.js menus to Floating UI

* feat: Update collaboration connection for HocusPocus v3

* fix: Connect/disconnect websocketProvider

* cleanup

* cleanup

* feat: Improved placeholder and upload handling for images

* feat: Improved placeholder and upload handling for videos

* refactor: Image node and view clean-up

* feat: Improved placeholder and upload handling for attachments

* fix: Video view styles

* fix: Transaction handling on asset upload

* fix: Use imageDimensionsFromStream

* feat: Multiple file upload, improved placeholders, local previews

* fix: Drag & drop, paste upload

* fix: Allow media as attachment

* * add skeleton pulse animation
* add translation strings
* fix attachment view responsiveness

* fix collab connection status display

* Tiptap v3.17.0

* fix suggestion menu exit bug

* fix search shortcut

* fix history editor css

* tiptap 3.17.1

---------

Co-authored-by: Arek Nawo <areknawo@areknawo.com>
2026-01-24 20:41:08 +00:00

158 lines
4.0 KiB
TypeScript

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;
align?: string;
attachmentId?: string;
size?: number;
width?: number;
aspectRatio?: number;
placeholder?: {
id: string;
name: string;
};
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
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<ImageOptions>({
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,
}),
},
aspectRatio: {
default: null,
parseHTML: (element) => element.getAttribute("data-aspect-ratio"),
renderHTML: (attributes: ImageAttributes) => ({
"data-aspect-ratio": attributes.aspectRatio,
}),
},
placeholder: {
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);
},
});