mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 18:16:57 +08:00
feat: add resizable embed component │ (#1401)
- Created reusable ResizableWrapper component - Added drag-to-resize functionality for embeds
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.resizing {
|
||||
user-select: none;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.resizeHandleBottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 24px;
|
||||
cursor: ns-resize;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
touch-action: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
@mixin light {
|
||||
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.05)
|
||||
);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@mixin light {
|
||||
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper:hover .resizeHandleBottom,
|
||||
.resizing .resizeHandleBottom {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.resizeBar {
|
||||
width: 50px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-5);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-gray-6);
|
||||
}
|
||||
}
|
||||
|
||||
.resizeHandleBottom:hover .resizeBar,
|
||||
.resizing .resizeBar {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-7);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-gray-4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import classes from "./resizable-wrapper.module.css";
|
||||
|
||||
interface ResizableWrapperProps {
|
||||
children: ReactNode;
|
||||
initialHeight?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
onResize?: (height: number) => void;
|
||||
isEditable?: boolean;
|
||||
className?: string;
|
||||
showHandles?: "always" | "hover";
|
||||
direction?: "vertical" | "horizontal" | "both";
|
||||
}
|
||||
|
||||
export const ResizableWrapper: React.FC<ResizableWrapperProps> = ({
|
||||
children,
|
||||
initialHeight = 480,
|
||||
minHeight = 200,
|
||||
maxHeight = 1200,
|
||||
onResize,
|
||||
isEditable = true,
|
||||
className,
|
||||
showHandles = "hover",
|
||||
direction = "vertical",
|
||||
}) => {
|
||||
const [resizeParams, setResizeParams] = useState<{
|
||||
initialSize: number;
|
||||
initialClientY: number;
|
||||
initialClientX: number;
|
||||
} | null>(null);
|
||||
const [currentHeight, setCurrentHeight] = useState(initialHeight);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!resizeParams) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!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 handleMouseUp = () => {
|
||||
setResizeParams(null);
|
||||
if (onResize && currentHeight !== initialHeight) {
|
||||
onResize(currentHeight);
|
||||
}
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [resizeParams, currentHeight, initialHeight, onResize, minHeight, maxHeight, direction]);
|
||||
|
||||
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)));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={clsx(classes.wrapper, className, {
|
||||
[classes.resizing]: !!resizeParams,
|
||||
})}
|
||||
style={{ height: currentHeight }}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user