refactor: custom image extension folder structure

This commit is contained in:
Aaryan Khandelwal
2024-09-13 15:50:26 +05:30
parent 2583038b0a
commit f8d85fd633
16 changed files with 104 additions and 54 deletions
@@ -1,3 +0,0 @@
export const ImageShimmer: React.FC<{ width: string; height: string }> = ({ width, height }) => (
<div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />
);
@@ -1 +0,0 @@
export * from "./image-upload";
@@ -1 +0,0 @@
export * from "./custom-image";
@@ -1,29 +1,13 @@
import React, { useRef, useState, useCallback, useLayoutEffect } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { NodeSelection } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react";
// extensions
import { CustomImageNodeViewProps } from "@/extensions/custom-image";
// helpers
import { cn } from "@/helpers/common";
// components
import { ImageShimmer } from "./image-loader";
interface ImageBlockViewProps {
editor: Editor;
getPos: () => number;
node: ProsemirrorNode & {
attrs: {
src: string;
width: string;
height: string;
};
};
updateAttributes: (attrs: Record<string, any>) => void;
selected: boolean;
}
const MIN_SIZE = 100;
export const ImageComponent: React.FC<ImageBlockViewProps> = (props) => {
export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
const { node, updateAttributes, selected, getPos, editor } = props;
const { src, width, height } = node.attrs;
@@ -111,7 +95,7 @@ export const ImageComponent: React.FC<ImageBlockViewProps> = (props) => {
height: size.height,
}}
>
{isLoading && <ImageShimmer width={size.width} height={size.height} />}
{isLoading && <div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />}
<img
ref={imageRef}
src={src}
@@ -123,7 +107,7 @@ export const ImageComponent: React.FC<ImageBlockViewProps> = (props) => {
height: size.height,
}}
/>
{selected && <div className="absolute inset-0 size-full bg-custom-primary-500/30" />}
{editor.isEditable && selected && <div className="absolute inset-0 size-full bg-custom-primary-500/30" />}
<>
<div className="opacity-0 group-hover/image-component:opacity-100 absolute inset-0 border-2 border-custom-primary-100 pointer-events-none rounded-md transition-opacity duration-100 ease-in-out" />
<div
@@ -134,5 +118,3 @@ export const ImageComponent: React.FC<ImageBlockViewProps> = (props) => {
</div>
);
};
export default ImageComponent;
@@ -1,11 +1,15 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Editor, NodeViewWrapper } from "@tiptap/react";
import { UploadEntity, UploadImageExtensionStorage } from "@/extensions/custom-image-component";
import ImageComponent from "@/extensions/custom-image-component/components/image-block-view";
import { ImageUploader } from "./image-uploader";
// extensions
import {
CustomImageBlock,
CustomImageUploader,
UploadEntity,
UploadImageExtensionStorage,
} from "@/extensions/custom-image";
interface ImageUploadProps {
export type CustomImageNodeViewProps = {
getPos: () => number;
editor: Editor;
node: ProsemirrorNode & {
@@ -17,9 +21,9 @@ interface ImageUploadProps {
};
updateAttributes: (attrs: Record<string, any>) => void;
selected: boolean;
}
};
export const CustomImage = (props: ImageUploadProps) => {
export const CustomImageNode = (props: CustomImageNodeViewProps) => {
const { getPos, editor, node, updateAttributes, selected } = props;
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -89,7 +93,7 @@ export const CustomImage = (props: ImageUploadProps) => {
<NodeViewWrapper>
<div className="p-0 mx-0 my-2" data-drag-handle>
{isUploaded ? (
<ImageComponent
<CustomImageBlock
editor={editor}
getPos={getPos}
node={node}
@@ -97,7 +101,7 @@ export const CustomImage = (props: ImageUploadProps) => {
selected={selected}
/>
) : (
<ImageUploader
<CustomImageUploader
onUpload={onUpload}
editor={editor}
fileInputRef={fileInputRef}
@@ -109,5 +113,3 @@ export const CustomImage = (props: ImageUploadProps) => {
</NodeViewWrapper>
);
};
export default CustomImage;
@@ -5,6 +5,7 @@ import { ImageIcon } from "lucide-react";
import { cn } from "@/helpers/common";
// hooks
import { useUploader, useFileUpload, useDropZone } from "@/hooks/use-file-upload";
// plugins
import { isFileValid } from "@/plugins/image";
type RefType = React.RefObject<HTMLInputElement> | ((instance: HTMLInputElement | null) => void);
@@ -17,7 +18,7 @@ const assignRef = (ref: RefType, value: HTMLInputElement | null) => {
}
};
export const ImageUploader = (props: {
export const CustomImageUploader = (props: {
onUpload: (url: string) => void;
editor: Editor;
fileInputRef: RefType;
@@ -87,5 +88,3 @@ export const ImageUploader = (props: {
</div>
);
};
export default ImageUploader;
@@ -0,0 +1,3 @@
export * from "./image-block";
export * from "./image-node";
export * from "./image-uploader";
@@ -2,11 +2,12 @@ import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
import { TFileHandler } from "@/types";
import { CustomImage } from "./image-upload";
// extensions
import { CustomImageNode } from "@/extensions/custom-image";
// plugins
import { isFileValid } from "@/plugins/image";
// types
import { TFileHandler } from "@/types";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
@@ -23,7 +24,7 @@ export interface UploadImageExtensionStorage {
export type UploadEntity = { event: "insert" } | { event: "drop"; file: File };
export const CustomImageComponent = (props: TFileHandler) => {
export const CustomImageExtension = (props: TFileHandler) => {
const { upload } = props;
return Image.extend<{}, UploadImageExtensionStorage>({
@@ -124,9 +125,7 @@ export const CustomImageComponent = (props: TFileHandler) => {
},
addNodeView() {
return ReactNodeViewRenderer(CustomImage);
return ReactNodeViewRenderer(CustomImageNode);
},
});
};
export default CustomImageComponent;
@@ -0,0 +1,3 @@
export * from "./components";
export * from "./custom-image";
export * from "./read-only-custom-image";
@@ -0,0 +1,60 @@
import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { ReactNodeViewRenderer } from "@tiptap/react";
// components
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
export const CustomReadOnlyImageExtension = () =>
Image.extend<{}, UploadImageExtensionStorage>({
name: "imageComponent",
selectable: true,
group: "block",
atom: true,
draggable: true,
addAttributes() {
return {
...this.parent?.(),
width: {
default: "35%",
},
src: {
default: null,
},
height: {
default: "auto",
},
["data-type"]: {
default: this.name,
},
["data-file"]: {
default: null,
},
["id"]: {
default: null,
},
};
},
parseHTML() {
return [
{
tag: "image-component",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["image-component", mergeAttributes(HTMLAttributes)];
},
addStorage() {
return {
fileMap: new Map(),
};
},
addNodeView() {
return ReactNodeViewRenderer(CustomImageNode);
},
});
@@ -13,6 +13,7 @@ import {
CustomCodeInlineExtension,
CustomCodeMarkPlugin,
CustomHorizontalRule,
CustomImageExtension,
CustomKeymap,
CustomLinkExtension,
CustomMention,
@@ -30,7 +31,6 @@ import {
import { isValidHttpUrl } from "@/helpers/common";
// types
import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
import { CustomImageComponent } from "./custom-image-component";
type TArguments = {
enableHistory: boolean;
@@ -106,7 +106,7 @@ export const CoreEditorExtensions = ({
class: "rounded-md",
},
}),
CustomImageComponent({
CustomImageExtension({
delete: deleteFile,
restore: restoreFile,
upload: uploadFile,
@@ -1,6 +1,6 @@
import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { UploadImageExtensionStorage } from "@/extensions/custom-image-component";
import { UploadImageExtensionStorage } from "@/extensions/custom-image";
export const CustomImageComponentWithoutProps = () =>
Image.extend<{}, UploadImageExtensionStorage>({
@@ -1,5 +1,6 @@
export * from "./code";
export * from "./code-inline";
export * from "./custom-image";
export * from "./custom-link";
export * from "./custom-list-keymap";
export * from "./image";
@@ -19,6 +19,7 @@ import {
TableRow,
Table,
CustomMention,
CustomReadOnlyImageExtension,
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
@@ -74,6 +75,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
class: "rounded-md",
},
}),
CustomReadOnlyImageExtension(),
TiptapUnderline,
TextStyle,
TaskList.configure({
+3 -1
View File
@@ -69,7 +69,9 @@ export const isCommentEmpty = (comment: string | undefined): boolean => {
// return true if comment is undefined
if (!comment) return true;
return (
comment?.trim() === "" || comment === "<p></p>" || isEmptyHtmlString(comment ?? "", ["img", "mention-component"])
comment?.trim() === "" ||
comment === "<p></p>" ||
isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component"])
);
};
+3 -1
View File
@@ -249,7 +249,9 @@ export const isCommentEmpty = (comment: string | undefined): boolean => {
// return true if comment is undefined
if (!comment) return true;
return (
comment?.trim() === "" || comment === "<p></p>" || isEmptyHtmlString(comment ?? "", ["img", "mention-component"])
comment?.trim() === "" ||
comment === "<p></p>" ||
isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component"])
);
};