fix: image insertion at pos and loading effect added

This commit is contained in:
Palanikannan M
2024-09-12 16:26:00 +05:30
parent 56a0b9f236
commit d8308cfce4
10 changed files with 50 additions and 87 deletions

View File

@@ -189,16 +189,17 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
icon: TableIcon,
});
export const ImageItem = (editor: Editor, uploadFile: UploadImage) =>
export const ImageItem = (editor: Editor) =>
({
key: "image",
name: "Image",
isActive: () => editor?.isActive("image"),
command: (savedSelection: Selection | null) => editor?.commands.setImageUpload({ event: "insert" }),
command: (savedSelection: Selection | null) =>
editor?.commands.setImageUpload({ event: "insert", pos: savedSelection?.from }),
icon: ImageIcon,
}) as const;
export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImage) {
export function getEditorMenuItems(editor: Editor | null) {
if (!editor) {
return [];
}
@@ -220,6 +221,6 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag
NumberedListItem(editor),
QuoteItem(editor),
TableItem(editor),
ImageItem(editor, uploadFile),
ImageItem(editor),
];
}

View File

@@ -11,7 +11,7 @@ import { CustomCodeInlineExtension } from "./code-inline";
import { CustomLinkExtension } from "./custom-link";
import { CustomHorizontalRule } from "./horizontal-rule";
import { ImageExtensionWithoutProps } from "./image";
import { ImageBlockWithoutProps } from "./image/image-block-without-props";
import { CustomImageComponentWithoutProps } from "./image/image-component-without-props";
import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props";
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
import { CustomQuoteExtension } from "./quote";
@@ -62,7 +62,7 @@ export const CoreEditorExtensionsWithoutProps = [
class: "rounded-md",
},
}),
ImageBlockWithoutProps(),
CustomImageComponentWithoutProps(),
TiptapUnderline,
TextStyle,
TaskList.configure({

View File

@@ -30,7 +30,7 @@ import {
import { isValidHttpUrl } from "@/helpers/common";
// types
import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
import { ImageBlock } from "./image-block";
import { CustomImageComponent } from "./image-block";
type TArguments = {
enableHistory: boolean;
@@ -108,7 +108,7 @@ export const CoreEditorExtensions = ({
class: "rounded-md",
},
}),
ImageBlock({
CustomImageComponent({
deleteFile,
restoreFile,
uploadFile,

View File

@@ -1,6 +1,7 @@
import React, { useRef, useState, useCallback, useEffect } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Editor } from "@tiptap/react";
import { ImageShimmer } from "./image-loader";
interface ImageBlockViewProps {
editor: Editor;
@@ -27,6 +28,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
const imageRef = useRef<HTMLImageElement>(null);
const isResizing = useRef(false);
const aspectRatio = useRef(1);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (imageRef.current) {
@@ -39,6 +41,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
const newHeight = newWidth / aspectRatio.current;
setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
}
setIsLoading(false);
};
}
}, [src, width, height]);
@@ -97,6 +100,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
return (
<div ref={containerRef} className="relative inline-block" onMouseDown={handleMouseDown} data-drag-handle>
{isLoading ? <ImageShimmer width={size.width} height={size.height} /> : null}
<img
ref={imageRef}
src={src}
@@ -105,6 +109,7 @@ export const ImageBlockView: React.FC<ImageBlockViewProps> = (props) => {
style={{
width: size.width,
height: size.height,
display: isLoading ? "none" : "block",
}}
/>
{isSelected && (

View File

@@ -0,0 +1,3 @@
export const ImageShimmer: React.FC<{ width: string; height: string }> = ({ width, height }) => (
<div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />
);

View File

@@ -1,4 +1,4 @@
import { mergeAttributes, Range } from "@tiptap/core";
import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
@@ -9,19 +9,14 @@ import { ImageUpload } from "../image-upload/view";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
imageBlock: {
setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType;
setImageBlockAt: (attributes: {
src: string;
pos: number | Range;
width?: number;
height?: number;
}) => ReturnType;
imageComponent: {
setImageUpload: ({ file, pos, event }: { file?: File; pos?: number; event: "insert" | "drop" }) => ReturnType;
uploadImage: (file: File) => () => Promise<string> | undefined;
};
}
}
export const ImageBlock = ({
export const CustomImageComponent = ({
uploadFile,
// deleteFile,
// restoreFile,
@@ -33,7 +28,7 @@ export const ImageBlock = ({
cancelUploadImage?: () => void;
}) =>
Image.extend<{}, UploadImageExtensionStorage>({
name: "imageBlock",
name: "imageComponent",
group: "inline",
draggable: true,
@@ -64,13 +59,13 @@ export const ImageBlock = ({
parseHTML() {
return [
{
tag: "image-block",
tag: "image-component",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["image-block", mergeAttributes(HTMLAttributes)];
return ["image-component", mergeAttributes(HTMLAttributes)];
},
addStorage() {
@@ -81,31 +76,17 @@ export const ImageBlock = ({
addCommands() {
return {
setImageBlock:
(attrs) =>
({ commands }) =>
commands.insertContent({
type: this.name,
attrs: { src: attrs.src },
}),
setImageBlockAt:
(attrs) =>
({ commands }) =>
commands.insertContentAt(attrs.pos, {
type: this.name,
attrs: { src: attrs.src },
}),
setImageUpload:
(props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) =>
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
({ commands }) => {
const fileId = uuidv4();
if (props?.file && props?.event === "drop") {
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
if (props?.event === "drop" && props.file) {
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
file: props.file,
event: props.event,
});
} else if (props.event !== "drop") {
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
} else if (props.event === "insert") {
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
event: props.event,
});
}
@@ -115,6 +96,12 @@ export const ImageBlock = ({
"data-file": props?.file ? `data-file="${props.file}"` : "",
};
if (props.pos) {
return commands.insertContentAt(props.pos, {
type: this.name,
attrs: attributes,
});
}
return commands.insertContent({
type: this.name,
attrs: attributes,
@@ -134,4 +121,4 @@ export const ImageBlock = ({
inline: true,
});
export default ImageBlock;
export default CustomImageComponent;

View File

@@ -3,30 +3,11 @@ import { v4 as uuidv4 } from "uuid";
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
import { ImageUpload as ImageUploadComponent } from "./view/image-upload";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
imageUpload: {
setImageUpload: ({
file,
pos,
event,
}: {
file?: File;
pos?: number;
event: "insert" | "replace" | "drop";
}) => ReturnType;
uploadImage: (file: File) => () => Promise<string> | undefined;
restoreImage: (assetUrlWithWorkspaceId: string) => Promise<ReturnType>;
deleteImage: (assetUrlWithWorkspaceId: string) => Promise<ReturnType>;
};
}
}
export interface UploadImageExtensionStorage {
fileMap: Map<string, UploadEntity>;
}
export type UploadEntity = ({ event: "insert" } | { event: "replace" } | { event: "drop"; file: File }) & {
export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & {
pos?: number;
};
@@ -91,16 +72,16 @@ export const ImageUpload = ({
addCommands() {
return {
setImageUpload:
(props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) =>
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
({ commands }) => {
const fileId = uuidv4();
if (props?.file && props?.event === "drop") {
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
file: props.file,
event: props.event,
});
} else if (props.event !== "drop") {
(this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, {
(this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, {
event: props.event,
});
}

View File

@@ -24,7 +24,7 @@ export const ImageUpload: React.FC<ImageUploadProps> = ({ getPos, editor, node,
const [isUploaded, setIsUploaded] = useState(!!node.attrs.src);
const id = node.attrs.id as string;
const editorStorage = editor.storage.imageBlock as UploadImageExtensionStorage | undefined;
const editorStorage = editor.storage.imageComponent as UploadImageExtensionStorage | undefined;
const getUploadEntity = useCallback(
(): UploadEntity | undefined => editorStorage?.fileMap.get(id),

View File

@@ -1,24 +1,10 @@
import { mergeAttributes, Range } from "@tiptap/core";
import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { UploadImageExtensionStorage } from "../image-upload";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
imageBlock: {
setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType;
setImageBlockAt: (attributes: {
src: string;
pos: number | Range;
width?: number;
height?: number;
}) => ReturnType;
};
}
}
export const ImageBlockWithoutProps = () =>
export const CustomImageComponentWithoutProps = () =>
Image.extend<{}, UploadImageExtensionStorage>({
name: "imageBlock",
name: "imageComponent",
group: "inline",
draggable: true,
@@ -49,13 +35,13 @@ export const ImageBlockWithoutProps = () =>
parseHTML() {
return [
{
tag: "image-block",
tag: "image-component",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["image-block", mergeAttributes(HTMLAttributes)];
return ["image-component", mergeAttributes(HTMLAttributes)];
},
addStorage() {
@@ -67,4 +53,4 @@ export const ImageBlockWithoutProps = () =>
inline: true,
});
export default ImageBlockWithoutProps;
export default CustomImageComponentWithoutProps;

View File

@@ -64,7 +64,7 @@
pointer-events: none;
}
&.node-imageUpload {
&.node-imageComponent {
--horizontal-offset: 0px;
&::after {
@@ -104,7 +104,7 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after {
margin-left: -35px;
}
.ProseMirror node-imageBlock {
.ProseMirror node-imageComponent {
transition: filter 0.1s ease-in-out;
cursor: pointer;