Skip to content

Commit

Permalink
[WEB-2450] fix: image resize component (#5623)
Browse files Browse the repository at this point in the history
* fix: image resize fixed for initial render

* fix: working image resize with mousemove handler only inside the editor

* fix: unnecessary calc

* fix: setting state to true
  • Loading branch information
Palanikannan1437 authored Sep 17, 2024
1 parent 7d7415b commit 146a500
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useState, useCallback, useLayoutEffect } from "react";
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
import { NodeSelection } from "@tiptap/pm/state";
// extensions
import { CustomImageNodeViewProps } from "@/extensions/custom-image";
Expand All @@ -13,52 +13,84 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {

const [size, setSize] = useState({ width: width || "35%", height: height || "auto" });
const [isLoading, setIsLoading] = useState(true);
const [initialResizeComplete, setInitialResizeComplete] = useState(false);
const isShimmerVisible = isLoading || !initialResizeComplete;
const [editorContainer, setEditorContainer] = useState<HTMLElement | null>(null);

const containerRef = useRef<HTMLDivElement>(null);
const containerRect = useRef<DOMRect | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const isResizing = useRef(false);
const aspectRatio = useRef(1);
const aspectRatioRef = useRef<number | null>(null);

useLayoutEffect(() => {
if (imageRef.current) {
const img = imageRef.current;
img.onload = () => {
if (node.attrs.width === "35%" && node.attrs.height === "auto") {
aspectRatio.current = img.naturalWidth / img.naturalHeight;
const initialWidth = Math.max(img.naturalWidth * 0.35, MIN_SIZE);
const initialHeight = initialWidth / aspectRatio.current;
setSize({ width: `${initialWidth}px`, height: `${initialHeight}px` });
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);
};
}
}, [src]);

const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
isResizing.current = true;
if (containerRef.current) {
containerRect.current = containerRef.current.getBoundingClientRect();
}
}, []);
}, [width, height, updateAttributes]);

useLayoutEffect(() => {
// for realtime resizing and undo/redo
setSize({ width, height });
}, [width, height]);

const handleResize = useCallback((e: MouseEvent | TouchEvent) => {
if (!isResizing.current || !containerRef.current || !containerRect.current) return;
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 clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const handleResize = useCallback(
(e: MouseEvent | TouchEvent) => {
if (!isResizing.current || !containerRef.current || !containerRect.current) return;

const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE);
const newHeight = newWidth / aspectRatio.current;
if (size) {
aspectRatioRef.current = Number(size.width.replace("px", "")) / Number(size.height.replace("px", ""));
}

setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
}, []);
if (!aspectRatioRef.current) 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;

setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
},
[size]
);

const handleResizeEnd = useCallback(() => {
if (isResizing.current) {
Expand All @@ -77,18 +109,23 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
[editor, getPos]
);

useLayoutEffect(() => {
const handleGlobalMouseMove = (e: MouseEvent) => handleResize(e);
const handleGlobalMouseUp = () => handleResizeEnd();
useEffect(() => {
if (!editorContainer) return;

document.addEventListener("mousemove", handleGlobalMouseMove);
document.addEventListener("mouseup", handleGlobalMouseUp);
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 () => {
document.removeEventListener("mousemove", handleGlobalMouseMove);
document.removeEventListener("mouseup", handleGlobalMouseUp);
editorContainer.removeEventListener("mousemove", handleMouseMove);
editorContainer.removeEventListener("mouseup", handleMouseUp);
editorContainer.removeEventListener("mouseleave", handleMouseLeave);
};
}, [handleResize, handleResizeEnd]);
}, [handleResize, handleResizeEnd, editorContainer]);

return (
<div
Expand All @@ -100,12 +137,16 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
height: size.height,
}}
>
{isLoading && <div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />}
{isShimmerVisible && (
<div className="animate-pulse bg-custom-background-80 rounded-md" style={{ width, height }} />
)}
<img
ref={imageRef}
src={src}
width={size.width}
height={size.height}
className={cn("block rounded-md", {
hidden: isLoading,
hidden: isShimmerVisible,
"read-only-image": !editor.isEditable,
})}
style={{
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/core/plugins/image/delete-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag

async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
try {
if (!src) return;
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
await deleteImage(assetUrlWithWorkspaceId);
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/core/plugins/image/restore-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor

async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
try {
if (!src) return;
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
await restoreImage(assetUrlWithWorkspaceId);
} catch (error) {
Expand Down

0 comments on commit 146a500

Please sign in to comment.