From 8bb5dad607deceeb0114f8578a065cad36540521 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 24 Sep 2024 21:03:04 +0530 Subject: [PATCH 01/24] fix: added aspect ratio to resizing --- .../custom-image/components/image-block.tsx | 190 ++++++++++-------- .../components/image-uploader.tsx | 123 ++++++++---- .../components/toolbar/full-screen.tsx | 24 ++- .../extensions/custom-image/custom-image.ts | 3 + .../custom-image/read-only-custom-image.ts | 3 + .../image/image-component-without-props.tsx | 3 + 6 files changed, 216 insertions(+), 130 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 89ace94a57f..14a17a86613 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -11,7 +11,16 @@ export const CustomImageBlock: React.FC = (props) => { const { node, updateAttributes, selected, getPos, editor } = props; const { src, width, height } = node.attrs; - const [size, setSize] = useState({ width: width || "35%", height: height || "auto" }); + const [size, setSize] = useState<{ + width: string; + height: string; + aspectRatio: number | null; + }>({ + width: width || "35%", + height: height || "auto", + aspectRatio: null, + }); + const [isLoading, setIsLoading] = useState(true); const [initialResizeComplete, setInitialResizeComplete] = useState(false); const isShimmerVisible = isLoading || !initialResizeComplete; @@ -20,86 +29,92 @@ export const CustomImageBlock: React.FC = (props) => { const containerRef = useRef(null); const containerRect = useRef(null); const imageRef = useRef(null); - const isResizing = useRef(false); - const aspectRatioRef = useRef(null); + const [isResizing, setIsResizing] = useState(false); - useLayoutEffect(() => { - if (imageRef.current) { - const img = imageRef.current; - img.onload = () => { - const closestEditorContainer = img.closest(".editor-container"); - if (!closestEditorContainer) { - console.error("Editor container not found"); - return; - } - - setEditorContainer(closestEditorContainer as HTMLElement); - - if (width === "35%") { - const editorWidth = closestEditorContainer.clientWidth; - const initialWidth = Math.max(editorWidth * 0.35, MIN_SIZE); - const aspectRatio = img.naturalWidth / img.naturalHeight; - const initialHeight = initialWidth / aspectRatio; - - const newSize = { - width: `${Math.round(initialWidth)}px`, - height: `${Math.round(initialHeight)}px`, - }; - - setSize(newSize); - updateAttributes(newSize); - } - setInitialResizeComplete(true); - setIsLoading(false); + const handleImageLoad = useCallback(() => { + const img = imageRef.current; + if (!img) return; + + const closestEditorContainer = img.closest(".editor-container"); + if (!closestEditorContainer) { + console.error("Editor container not found"); + return; + } + + setEditorContainer(closestEditorContainer as HTMLElement); + const aspectRatio = img.naturalWidth / img.naturalHeight; + + if (width === "35%") { + const editorWidth = closestEditorContainer.clientWidth; + const initialWidth = Math.max(editorWidth * 0.35, MIN_SIZE); + const initialHeight = initialWidth / aspectRatio; + + const newSize = { + width: `${Math.round(initialWidth)}px`, + height: `${Math.round(initialHeight)}px`, + aspectRatio: aspectRatio, }; + + setSize(newSize); + updateAttributes(newSize); + } else { + setSize((prevSize) => ({ ...prevSize, aspectRatio })); } - }, [width, height, updateAttributes]); + setInitialResizeComplete(true); + setIsLoading(false); + }, [width, updateAttributes, imageRef]); useLayoutEffect(() => { - setSize({ width, height }); + setSize((prevSize) => ({ ...prevSize, width, height })); }, [width, height]); - const handleResizeStart = useCallback( - (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); - e.stopPropagation(); - isResizing.current = true; - if (containerRef.current && editorContainer) { - aspectRatioRef.current = Number(size.width.replace("px", "")) / Number(size.height.replace("px", "")); - containerRect.current = containerRef.current.getBoundingClientRect(); - } - }, - [size, editorContainer] - ); - const handleResize = useCallback( (e: MouseEvent | TouchEvent) => { - if (!isResizing.current || !containerRef.current || !containerRect.current) return; - - if (size) { - aspectRatioRef.current = Number(size.width.replace("px", "")) / Number(size.height.replace("px", "")); - } - - if (!aspectRatioRef.current) return; + if (!containerRef.current || !containerRect.current || !size.aspectRatio) return; const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE); - const newHeight = newWidth / aspectRatioRef.current; + const newHeight = newWidth / size.aspectRatio; - setSize({ width: `${newWidth}px`, height: `${newHeight}px` }); + setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` })); }, [size] ); const handleResizeEnd = useCallback(() => { - if (isResizing.current) { - isResizing.current = false; - updateAttributes(size); - } + setIsResizing(false); + updateAttributes(size); }, [size, updateAttributes]); - const handleMouseDown = useCallback( + const handleResizeStart = useCallback( + (e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsResizing(true); + + if (containerRef.current && editorContainer) { + containerRect.current = containerRef.current.getBoundingClientRect(); + } + }, + [editorContainer] + ); + + useEffect(() => { + if (isResizing) { + window.addEventListener("mousemove", handleResize); + window.addEventListener("mouseup", handleResizeEnd); + window.addEventListener("mouseleave", handleResizeEnd); + + return () => { + window.removeEventListener("mousemove", handleResize); + window.removeEventListener("mouseup", handleResizeEnd); + window.removeEventListener("mouseleave", handleResizeEnd); + }; + } + }, [isResizing, handleResize, handleResizeEnd]); + + const handleImageMouseDown = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); const pos = getPos(); @@ -109,49 +124,35 @@ export const CustomImageBlock: React.FC = (props) => { [editor, getPos] ); - useEffect(() => { - if (!editorContainer) return; - - const handleMouseMove = (e: MouseEvent) => handleResize(e); - const handleMouseUp = () => handleResizeEnd(); - const handleMouseLeave = () => handleResizeEnd(); - - editorContainer.addEventListener("mousemove", handleMouseMove); - editorContainer.addEventListener("mouseup", handleMouseUp); - editorContainer.addEventListener("mouseleave", handleMouseLeave); - - return () => { - editorContainer.removeEventListener("mousemove", handleMouseMove); - editorContainer.removeEventListener("mouseup", handleMouseUp); - editorContainer.removeEventListener("mouseleave", handleMouseLeave); - }; - }, [handleResize, handleResizeEnd, editorContainer]); - return (
{isShimmerVisible && ( -
+
)} = (props) => { width, }} /> - {editor.isEditable && selected &&
} - {editor.isEditable && ( + {editor.isEditable && selected && !isShimmerVisible && ( +
+ )} + {editor.isEditable && !isShimmerVisible && ( <> -
+
diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index d288630c637..90ff57df93f 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -1,6 +1,8 @@ -import { ChangeEvent, useCallback, useEffect, useRef } from "react"; +import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; import { Editor } from "@tiptap/core"; import { ImageIcon } from "lucide-react"; +import { Spinner } from "@plane/ui"; + // helpers import { cn } from "@/helpers/common"; // hooks @@ -29,62 +31,111 @@ export const CustomImageUploader = (props: { const { loading, uploadFile } = useUploader({ onUpload, editor }); const { handleUploadClick, ref: internalRef } = useFileUpload(); const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile }); + const imageRef = useRef(null); const localRef = useRef(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [width, setWidth] = useState(null); + + useEffect(() => { + if (previewUrl) { + const closestEditorContainer = imageRef.current?.closest(".editor-container"); + if (closestEditorContainer) { + const editorWidth = closestEditorContainer?.clientWidth; + const initialWidth = Math.max(editorWidth * 0.35, 100); + setWidth(initialWidth); + } + } + }, [previewUrl, imageRef.current]); const onFileChange = useCallback( (e: ChangeEvent) => { const file = e.target.files?.[0]; if (file) { if (isFileValid(file)) { + editor.storage.image.uploadInProgress = true; + const reader = new FileReader(); + reader.onload = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(file); uploadFile(file); } } }, - [uploadFile] + [uploadFile, editor.storage.image] ); useEffect(() => { - // no need to validate as the file is already validated before the drop onto - // the editor if (existingFile) { + const reader = new FileReader(); + reader.onload = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(existingFile); uploadFile(existingFile); } }, [existingFile, uploadFile]); return ( -
+ {!previewUrl ? ( +
+ +
+ {loading + ? "Uploading..." + : draggedInside + ? "Drop image here" + : existingFile + ? "Uploading..." + : "Add an image"} +
+ { + localRef.current = element; + assignRef(fileInputRef, element); + assignRef(internalRef as RefType, element); + }} + hidden + type="file" + accept=".jpg,.jpeg,.png,.webp" + onChange={onFileChange} + /> +
+ ) : ( +
+ Preview + {loading && ( +
+ +
+ )} +
)} - onDrop={onDrop} - onDragOver={onDragEnter} - onDragLeave={onDragLeave} - contentEditable={false} - onClick={handleUploadClick} - > - -
- {loading ? "Uploading..." : draggedInside ? "Drop image here" : existingFile ? "Uploading..." : "Add an image"} -
- { - localRef.current = element; - assignRef(fileInputRef, element); - assignRef(internalRef as RefType, element); - }} - hidden - type="file" - accept=".jpg,.jpeg,.png,.webp" - onChange={onFileChange} - /> -
+ ); }; 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 9333fb21c58..d5ccb3de9b4 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,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react"; // helpers import { cn } from "@/helpers/common"; @@ -20,6 +20,8 @@ export const ImageFullScreenAction: React.FC = (props) => { const { height, src, width } = image; // states const [magnification, setMagnification] = useState(1); + // refs + const modalRef = useRef(null); // derived values const widthInNumber = useMemo(() => Number(width.replace("px", "")), [width]); const heightInNumber = useMemo(() => Number(height.replace("px", "")), [height]); @@ -63,18 +65,24 @@ export const ImageFullScreenAction: React.FC = (props) => { }, [handleClose, handleDecreaseMagnification, handleIncreaseMagnification] ); + // click outside handler + const handleClickOutside = useCallback( + (e: React.MouseEvent) => { + console.log("click outside", modalRef.current, e.target); + if (modalRef.current && e.target === modalRef.current) { + handleClose(); + } + }, + [handleClose] + ); // register keydown listener useEffect(() => { document.addEventListener("keydown", handleKeyDown); - if (!isFullScreenEnabled) { - document.removeEventListener("keydown", handleKeyDown); - } - return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [handleKeyDown, isFullScreenEnabled]); + }, [handleKeyDown]); return ( <> @@ -86,7 +94,7 @@ export const ImageFullScreenAction: React.FC = (props) => { } )} > -
+
-
+