refactor: custom image extension folder structure
This commit is contained in:
@@ -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";
|
||||
+5
-23
@@ -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;
|
||||
+12
-10
@@ -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;
|
||||
+2
-3
@@ -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";
|
||||
+7
-8
@@ -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({
|
||||
|
||||
@@ -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"])
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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"])
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user