[WEB-2450] fix: image resize component (#5623)
* 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
This commit is contained in:
parent
7d7415b235
commit
146a500f9f
3 changed files with 78 additions and 35 deletions
|
|
@ -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";
|
import { NodeSelection } from "@tiptap/pm/state";
|
||||||
// extensions
|
// extensions
|
||||||
import { CustomImageNodeViewProps } from "@/extensions/custom-image";
|
import { CustomImageNodeViewProps } from "@/extensions/custom-image";
|
||||||
|
|
@ -13,52 +13,84 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
|
||||||
|
|
||||||
const [size, setSize] = useState({ width: width || "35%", height: height || "auto" });
|
const [size, setSize] = useState({ width: width || "35%", height: height || "auto" });
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRect = useRef<DOMRect | null>(null);
|
const containerRect = useRef<DOMRect | null>(null);
|
||||||
const imageRef = useRef<HTMLImageElement>(null);
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
const isResizing = useRef(false);
|
const isResizing = useRef(false);
|
||||||
const aspectRatio = useRef(1);
|
const aspectRatioRef = useRef<number | null>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (imageRef.current) {
|
if (imageRef.current) {
|
||||||
const img = imageRef.current;
|
const img = imageRef.current;
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
if (node.attrs.width === "35%" && node.attrs.height === "auto") {
|
const closestEditorContainer = img.closest(".editor-container");
|
||||||
aspectRatio.current = img.naturalWidth / img.naturalHeight;
|
if (!closestEditorContainer) {
|
||||||
const initialWidth = Math.max(img.naturalWidth * 0.35, MIN_SIZE);
|
console.error("Editor container not found");
|
||||||
const initialHeight = initialWidth / aspectRatio.current;
|
return;
|
||||||
setSize({ width: `${initialWidth}px`, height: `${initialHeight}px` });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [src]);
|
}, [width, height, updateAttributes]);
|
||||||
|
|
||||||
const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
isResizing.current = true;
|
|
||||||
if (containerRef.current) {
|
|
||||||
containerRect.current = containerRef.current.getBoundingClientRect();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// for realtime resizing and undo/redo
|
|
||||||
setSize({ width, height });
|
setSize({ width, height });
|
||||||
}, [width, height]);
|
}, [width, height]);
|
||||||
|
|
||||||
const handleResize = useCallback((e: MouseEvent | TouchEvent) => {
|
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 (!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;
|
||||||
|
|
||||||
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
||||||
|
|
||||||
const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE);
|
const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE);
|
||||||
const newHeight = newWidth / aspectRatio.current;
|
const newHeight = newWidth / aspectRatioRef.current;
|
||||||
|
|
||||||
setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
|
setSize({ width: `${newWidth}px`, height: `${newHeight}px` });
|
||||||
}, []);
|
},
|
||||||
|
[size]
|
||||||
|
);
|
||||||
|
|
||||||
const handleResizeEnd = useCallback(() => {
|
const handleResizeEnd = useCallback(() => {
|
||||||
if (isResizing.current) {
|
if (isResizing.current) {
|
||||||
|
|
@ -77,18 +109,23 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
|
||||||
[editor, getPos]
|
[editor, getPos]
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
const handleGlobalMouseMove = (e: MouseEvent) => handleResize(e);
|
if (!editorContainer) return;
|
||||||
const handleGlobalMouseUp = () => handleResizeEnd();
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleGlobalMouseMove);
|
const handleMouseMove = (e: MouseEvent) => handleResize(e);
|
||||||
document.addEventListener("mouseup", handleGlobalMouseUp);
|
const handleMouseUp = () => handleResizeEnd();
|
||||||
|
const handleMouseLeave = () => handleResizeEnd();
|
||||||
|
|
||||||
|
editorContainer.addEventListener("mousemove", handleMouseMove);
|
||||||
|
editorContainer.addEventListener("mouseup", handleMouseUp);
|
||||||
|
editorContainer.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousemove", handleGlobalMouseMove);
|
editorContainer.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener("mouseup", handleGlobalMouseUp);
|
editorContainer.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
editorContainer.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
};
|
};
|
||||||
}, [handleResize, handleResizeEnd]);
|
}, [handleResize, handleResizeEnd, editorContainer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -100,12 +137,16 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
|
||||||
height: size.height,
|
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
|
<img
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
src={src}
|
src={src}
|
||||||
|
width={size.width}
|
||||||
|
height={size.height}
|
||||||
className={cn("block rounded-md", {
|
className={cn("block rounded-md", {
|
||||||
hidden: isLoading,
|
hidden: isShimmerVisible,
|
||||||
"read-only-image": !editor.isEditable,
|
"read-only-image": !editor.isEditable,
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag
|
||||||
|
|
||||||
async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!src) return;
|
||||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||||
await deleteImage(assetUrlWithWorkspaceId);
|
await deleteImage(assetUrlWithWorkspaceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
|
||||||
|
|
||||||
async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
|
async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!src) return;
|
||||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||||
await restoreImage(assetUrlWithWorkspaceId);
|
await restoreImage(assetUrlWithWorkspaceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue