mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 01:37:05 +08:00
feat: enhance embed resizer
This commit is contained in:
@@ -2,111 +2,163 @@ import React, { ReactNode, useCallback, useEffect, useRef, useState } from "reac
|
||||
import clsx from "clsx";
|
||||
import classes from "./resizable-wrapper.module.css";
|
||||
|
||||
type Handle = "tl" | "tr" | "bl" | "br" | "bottom";
|
||||
|
||||
const HANDLE_SIGN: Record<Handle, { x: number; y: number }> = {
|
||||
br: { x: 1, y: 1 },
|
||||
bl: { x: -1, y: 1 },
|
||||
tr: { x: 1, y: -1 },
|
||||
tl: { x: -1, y: -1 },
|
||||
bottom: { x: 0, y: 1 },
|
||||
};
|
||||
|
||||
const HANDLE_CURSOR: Record<Handle, string> = {
|
||||
br: "nwse-resize",
|
||||
tl: "nwse-resize",
|
||||
bl: "nesw-resize",
|
||||
tr: "nesw-resize",
|
||||
bottom: "ns-resize",
|
||||
};
|
||||
|
||||
const CORNER_CLASSES: Record<string, string> = {
|
||||
tl: classes.cornerHandleTL,
|
||||
tr: classes.cornerHandleTR,
|
||||
bl: classes.cornerHandleBL,
|
||||
br: classes.cornerHandleBR,
|
||||
};
|
||||
|
||||
interface ResizableWrapperProps {
|
||||
children: ReactNode;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
onResize?: (height: number) => void;
|
||||
onResize?: (width: number, height: number) => void;
|
||||
isEditable?: boolean;
|
||||
className?: string;
|
||||
showHandles?: "always" | "hover";
|
||||
direction?: "vertical" | "horizontal" | "both";
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
type DragState = {
|
||||
handle: Handle;
|
||||
startX: number;
|
||||
startY: number;
|
||||
startWidth: number;
|
||||
startHeight: number;
|
||||
};
|
||||
|
||||
export const ResizableWrapper: React.FC<ResizableWrapperProps> = ({
|
||||
children,
|
||||
initialWidth = 640,
|
||||
initialHeight = 480,
|
||||
minWidth = 200,
|
||||
maxWidth = 1200,
|
||||
minHeight = 200,
|
||||
maxHeight = 1200,
|
||||
onResize,
|
||||
isEditable = true,
|
||||
className,
|
||||
showHandles = "hover",
|
||||
direction = "vertical",
|
||||
selected = false,
|
||||
}) => {
|
||||
const [resizeParams, setResizeParams] = useState<{
|
||||
initialSize: number;
|
||||
initialClientY: number;
|
||||
initialClientX: number;
|
||||
} | null>(null);
|
||||
const [currentHeight, setCurrentHeight] = useState(initialHeight);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!resizeParams) return;
|
||||
const dragRef = useRef<DragState | null>(null);
|
||||
const widthRef = useRef(initialWidth);
|
||||
const heightRef = useRef(initialHeight);
|
||||
const onResizeRef = useRef(onResize);
|
||||
onResizeRef.current = onResize;
|
||||
const constraintsRef = useRef({ minWidth, maxWidth, minHeight, maxHeight });
|
||||
constraintsRef.current = { minWidth, maxWidth, minHeight, maxHeight };
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!wrapperRef.current) return;
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
const drag = dragRef.current;
|
||||
if (!drag || !wrapperRef.current) return;
|
||||
|
||||
if (direction === "vertical" || direction === "both") {
|
||||
const deltaY = e.clientY - resizeParams.initialClientY;
|
||||
const newHeight = Math.min(
|
||||
Math.max(resizeParams.initialSize + deltaY, minHeight),
|
||||
maxHeight
|
||||
);
|
||||
setCurrentHeight(newHeight);
|
||||
wrapperRef.current.style.height = `${newHeight}px`;
|
||||
}
|
||||
const sign = HANDLE_SIGN[drag.handle];
|
||||
const { minWidth, maxWidth, minHeight, maxHeight } = constraintsRef.current;
|
||||
|
||||
const deltaY = e.clientY - drag.startY;
|
||||
const newHeight = Math.min(Math.max(drag.startHeight + deltaY * sign.y, minHeight), maxHeight);
|
||||
heightRef.current = newHeight;
|
||||
wrapperRef.current.style.height = `${newHeight}px`;
|
||||
|
||||
if (sign.x !== 0) {
|
||||
const deltaX = e.clientX - drag.startX;
|
||||
const newWidth = Math.min(Math.max(drag.startWidth + deltaX * sign.x, minWidth), maxWidth);
|
||||
widthRef.current = newWidth;
|
||||
wrapperRef.current.style.width = `${newWidth}px`;
|
||||
}
|
||||
}).current;
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
dragRef.current = null;
|
||||
setIsResizing(false);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
onResizeRef.current?.(widthRef.current, heightRef.current);
|
||||
}).current;
|
||||
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent, handle: Handle) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragRef.current = {
|
||||
handle,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startWidth: widthRef.current,
|
||||
startHeight: heightRef.current,
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setResizeParams(null);
|
||||
if (onResize && currentHeight !== initialHeight) {
|
||||
onResize(currentHeight);
|
||||
}
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
setIsResizing(true);
|
||||
document.body.style.cursor = HANDLE_CURSOR[handle];
|
||||
document.body.style.userSelect = "none";
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}, [handleMouseMove, handleMouseUp]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [resizeParams, currentHeight, initialHeight, onResize, minHeight, maxHeight, direction]);
|
||||
}, [handleMouseMove, handleMouseUp]);
|
||||
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setResizeParams({
|
||||
initialSize: currentHeight,
|
||||
initialClientY: e.clientY,
|
||||
initialClientX: e.clientX,
|
||||
});
|
||||
|
||||
document.body.style.cursor = "ns-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}, [currentHeight]);
|
||||
|
||||
const shouldShowHandles =
|
||||
isEditable &&
|
||||
(showHandles === "always" || (showHandles === "hover" && (isHovered || resizeParams)));
|
||||
const shouldShowHandles = isEditable && (isHovered || isResizing || selected);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={clsx(classes.wrapper, className, {
|
||||
[classes.resizing]: !!resizeParams,
|
||||
[classes.resizing]: isResizing,
|
||||
})}
|
||||
style={{ height: currentHeight }}
|
||||
style={{ width: widthRef.current, height: heightRef.current }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{children}
|
||||
{!!resizeParams && <div className={classes.overlay} />}
|
||||
{shouldShowHandles && direction === "vertical" && (
|
||||
<div
|
||||
className={classes.resizeHandleBottom}
|
||||
onMouseDown={handleResizeStart}
|
||||
>
|
||||
<div className={classes.resizeBar} />
|
||||
</div>
|
||||
{isResizing && <div className={classes.overlay} />}
|
||||
{shouldShowHandles && (
|
||||
<>
|
||||
{(["tl", "tr", "bl", "br"] as const).map((corner) => (
|
||||
<div
|
||||
key={corner}
|
||||
className={clsx(classes.cornerHandle, CORNER_CLASSES[corner])}
|
||||
onMouseDown={(e) => handleResizeStart(e, corner)}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className={classes.resizeHandleBottom}
|
||||
onMouseDown={(e) => handleResizeStart(e, "bottom")}
|
||||
>
|
||||
<div className={classes.resizeBar} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user