From 9b09737cff708d03c24c8d6b278981358b34fdbc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 20 Jun 2025 16:20:23 +0530 Subject: [PATCH 1/4] refactor: custom image extension --- packages/editor/src/ce/types/storage.ts | 2 +- .../components/{image-block.tsx => block.tsx} | 79 +++----- .../custom-image/components/index.ts | 4 - .../{image-node.tsx => node-view.tsx} | 42 ++-- .../components/toolbar/full-screen.tsx | 21 +- .../custom-image/components/toolbar/root.tsx | 12 +- .../{image-uploader.tsx => uploader.tsx} | 24 +-- .../extensions/custom-image/custom-image.ts | 180 ------------------ .../custom-image/extension-config.ts | 47 +++++ .../core/extensions/custom-image/extension.ts | 121 ++++++++++++ .../src/core/extensions/custom-image/index.ts | 3 - .../custom-image/read-only-custom-image.ts | 79 -------- .../src/core/extensions/custom-image/types.ts | 51 +++++ .../src/core/extensions/custom-image/utils.ts | 33 ++++ .../editor/src/core/extensions/extensions.ts | 14 +- .../src/core/extensions/image/extension.tsx | 39 ++-- .../editor/src/core/extensions/image/index.ts | 1 - .../core/extensions/image/read-only-image.tsx | 37 ---- packages/editor/src/core/extensions/index.ts | 1 - .../core/extensions/read-only-extensions.ts | 16 +- 20 files changed, 361 insertions(+), 445 deletions(-) rename packages/editor/src/core/extensions/custom-image/components/{image-block.tsx => block.tsx} (88%) delete mode 100644 packages/editor/src/core/extensions/custom-image/components/index.ts rename packages/editor/src/core/extensions/custom-image/components/{image-node.tsx => node-view.tsx} (71%) rename packages/editor/src/core/extensions/custom-image/components/{image-uploader.tsx => uploader.tsx} (91%) delete mode 100644 packages/editor/src/core/extensions/custom-image/custom-image.ts create mode 100644 packages/editor/src/core/extensions/custom-image/extension-config.ts create mode 100644 packages/editor/src/core/extensions/custom-image/extension.ts delete mode 100644 packages/editor/src/core/extensions/custom-image/index.ts delete mode 100644 packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts create mode 100644 packages/editor/src/core/extensions/custom-image/types.ts create mode 100644 packages/editor/src/core/extensions/custom-image/utils.ts delete mode 100644 packages/editor/src/core/extensions/image/read-only-image.tsx diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts index 5f576df5090..84eee65f982 100644 --- a/packages/editor/src/ce/types/storage.ts +++ b/packages/editor/src/ce/types/storage.ts @@ -2,7 +2,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { type HeadingExtensionStorage } from "@/extensions"; -import { type CustomImageExtensionStorage } from "@/extensions/custom-image"; +import { type CustomImageExtensionStorage } from "@/extensions/custom-image/types"; import { type CustomLinkStorage } from "@/extensions/custom-link"; import { type ImageExtensionStorage } from "@/extensions/image"; import { type MentionExtensionStorage } from "@/extensions/mentions"; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/block.tsx similarity index 88% rename from packages/editor/src/core/extensions/custom-image/components/image-block.tsx rename to packages/editor/src/core/extensions/custom-image/components/block.tsx index 5dfbad01294..b373b47198e 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/block.tsx @@ -1,68 +1,42 @@ import { NodeSelection } from "@tiptap/pm/state"; import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; -// extensions -import { CustomBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; +// local imports +import { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types"; +import { ensurePixelString } from "../utils"; +import type { CustomImageNodeViewProps } from "./node-view"; +import { ImageToolbarRoot } from "./toolbar"; import { ImageUploadStatus } from "./upload-status"; const MIN_SIZE = 100; -type Pixel = `${number}px`; - -type PixelAttribute = Pixel | TDefault; - -export type ImageAttributes = { - src: string | null; - width: PixelAttribute<"35%" | number>; - height: PixelAttribute<"auto" | number>; - aspectRatio: number | null; - id: string | null; -}; - -type Size = { - width: PixelAttribute<"35%">; - height: PixelAttribute<"auto">; - aspectRatio: number | null; -}; - -const ensurePixelString = (value: Pixel | TDefault | number | undefined | null, defaultValue?: TDefault) => { - if (!value || value === defaultValue) { - return defaultValue; - } - - if (typeof value === "number") { - return `${value}px` satisfies Pixel; - } - - return value; -}; - -type CustomImageBlockProps = CustomBaseImageNodeViewProps & { - imageFromFileSystem: string | undefined; - setFailedToLoadImage: (isError: boolean) => void; +type CustomImageBlockProps = CustomImageNodeViewProps & { editorContainer: HTMLDivElement | null; + imageFromFileSystem: string | undefined; setEditorContainer: (editorContainer: HTMLDivElement | null) => void; + setFailedToLoadImage: (isError: boolean) => void; src: string | undefined; }; export const CustomImageBlock: React.FC = (props) => { // props const { - node, - updateAttributes, - setFailedToLoadImage, - imageFromFileSystem, - selected, - getPos, editor, editorContainer, - src: resolvedImageSrc, + extension, + getPos, + imageFromFileSystem, + node, + selected, setEditorContainer, + setFailedToLoadImage, + src: resolvedImageSrc, + updateAttributes, } = props; const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio, src: imgNodeSrc } = node.attrs; // states - const [size, setSize] = useState({ + const [size, setSize] = useState({ width: ensurePixelString(nodeWidth, "35%") ?? "35%", height: ensurePixelString(nodeHeight, "auto") ?? "auto", aspectRatio: nodeAspectRatio || null, @@ -77,7 +51,7 @@ export const CustomImageBlock: React.FC = (props) => { const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false); const updateAttributesSafely = useCallback( - (attributes: Partial, errorMessage: string) => { + (attributes: Partial, errorMessage: string) => { try { updateAttributes(attributes); } catch (error) { @@ -139,7 +113,7 @@ export const CustomImageBlock: React.FC = (props) => { } } setInitialResizeComplete(true); - }, [nodeWidth, updateAttributes, editorContainer, nodeAspectRatio]); + }, [nodeWidth, updateAttributesSafely, editorContainer, nodeAspectRatio]); // for real time resizing useLayoutEffect(() => { @@ -168,7 +142,7 @@ export const CustomImageBlock: React.FC = (props) => { const handleResizeEnd = useCallback(() => { setIsResizing(false); updateAttributesSafely(size, "Failed to update attributes at the end of resizing:"); - }, [size, updateAttributes]); + }, [size, updateAttributesSafely]); const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); @@ -242,7 +216,7 @@ export const CustomImageBlock: React.FC = (props) => { onLoad={handleImageLoad} onError={async (e) => { // for old image extension this command doesn't exist or if the image failed to load for the first time - if (!editor?.commands.restoreImage || hasTriedRestoringImageOnce) { + if (!extension.options.restoreImage || hasTriedRestoringImageOnce) { setFailedToLoadImage(true); return; } @@ -253,7 +227,7 @@ export const CustomImageBlock: React.FC = (props) => { if (!imgNodeSrc) { throw new Error("No source image to restore from"); } - await editor?.commands.restoreImage?.(imgNodeSrc); + await extension.options.restoreImage?.(imgNodeSrc); if (!imageRef.current) { throw new Error("Image reference not found"); } @@ -288,12 +262,7 @@ export const CustomImageBlock: React.FC = (props) => { containerClassName={ "absolute top-1 right-1 z-20 bg-black/40 rounded opacity-0 pointer-events-none group-hover/image-component:opacity-100 group-hover/image-component:pointer-events-auto transition-opacity" } - image={{ - src: resolvedImageSrc, - aspectRatio: size.aspectRatio === null ? 1 : size.aspectRatio, - height: size.height, - width: size.width, - }} + image={node.attrs} /> )} {selected && displayedImageSrc === resolvedImageSrc && ( diff --git a/packages/editor/src/core/extensions/custom-image/components/index.ts b/packages/editor/src/core/extensions/custom-image/components/index.ts deleted file mode 100644 index 9d12c3ecf19..00000000000 --- a/packages/editor/src/core/extensions/custom-image/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./toolbar"; -export * from "./image-block"; -export * from "./image-node"; -export * from "./image-uploader"; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx similarity index 71% rename from packages/editor/src/core/extensions/custom-image/components/image-node.tsx rename to packages/editor/src/core/extensions/custom-image/components/node-view.tsx index 8dfe6974b75..07fa8047a88 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx @@ -2,25 +2,26 @@ import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { useEffect, useRef, useState } from "react"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; -// extensions -import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image"; // helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; +// local imports +import type { CustomImageExtension, TCustomImageAttributes } from "../types"; +import { CustomImageBlock } from "./block"; +import { CustomImageUploader } from "./uploader"; -export type CustomBaseImageNodeViewProps = { +export type CustomImageNodeViewProps = Omit & { + extension: CustomImageExtension; getPos: () => number; editor: Editor; node: NodeViewProps["node"] & { - attrs: ImageAttributes; + attrs: TCustomImageAttributes; }; - updateAttributes: (attrs: Partial) => void; + updateAttributes: (attrs: Partial) => void; selected: boolean; }; -export type CustomImageNodeProps = NodeViewProps & CustomBaseImageNodeViewProps; - -export const CustomImageNode = (props: CustomImageNodeProps) => { - const { getPos, editor, node, updateAttributes, selected } = props; +export const CustomImageNodeView: React.FC = (props) => { + const { editor, extension, node } = props; const { src: imgNodeSrc } = node.attrs; const [isUploaded, setIsUploaded] = useState(false); @@ -50,41 +51,34 @@ export const CustomImageNode = (props: CustomImageNodeProps) => { }, [resolvedSrc]); useEffect(() => { + if (!imgNodeSrc) return; + const getImageSource = async () => { - // @ts-expect-error function not expected here, but will still work and don't remove await - const url: string = await editor?.commands?.getImageSource?.(imgNodeSrc); + const url: string = await extension.options.getImageSource?.(imgNodeSrc); setResolvedSrc(url as string); }; getImageSource(); - }, [imgNodeSrc]); + }, [imgNodeSrc, extension.options]); return (
{(isUploaded || imageFromFileSystem) && !failedToLoadImage ? ( ) : ( )}
diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx index 61ae307bb66..5921e95c2b7 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx @@ -1,15 +1,12 @@ import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useState, useRef } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; +// local imports +import { TCustomImageAttributes } from "../../types"; type Props = { - image: { - src: string; - height: string; - width: string; - aspectRatio: number; - }; + image: TCustomImageAttributes; isOpen: boolean; toggleFullScreenMode: (val: boolean) => void; }; @@ -31,7 +28,7 @@ export const ImageFullScreenAction: React.FC = (props) => { const modalRef = useRef(null); const imgRef = useRef(null); - const widthInNumber = useMemo(() => Number(width?.replace("px", "")), [width]); + const widthInNumber = useMemo(() => Number(width?.toString()?.replace("px", "")), [width]); const setImageRef = useCallback( (node: HTMLImageElement | null) => { @@ -42,7 +39,7 @@ export const ImageFullScreenAction: React.FC = (props) => { const viewportWidth = window.innerWidth * 0.9; const viewportHeight = window.innerHeight * 0.75; const imageWidth = widthInNumber; - const imageHeight = imageWidth / aspectRatio; + const imageHeight = imageWidth / (aspectRatio ?? 1); const widthRatio = viewportWidth / imageWidth; const heightRatio = viewportHeight / imageHeight; @@ -208,13 +205,13 @@ export const ImageFullScreenAction: React.FC = (props) => { = (props) => { = (props) => {