From 9b09737cff708d03c24c8d6b278981358b34fdbc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 20 Jun 2025 16:20:23 +0530 Subject: [PATCH 01/11] 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) => { + + {isDropdownOpen && ( +
+ {IMAGE_ALIGNMENT_OPTIONS.map((option) => ( + + + + ))} +
+ )} + + ); +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx new file mode 100644 index 00000000000..ee8fc7f9279 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx @@ -0,0 +1,24 @@ +import { Download } from "lucide-react"; +// plane imports +import { Tooltip } from "@plane/ui"; + +type Props = { + src: string; +}; + +export const ImageDownloadAction: React.FC = (props) => { + const { src } = props; + + return ( + + + + + + ); +}; 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 43f178dc8f4..c4fa6707873 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,6 +1,7 @@ import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useState, useRef } from "react"; // plane imports +import { Tooltip } from "@plane/ui"; import { cn } from "@plane/utils"; type Props = { @@ -10,8 +11,7 @@ type Props = { aspectRatio: number; src: string; }; - isOpen: boolean; - toggleFullScreenMode: (val: boolean) => void; + toggleToolbarViewStatus: (val: boolean) => void; }; const MIN_ZOOM = 0.5; @@ -20,16 +20,19 @@ const ZOOM_SPEED = 0.05; const ZOOM_STEPS = [0.5, 1, 1.5, 2]; export const ImageFullScreenAction: React.FC = (props) => { - const { image, isOpen: isFullScreenEnabled, toggleFullScreenMode } = props; - const { src, width, aspectRatio } = image; - + const { image, toggleToolbarViewStatus } = props; + // state + const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); const [magnification, setMagnification] = useState(1); const [initialMagnification, setInitialMagnification] = useState(1); const [isDragging, setIsDragging] = useState(false); + // refs const dragStart = useRef({ x: 0, y: 0 }); const dragOffset = useRef({ x: 0, y: 0 }); const modalRef = useRef(null); const imgRef = useRef(null); + // derived values + const { src, width, aspectRatio } = image; const widthInNumber = useMemo(() => Number(width?.replace("px", "")), [width]); @@ -59,10 +62,10 @@ export const ImageFullScreenAction: React.FC = (props) => { const handleClose = useCallback(() => { if (isDragging) return; - toggleFullScreenMode(false); + setIsFullScreenEnabled(false); setMagnification(1); setInitialMagnification(1); - }, [isDragging, toggleFullScreenMode]); + }, [isDragging]); const handleMagnification = useCallback((direction: "increase" | "decrease") => { setMagnification((prev) => { @@ -165,7 +168,7 @@ export const ImageFullScreenAction: React.FC = (props) => { return; } }, - [isFullScreenEnabled, magnification] + [isFullScreenEnabled] ); // Event listeners @@ -185,6 +188,10 @@ export const ImageFullScreenAction: React.FC = (props) => { }; }, [isFullScreenEnabled, handleKeyDown, handleMouseMove, handleMouseUp, handleWheel]); + useEffect(() => { + toggleToolbarViewStatus(isFullScreenEnabled); + }, [isFullScreenEnabled, toggleToolbarViewStatus]); + return ( <>
= (props) => {
- + + + ); }; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx index f9cd28d48d0..d6c35664f77 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx @@ -2,35 +2,43 @@ import { useState } from "react"; // plane imports import { cn } from "@plane/utils"; // local imports +import type { TCustomImageAlignment } from "../../types"; +import { ImageAlignmentAction } from "./alignment"; +import { ImageDownloadAction } from "./download"; import { ImageFullScreenAction } from "./full-screen"; type Props = { - containerClassName?: string; - image: { - width: string; - height: string; - aspectRatio: number; - src: string; - }; + alignment: TCustomImageAlignment; + width: string; + height: string; + aspectRatio: number; + src: string; + downloadSrc: string; + handleAlignmentChange: (alignment: TCustomImageAlignment) => void; }; export const ImageToolbarRoot: React.FC = (props) => { - const { containerClassName, image } = props; - // state - const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); + const { alignment, downloadSrc, handleAlignmentChange } = props; + // states + const [shouldShowToolbar, setShouldShowToolbar] = useState(false); return ( <>
- setIsFullScreenEnabled(val)} + + +
); diff --git a/packages/editor/src/core/extensions/custom-image/extension.ts b/packages/editor/src/core/extensions/custom-image/extension.ts index ec795da842b..3ce93ecd87c 100644 --- a/packages/editor/src/core/extensions/custom-image/extension.ts +++ b/packages/editor/src/core/extensions/custom-image/extension.ts @@ -20,7 +20,7 @@ type Props = { export const CustomImageExtension = (props: Props) => { const { fileHandler, isEditable } = props; // derived values - const { getAssetSrc, restore: restoreImageFn } = fileHandler; + const { getAssetSrc, getAssetDownloadSrc, restore: restoreImageFn } = fileHandler; return CustomImageExtensionConfig.extend({ selectable: isEditable, @@ -31,6 +31,7 @@ export const CustomImageExtension = (props: Props) => { return { ...this.parent?.(), + getImageDownloadSource: getAssetDownloadSrc, getImageSource: getAssetSrc, restoreImage: restoreImageFn, uploadImage: upload, diff --git a/packages/editor/src/core/extensions/custom-image/types.ts b/packages/editor/src/core/extensions/custom-image/types.ts index 675d8a22155..4ed5cd6ce4a 100644 --- a/packages/editor/src/core/extensions/custom-image/types.ts +++ b/packages/editor/src/core/extensions/custom-image/types.ts @@ -8,6 +8,7 @@ export enum ECustomImageAttributeNames { HEIGHT = "height", ASPECT_RATIO = "aspectRatio", SOURCE = "src", + ALIGNMENT = "alignment", } export type Pixel = `${number}px`; @@ -20,12 +21,15 @@ export type TCustomImageSize = { aspectRatio: number | null; }; +export type TCustomImageAlignment = "left" | "center" | "right"; + export type TCustomImageAttributes = { [ECustomImageAttributeNames.ID]: string | null; [ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null; [ECustomImageAttributeNames.HEIGHT]: PixelAttribute<"auto" | number> | null; [ECustomImageAttributeNames.ASPECT_RATIO]: number | null; [ECustomImageAttributeNames.SOURCE]: string | null; + [ECustomImageAttributeNames.ALIGNMENT]: TCustomImageAlignment; }; export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean }; @@ -37,6 +41,7 @@ export type InsertImageComponentProps = { }; export type CustomImageExtensionOptions = { + getImageDownloadSource: TFileHandler["getAssetDownloadSrc"]; getImageSource: TFileHandler["getAssetSrc"]; restoreImage: TFileHandler["restore"]; uploadImage?: TFileHandler["upload"]; diff --git a/packages/editor/src/core/extensions/custom-image/utils.ts b/packages/editor/src/core/extensions/custom-image/utils.ts index 0711e094f1b..e2ed15be279 100644 --- a/packages/editor/src/core/extensions/custom-image/utils.ts +++ b/packages/editor/src/core/extensions/custom-image/utils.ts @@ -1,10 +1,11 @@ import type { Editor } from "@tiptap/core"; +import { AlignCenter, AlignLeft, AlignRight, type LucideIcon } from "lucide-react"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; // local imports -import { ECustomImageAttributeNames, type Pixel, type TCustomImageAttributes } from "./types"; +import { ECustomImageAttributeNames, TCustomImageAlignment, type Pixel, type TCustomImageAttributes } from "./types"; export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = { [ECustomImageAttributeNames.SOURCE]: null, @@ -12,6 +13,7 @@ export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = { [ECustomImageAttributeNames.WIDTH]: "35%", [ECustomImageAttributeNames.HEIGHT]: "auto", [ECustomImageAttributeNames.ASPECT_RATIO]: null, + [ECustomImageAttributeNames.ALIGNMENT]: "left", }; export const getImageComponentImageFileMap = (editor: Editor) => @@ -31,3 +33,25 @@ export const ensurePixelString = ( return value; }; + +export const IMAGE_ALIGNMENT_OPTIONS: { + label: string; + value: TCustomImageAlignment; + icon: LucideIcon; +}[] = [ + { + label: "Left", + value: "left", + icon: AlignLeft, + }, + { + label: "Center", + value: "center", + icon: AlignCenter, + }, + { + label: "Right", + value: "right", + icon: AlignRight, + }, +]; diff --git a/packages/editor/src/core/hooks/use-outside-click-detector.ts b/packages/editor/src/core/hooks/use-outside-click-detector.ts new file mode 100644 index 00000000000..04ebc3c6fdb --- /dev/null +++ b/packages/editor/src/core/hooks/use-outside-click-detector.ts @@ -0,0 +1,29 @@ +import React, { useEffect } from "react"; + +export const useOutsideClickDetector = ( + ref: React.RefObject, + callback: () => void, + useCapture = false +) => { + const handleClick = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + // check for the closest element with attribute name data-prevent-outside-click + const preventOutsideClickElement = (event.target as unknown as HTMLElement | undefined)?.closest( + "[data-prevent-outside-click]" + ); + // if the closest element with attribute name data-prevent-outside-click is found, return + if (preventOutsideClickElement) { + return; + } + // else call the callback + callback(); + } + }; + + useEffect(() => { + document.addEventListener("mousedown", handleClick, useCapture); + return () => { + document.removeEventListener("mousedown", handleClick, useCapture); + }; + }); +}; diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index 60ccfa84123..7ef685ad02d 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -3,6 +3,7 @@ import { TWebhookConnectionQueryParams } from "@plane/types"; export type TReadOnlyFileHandler = { checkIfAssetExists: (assetId: string) => Promise; + getAssetDownloadSrc: (path: string) => Promise; getAssetSrc: (path: string) => Promise; restore: (assetSrc: string) => Promise; }; diff --git a/packages/utils/src/editor.ts b/packages/utils/src/editor.ts index 1bdf3a50419..fb15edd078f 100644 --- a/packages/utils/src/editor.ts +++ b/packages/utils/src/editor.ts @@ -22,6 +22,21 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => { return url; }; +/** + * @description generate the file source using assetId + * @param {TEditorSrcArgs} args + */ +export const getEditorAssetDownloadSrc = (args: TEditorSrcArgs): string | undefined => { + const { assetId, projectId, workspaceSlug } = args; + let url: string | undefined = ""; + if (projectId) { + url = getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/download/${assetId}/`); + } else { + url = getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/download/${assetId}/`); + } + return url; +}; + export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => { if (!jsx) return ""; diff --git a/web/core/hooks/editor/use-editor-config.ts b/web/core/hooks/editor/use-editor-config.ts index 7e5692b07b5..466ff7d9518 100644 --- a/web/core/hooks/editor/use-editor-config.ts +++ b/web/core/hooks/editor/use-editor-config.ts @@ -2,7 +2,7 @@ import { useCallback } from "react"; // plane editor import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor"; // helpers -import { getEditorAssetSrc } from "@plane/utils"; +import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils"; // hooks import { useEditorAsset } from "@/hooks/store"; // plane web hooks @@ -33,6 +33,20 @@ export const useEditorConfig = () => { const res = await fileService.checkIfAssetExists(workspaceSlug, assetId); return res?.exists ?? false; }, + getAssetDownloadSrc: async (path) => { + if (!path) return ""; + if (path?.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetDownloadSrc({ + assetId: path, + projectId, + workspaceSlug, + }) ?? "" + ); + } + }, getAssetSrc: async (path) => { if (!path) return ""; if (path?.startsWith("http")) { From ec8636a75dbb8f544eac25f937dc542f666fd534 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 23 Jun 2025 15:22:50 +0530 Subject: [PATCH 06/11] chore: render image full screen modal in a portal --- .../custom-image/components/block.tsx | 8 +- .../components/toolbar/full-screen.tsx | 246 +---------------- .../components/toolbar/full-screen/index.ts | 1 + .../components/toolbar/full-screen/modal.tsx | 256 ++++++++++++++++++ .../components/toolbar/full-screen/root.tsx | 43 +++ space/app/layout.tsx | 1 + web/app/layout.tsx | 1 + 7 files changed, 317 insertions(+), 239 deletions(-) create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/index.ts create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx create mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx diff --git a/packages/editor/src/core/extensions/custom-image/components/block.tsx b/packages/editor/src/core/extensions/custom-image/components/block.tsx index 30039d26850..a0e85960c04 100644 --- a/packages/editor/src/core/extensions/custom-image/components/block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/block.tsx @@ -201,7 +201,7 @@ export const CustomImageBlock: React.FC = (props) => { // show the image upload status only when the resolvedImageSrc is not ready const showUploadStatus = !resolvedImageSrc; // show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) - const showImageUtils = resolvedImageSrc && resolvedDownloadSrc && initialResizeComplete; + const showImageToolbar = resolvedImageSrc && resolvedDownloadSrc && initialResizeComplete; // show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) const showImageResizer = editor.isEditable && resolvedImageSrc && initialResizeComplete; // show the preview image from the file system if the remote image's src is not set @@ -210,8 +210,8 @@ export const CustomImageBlock: React.FC = (props) => { return (
= (props) => { }} /> {showUploadStatus && node.attrs.id && } - {showImageUtils && ( + {showImageToolbar && ( void; }; -const MIN_ZOOM = 0.5; -const MAX_ZOOM = 2; -const ZOOM_SPEED = 0.05; -const ZOOM_STEPS = [0.5, 1, 1.5, 2]; - export const ImageFullScreenAction: React.FC = (props) => { const { image, toggleToolbarViewStatus } = props; // state const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); - const [magnification, setMagnification] = useState(1); - const [initialMagnification, setInitialMagnification] = useState(1); - const [isDragging, setIsDragging] = useState(false); - // refs - const dragStart = useRef({ x: 0, y: 0 }); - const dragOffset = useRef({ x: 0, y: 0 }); - const modalRef = useRef(null); - const imgRef = useRef(null); // derived values const { src, width, aspectRatio } = image; - const widthInNumber = useMemo(() => Number(width?.replace("px", "")), [width]); - - const setImageRef = useCallback( - (node: HTMLImageElement | null) => { - if (!node || !isFullScreenEnabled) return; - - imgRef.current = node; - - const viewportWidth = window.innerWidth * 0.9; - const viewportHeight = window.innerHeight * 0.75; - const imageWidth = widthInNumber; - const imageHeight = imageWidth / aspectRatio; - - const widthRatio = viewportWidth / imageWidth; - const heightRatio = viewportHeight / imageHeight; - - setInitialMagnification(Math.min(widthRatio, heightRatio)); - setMagnification(1); - - // Reset image position - node.style.left = "0px"; - node.style.top = "0px"; - }, - [isFullScreenEnabled, widthInNumber, aspectRatio] - ); - - const handleClose = useCallback(() => { - if (isDragging) return; - setIsFullScreenEnabled(false); - setMagnification(1); - setInitialMagnification(1); - }, [isDragging]); - - const handleMagnification = useCallback((direction: "increase" | "decrease") => { - setMagnification((prev) => { - // Find the appropriate target zoom level based on current magnification - let targetZoom: number; - if (direction === "increase") { - targetZoom = ZOOM_STEPS.find((step) => step > prev) ?? MAX_ZOOM; - } else { - // Reverse the array to find the next lower step - targetZoom = [...ZOOM_STEPS].reverse().find((step) => step < prev) ?? MIN_ZOOM; - } - - // Reset position when zoom matches initial magnification - if (targetZoom === 1 && imgRef.current) { - imgRef.current.style.left = "0px"; - imgRef.current.style.top = "0px"; - } - - return targetZoom; - }); - }, []); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === "Escape" || e.key === "+" || e.key === "=" || e.key === "-") { - e.preventDefault(); - e.stopPropagation(); - - if (e.key === "Escape") handleClose(); - if (e.key === "+" || e.key === "=") handleMagnification("increase"); - if (e.key === "-") handleMagnification("decrease"); - } - }, - [handleClose, handleMagnification] - ); - - const handleMouseDown = (e: React.MouseEvent) => { - if (!imgRef.current) return; - - const imgWidth = imgRef.current.offsetWidth * magnification; - const imgHeight = imgRef.current.offsetHeight * magnification; - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - if (imgWidth > viewportWidth || imgHeight > viewportHeight) { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - dragStart.current = { x: e.clientX, y: e.clientY }; - dragOffset.current = { - x: parseInt(imgRef.current.style.left || "0"), - y: parseInt(imgRef.current.style.top || "0"), - }; - } - }; - - const handleMouseMove = useCallback( - (e: MouseEvent) => { - if (!isDragging || !imgRef.current) return; - - const dx = e.clientX - dragStart.current.x; - const dy = e.clientY - dragStart.current.y; - - // Apply the scale factor to the drag movement - const scaledDx = dx / magnification; - const scaledDy = dy / magnification; - - imgRef.current.style.left = `${dragOffset.current.x + scaledDx}px`; - imgRef.current.style.top = `${dragOffset.current.y + scaledDy}px`; - }, - [isDragging, magnification] - ); - - const handleMouseUp = useCallback(() => { - if (!isDragging || !imgRef.current) return; - setIsDragging(false); - }, [isDragging]); - - const handleWheel = useCallback( - (e: WheelEvent) => { - if (!imgRef.current || !isFullScreenEnabled) return; - - e.preventDefault(); - - // Handle pinch-to-zoom - if (e.ctrlKey) { - const delta = e.deltaY; - setMagnification((prev) => { - const newZoom = prev * (1 - delta * ZOOM_SPEED); - const clampedZoom = Math.min(Math.max(newZoom, MIN_ZOOM), MAX_ZOOM); - - // Reset position when zoom matches initial magnification - if (clampedZoom === 1 && imgRef.current) { - imgRef.current.style.left = "0px"; - imgRef.current.style.top = "0px"; - } - - return clampedZoom; - }); - return; - } - }, - [isFullScreenEnabled] - ); - - // Event listeners - useEffect(() => { - if (!isFullScreenEnabled) return; - - document.addEventListener("keydown", handleKeyDown); - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("mouseup", handleMouseUp); - window.addEventListener("wheel", handleWheel, { passive: false }); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", handleMouseUp); - window.removeEventListener("wheel", handleWheel); - }; - }, [isFullScreenEnabled, handleKeyDown, handleMouseMove, handleMouseUp, handleWheel]); - useEffect(() => { toggleToolbarViewStatus(isFullScreenEnabled); }, [isFullScreenEnabled, toggleToolbarViewStatus]); return ( <> -
-
e.target === modalRef.current && handleClose()} - className="relative size-full grid place-items-center overflow-hidden" - > - - -
-
- - {Math.round(100 * magnification)}% - -
- -
-
-
+ + +
+
+ + {Math.round(100 * magnification)}% + +
+ +
+
+
+ ); +}; + +export const ImageFullScreenModal: React.FC = (props) => { + let modal = ; + const portal = document.querySelector("#editor-portal"); + if (portal) modal = ReactDOM.createPortal(modal, portal); + return modal; +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx new file mode 100644 index 00000000000..4dffc21707e --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx @@ -0,0 +1,43 @@ +import { Maximize } from "lucide-react"; +// local imports +import { ImageFullScreenModal } from "./modal"; + +type Props = { + image: { + src: string; + height: string; + width: string; + aspectRatio: number; + }; + isOpen: boolean; + toggleFullScreenMode: (val: boolean) => void; +}; + +export const ImageFullScreenActionRoot: React.FC = (props) => { + const { image, isOpen: isFullScreenEnabled, toggleFullScreenMode } = props; + // derived values + const { src, width, aspectRatio } = image; + + return ( + <> + + + + ); +}; diff --git a/space/app/layout.tsx b/space/app/layout.tsx index 96a19227371..d0c7435da93 100644 --- a/space/app/layout.tsx +++ b/space/app/layout.tsx @@ -33,6 +33,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) +
<>{children} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index fc454e54bb0..433dea7f915 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -80,6 +80,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
+
Date: Mon, 23 Jun 2025 15:25:37 +0530 Subject: [PATCH 07/11] chore: add missing attribute to image extension --- packages/editor/src/core/extensions/image/extension-config.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/editor/src/core/extensions/image/extension-config.tsx b/packages/editor/src/core/extensions/image/extension-config.tsx index 6dbad2d24d6..7456e3dfb06 100644 --- a/packages/editor/src/core/extensions/image/extension-config.tsx +++ b/packages/editor/src/core/extensions/image/extension-config.tsx @@ -19,6 +19,9 @@ export const ImageExtensionConfig = BaseImageExtension.extend< aspectRatio: { default: null, }, + alignment: { + default: "left", + }, }; }, }); From 0127b7dc72a194ce5a047e4721160e96916f9913 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 24 Jun 2025 14:16:54 +0530 Subject: [PATCH 08/11] chore: minor bugs and improvements --- .../components/toolbar/full-screen.tsx | 1 + .../components/toolbar/full-screen/modal.tsx | 16 +++++++-- .../core/hooks/use-outside-click-detector.ts | 33 ++++++++++--------- 3 files changed, 32 insertions(+), 18 deletions(-) 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 10d9141c6e0..d21194b48a8 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 @@ -44,6 +44,7 @@ export const ImageFullScreenAction: React.FC = (props) => { setIsFullScreenEnabled(true); }} className="flex-shrink-0 h-full grid place-items-center text-white/60 hover:text-white transition-colors" + aria-label="View image in full screen" > diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx index 97fc30fad74..d3e80688804 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx @@ -29,7 +29,10 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => { const modalRef = useRef(null); const imgRef = useRef(null); - const widthInNumber = useMemo(() => Number(width?.replace("px", "")), [width]); + const widthInNumber = useMemo(() => { + if (!width) return 0; + return Number(width.replace("px", "")); + }, [width]); const setImageRef = useCallback( (node: HTMLImageElement | null) => { @@ -146,7 +149,7 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => { e.preventDefault(); // Handle pinch-to-zoom - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { const delta = e.deltaY; setMagnification((prev) => { const newZoom = prev * (1 - delta * ZOOM_SPEED); @@ -190,6 +193,9 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => { className={cn("fixed inset-0 size-full z-30 bg-black/90 cursor-default", { "cursor-grabbing": isDragging, })} + role="dialog" + aria-modal="true" + aria-label="Fullscreen image viewer" >
{ export const ImageFullScreenModal: React.FC = (props) => { let modal = ; const portal = document.querySelector("#editor-portal"); - if (portal) modal = ReactDOM.createPortal(modal, portal); + if (portal) { + modal = ReactDOM.createPortal(modal, portal); + } else { + console.warn("Portal element #editor-portal not found. Rendering inline."); + } return modal; }; diff --git a/packages/editor/src/core/hooks/use-outside-click-detector.ts b/packages/editor/src/core/hooks/use-outside-click-detector.ts index 04ebc3c6fdb..04a3a4d6b28 100644 --- a/packages/editor/src/core/hooks/use-outside-click-detector.ts +++ b/packages/editor/src/core/hooks/use-outside-click-detector.ts @@ -1,29 +1,32 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; export const useOutsideClickDetector = ( ref: React.RefObject, callback: () => void, useCapture = false ) => { - const handleClick = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - // check for the closest element with attribute name data-prevent-outside-click - const preventOutsideClickElement = (event.target as unknown as HTMLElement | undefined)?.closest( - "[data-prevent-outside-click]" - ); - // if the closest element with attribute name data-prevent-outside-click is found, return - if (preventOutsideClickElement) { - return; + const handleClick = useCallback( + (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + // check for the closest element with attribute name data-prevent-outside-click + const preventOutsideClickElement = (event.target as unknown as HTMLElement | undefined)?.closest( + "[data-prevent-outside-click]" + ); + // if the closest element with attribute name data-prevent-outside-click is found, return + if (preventOutsideClickElement) { + return; + } + // else call the callback + callback(); } - // else call the callback - callback(); - } - }; + }, + [callback, ref] + ); useEffect(() => { document.addEventListener("mousedown", handleClick, useCapture); return () => { document.removeEventListener("mousedown", handleClick, useCapture); }; - }); + }, [handleClick, useCapture]); }; From 52c456aead30889e4472517de5e145ddb9fc74ae Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 24 Jun 2025 14:53:12 +0530 Subject: [PATCH 09/11] chore: add aria attributes --- .../components/toolbar/download.tsx | 10 ++-- .../components/toolbar/full-screen.tsx | 54 ------------------- .../components/toolbar/full-screen/modal.tsx | 23 ++++++-- .../components/toolbar/full-screen/root.tsx | 45 ++++++++++------ .../custom-image/components/toolbar/root.tsx | 4 +- 5 files changed, 56 insertions(+), 80 deletions(-) delete mode 100644 packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx index ee8fc7f9279..2f0a665ae5d 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/download.tsx @@ -11,14 +11,14 @@ export const ImageDownloadAction: React.FC = (props) => { return ( - window.open(src, "_blank")} className="flex-shrink-0 h-full grid place-items-center text-white/60 hover:text-white transition-colors" - target="_blank" - rel="noreferrer noopener" + aria-label="Download image" > - + ); }; 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 deleted file mode 100644 index d21194b48a8..00000000000 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Maximize } from "lucide-react"; -import { useEffect, useState } from "react"; -// plane imports -import { Tooltip } from "@plane/ui"; -// local imports -import { ImageFullScreenModal } from "./full-screen/modal"; - -type Props = { - image: { - width: string; - height: string; - aspectRatio: number; - src: string; - }; - toggleToolbarViewStatus: (val: boolean) => void; -}; - -export const ImageFullScreenAction: React.FC = (props) => { - const { image, toggleToolbarViewStatus } = props; - // state - const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); - // derived values - const { src, width, aspectRatio } = image; - - useEffect(() => { - toggleToolbarViewStatus(isFullScreenEnabled); - }, [isFullScreenEnabled, toggleToolbarViewStatus]); - - return ( - <> - - - - - - ); -}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx index d3e80688804..e49ee5ce9a1 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx @@ -1,4 +1,4 @@ -import { ExternalLink, Minus, Plus, X } from "lucide-react"; +import { Download, ExternalLink, Minus, Plus, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactDOM from "react-dom"; // plane imports @@ -12,13 +12,14 @@ const ZOOM_STEPS = [0.5, 1, 1.5, 2]; type Props = { aspectRatio: number; isFullScreenEnabled: boolean; + downloadSrc: string; src: string; toggleFullScreenMode: (val: boolean) => void; width: string; }; const ImageFullScreenModalWithoutPortal = (props: Props) => { - const { aspectRatio, isFullScreenEnabled, src, toggleFullScreenMode, width } = props; + const { aspectRatio, isFullScreenEnabled, downloadSrc, src, toggleFullScreenMode, width } = props; // refs const dragStart = useRef({ x: 0, y: 0 }); const dragOffset = useRef({ x: 0, y: 0 }); @@ -202,7 +203,12 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => { onMouseDown={(e) => e.target === modalRef.current && handleClose()} className="relative size-full grid place-items-center overflow-hidden" > - { onClick={() => handleMagnification("decrease")} className="size-6 grid place-items-center text-white/60 hover:text-white disabled:text-white/30 transition-colors duration-200" disabled={magnification <= MIN_ZOOM} + aria-label="Zoom out" > @@ -237,14 +244,24 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => { onClick={() => handleMagnification("increase")} className="size-6 grid place-items-center text-white/60 hover:text-white disabled:text-white/30 transition-colors duration-200" disabled={magnification >= MAX_ZOOM} + aria-label="Zoom in" >
+ diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx index 4dffc21707e..2108bfeaaee 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx @@ -1,22 +1,31 @@ import { Maximize } from "lucide-react"; +import { useEffect, useState } from "react"; +// plane imports +import { Tooltip } from "@plane/ui"; // local imports import { ImageFullScreenModal } from "./modal"; type Props = { image: { + downloadSrc: string; src: string; height: string; width: string; aspectRatio: number; }; - isOpen: boolean; - toggleFullScreenMode: (val: boolean) => void; + toggleToolbarViewStatus: (val: boolean) => void; }; export const ImageFullScreenActionRoot: React.FC = (props) => { - const { image, isOpen: isFullScreenEnabled, toggleFullScreenMode } = props; + const { image, toggleToolbarViewStatus } = props; + // states + const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); // derived values - const { src, width, aspectRatio } = image; + const { downloadSrc, src, width, aspectRatio } = image; + + useEffect(() => { + toggleToolbarViewStatus(isFullScreenEnabled); + }, [isFullScreenEnabled, toggleToolbarViewStatus]); return ( <> @@ -24,20 +33,24 @@ export const ImageFullScreenActionRoot: React.FC = (props) => { aspectRatio={aspectRatio} isFullScreenEnabled={isFullScreenEnabled} src={src} + downloadSrc={downloadSrc} width={width} - toggleFullScreenMode={toggleFullScreenMode} + toggleFullScreenMode={setIsFullScreenEnabled} /> - + + + ); }; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx index d6c35664f77..06277fa252b 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx @@ -5,7 +5,7 @@ import { cn } from "@plane/utils"; import type { TCustomImageAlignment } from "../../types"; import { ImageAlignmentAction } from "./alignment"; import { ImageDownloadAction } from "./download"; -import { ImageFullScreenAction } from "./full-screen"; +import { ImageFullScreenActionRoot } from "./full-screen"; type Props = { alignment: TCustomImageAlignment; @@ -38,7 +38,7 @@ export const ImageToolbarRoot: React.FC = (props) => { handleChange={handleAlignmentChange} toggleToolbarViewStatus={setShouldShowToolbar} /> - +
); From 25c2f825661f5f7d1a42a978c173894cad642bb4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 1 Jul 2025 17:23:29 +0530 Subject: [PATCH 10/11] chore: remove unnecessary file --- packages/editor/package.json | 1 + .../components/toolbar/alignment.tsx | 3 +- .../core/hooks/use-outside-click-detector.ts | 32 ------------------- 3 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 packages/editor/src/core/hooks/use-outside-click-detector.ts diff --git a/packages/editor/package.json b/packages/editor/package.json index c511d554f40..6071d908d39 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -36,6 +36,7 @@ "dependencies": { "@floating-ui/react": "^0.26.4", "@hocuspocus/provider": "^2.15.0", + "@plane/hooks": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/utils": "*", diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx index 782fef4d295..3790fa54712 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/alignment.tsx @@ -1,9 +1,8 @@ import { ChevronDown } from "lucide-react"; import { useEffect, useRef, useState } from "react"; // plane imports +import { useOutsideClickDetector } from "@plane/hooks"; import { Tooltip } from "@plane/ui"; -// hooks -import { useOutsideClickDetector } from "@/hooks/use-outside-click-detector"; // local imports import type { TCustomImageAlignment } from "../../types"; import { IMAGE_ALIGNMENT_OPTIONS } from "../../utils"; diff --git a/packages/editor/src/core/hooks/use-outside-click-detector.ts b/packages/editor/src/core/hooks/use-outside-click-detector.ts deleted file mode 100644 index 04a3a4d6b28..00000000000 --- a/packages/editor/src/core/hooks/use-outside-click-detector.ts +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useCallback, useEffect } from "react"; - -export const useOutsideClickDetector = ( - ref: React.RefObject, - callback: () => void, - useCapture = false -) => { - const handleClick = useCallback( - (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - // check for the closest element with attribute name data-prevent-outside-click - const preventOutsideClickElement = (event.target as unknown as HTMLElement | undefined)?.closest( - "[data-prevent-outside-click]" - ); - // if the closest element with attribute name data-prevent-outside-click is found, return - if (preventOutsideClickElement) { - return; - } - // else call the callback - callback(); - } - }, - [callback, ref] - ); - - useEffect(() => { - document.addEventListener("mousedown", handleClick, useCapture); - return () => { - document.removeEventListener("mousedown", handleClick, useCapture); - }; - }, [handleClick, useCapture]); -}; From bf30383eba730529901c592ecac8c87347514c3d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 2 Jul 2025 15:35:46 +0530 Subject: [PATCH 11/11] fix: full screen modal z-index --- .../custom-image/components/toolbar/full-screen/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx index 349b933c368..9a30908c2c1 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx @@ -191,7 +191,7 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => { return (