@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
425 lines (424 loc) • 13.6 kB
JavaScript
"use client";
import Center from "../Flex/Center.mjs";
import Icon from "../Icon/Icon.mjs";
import { handleVariants, panelVariants, styles, toggleVariants } from "./style.mjs";
import { reversePlacement } from "./utils.mjs";
import { memo, startTransition, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { ConfigProvider } from "antd";
import { cx } from "antd-style";
import useMergeState from "use-merge-value";
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-react";
import { useHover } from "ahooks";
import isEqual from "fast-deep-equal";
import { Resizable } from "re-resizable";
//#region src/DraggablePanel/DraggablePanel.tsx
const ARROW_MAP = {
bottom: ChevronUp,
left: ChevronRight,
right: ChevronLeft,
top: ChevronDown
};
const MARGIN_MAP = {
bottom: { marginTop: 4 },
left: { marginRight: 4 },
right: { marginLeft: 4 },
top: { marginBottom: 4 }
};
const DISABLED_RESIZING = {
bottom: false,
bottomLeft: false,
bottomRight: false,
left: false,
right: false,
top: false,
topLeft: false,
topRight: false
};
const toCssSize = (value, fallback) => {
if (typeof value === "number") return `${Math.max(value, 0)}px`;
if (typeof value === "string" && value.length > 0) return value;
return fallback;
};
const DraggablePanel = memo(({ headerHeight = 0, fullscreen, maxHeight, pin = true, mode = "fixed", children, placement = "right", resize, style, showBorder = true, showHandleHighlight = false, showHandleWideArea = true, backgroundColor, size, stableLayout = false, defaultSize: customizeDefaultSize, minWidth, minHeight, maxWidth, onSizeChange, onSizeDragging, expandable = true, expand, defaultExpand = true, onExpandChange, className, showHandleWhenCollapsed, destroyOnClose, styles: customStyles, classNames, dir }) => {
const ref = useRef(null);
const isHovering = useHover(ref);
const isVertical = placement === "top" || placement === "bottom";
const hoverTimeoutRef = useRef(void 0);
const resetTransitionTimeoutRef = useRef(void 0);
const resizableRef = useRef(null);
const initialExpandedSizeRef = useRef(void 0);
const { direction: antdDirection } = use(ConfigProvider.ConfigContext);
const direction = dir ?? antdDirection;
const internalPlacement = useMemo(() => {
if (direction !== "rtl") return placement;
if (placement === "left") return "right";
if (placement === "right") return "left";
return placement;
}, [direction, placement]);
const cssVariables = {
"--draggable-panel-bg": backgroundColor || "",
"--draggable-panel-header-height": `${headerHeight}px`
};
const [isExpand, setIsExpand] = useMergeState(defaultExpand, {
onChange: onExpandChange,
value: expand
});
const [shouldTransition, setShouldTransition] = useState(true);
const [showExpand, setShowExpand] = useState(true);
useEffect(() => {
if (pin) return;
if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);
if (isHovering && !isExpand) startTransition(() => setIsExpand(true));
else if (!isHovering && isExpand) hoverTimeoutRef.current = setTimeout(() => {
startTransition(() => setIsExpand(false));
}, 150);
}, [
pin,
isHovering,
isExpand,
setIsExpand
]);
useEffect(() => {
return () => {
if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);
if (resetTransitionTimeoutRef.current) clearTimeout(resetTransitionTimeoutRef.current);
};
}, []);
useEffect(() => {
initialExpandedSizeRef.current = void 0;
}, [internalPlacement]);
const reversed = reversePlacement(internalPlacement);
const canResizing = resize !== false && isExpand;
const resizing = useMemo(() => ({
bottom: false,
bottomLeft: false,
bottomRight: false,
left: false,
right: false,
top: false,
topLeft: false,
topRight: false,
[reversed]: true,
...resize
}), [reversed, resize]);
const defaultSize = useMemo(() => {
if (isVertical) return {
height: 180,
width: "100%",
...customizeDefaultSize
};
return {
height: "100%",
width: 280,
...customizeDefaultSize
};
}, [isVertical, customizeDefaultSize]);
const normalizedMaxHeight = typeof maxHeight === "number" ? Math.max(maxHeight, 0) : void 0;
const normalizedMaxWidth = typeof maxWidth === "number" ? Math.max(maxWidth, 0) : void 0;
const normalizedMinHeight = typeof minHeight === "number" ? Math.max(minHeight, 0) : void 0;
const normalizedMinWidth = typeof minWidth === "number" ? Math.max(minWidth, 0) : void 0;
const sizeProps = useMemo(() => {
if (!stableLayout && !isExpand) return isVertical ? {
minHeight: 0,
size: { height: 0 }
} : {
minWidth: 0,
size: { width: 0 }
};
return {
defaultSize,
maxHeight: normalizedMaxHeight,
maxWidth: normalizedMaxWidth,
minHeight: normalizedMinHeight,
minWidth: normalizedMinWidth,
size
};
}, [
stableLayout,
isExpand,
isVertical,
defaultSize,
normalizedMaxHeight,
normalizedMaxWidth,
normalizedMinHeight,
normalizedMinWidth,
size
]);
const fallbackExpandedSize = isVertical ? "180px" : "280px";
const controlledExpandedSize = useMemo(() => {
const controlledSize = isVertical ? size?.height : size?.width;
if (controlledSize === void 0) return void 0;
return toCssSize(controlledSize, fallbackExpandedSize);
}, [
isVertical,
size?.height,
size?.width,
fallbackExpandedSize
]);
const defaultExpandedSize = useMemo(() => {
return toCssSize(isVertical ? defaultSize.height : defaultSize.width, fallbackExpandedSize);
}, [
isVertical,
defaultSize.height,
defaultSize.width,
fallbackExpandedSize
]);
const [resizedExpandedSize, setResizedExpandedSize] = useState({});
const expandedOuterSize = controlledExpandedSize ?? (isVertical ? resizedExpandedSize.vertical : resizedExpandedSize.horizontal) ?? defaultExpandedSize;
const setExpandedMainSize = useCallback((nextSize) => {
if (!stableLayout) return;
const currentSize = isVertical ? nextSize.height : nextSize.width;
if (!currentSize) return;
const normalizedSize = toCssSize(currentSize, fallbackExpandedSize);
setResizedExpandedSize((state) => isVertical ? {
...state,
vertical: normalizedSize
} : {
...state,
horizontal: normalizedSize
});
}, [
fallbackExpandedSize,
isVertical,
stableLayout
]);
const captureInitialExpandedSize = useCallback(() => {
if (initialExpandedSizeRef.current) return initialExpandedSizeRef.current;
const rect = resizableRef.current?.resizable?.getBoundingClientRect();
if (!rect) return void 0;
const nextInitialSize = isVertical ? {
height: rect.height,
width: "100%"
} : {
height: "100%",
width: rect.width
};
initialExpandedSizeRef.current = nextInitialSize;
return nextInitialSize;
}, [isVertical]);
useEffect(() => {
if (!isExpand) return;
captureInitialExpandedSize();
}, [captureInitialExpandedSize, isExpand]);
const toggleExpand = useCallback(() => {
if (expandable) setIsExpand(!isExpand);
}, [
expandable,
isExpand,
setIsExpand
]);
const clampResizeSize = useCallback((el) => {
const rect = el.getBoundingClientRect();
const currentMainSize = isVertical ? rect.height : rect.width;
const minMainSize = isVertical ? normalizedMinHeight : normalizedMinWidth;
const maxMainSize = isVertical ? normalizedMaxHeight : normalizedMaxWidth;
let clampedMainSize = currentMainSize;
if (typeof minMainSize === "number") clampedMainSize = Math.max(clampedMainSize, minMainSize);
if (typeof maxMainSize === "number") clampedMainSize = Math.min(clampedMainSize, maxMainSize);
if (!Number.isFinite(clampedMainSize) || Math.abs(clampedMainSize - currentMainSize) < .5) return {
height: el.style.height,
width: el.style.width
};
const width = isVertical ? el.style.width || "100%" : `${clampedMainSize}px`;
const height = isVertical ? `${clampedMainSize}px` : el.style.height || "100%";
resizableRef.current?.updateSize({
height,
width
});
return {
height,
width
};
}, [
isVertical,
normalizedMaxHeight,
normalizedMaxWidth,
normalizedMinHeight,
normalizedMinWidth
]);
const handleResize = useCallback((_event, _direction, el, delta) => {
const nextSize = clampResizeSize(el);
setExpandedMainSize(nextSize);
onSizeDragging?.(delta, nextSize);
}, [
clampResizeSize,
onSizeDragging,
setExpandedMainSize
]);
const triggerResetWithoutTransition = useCallback(() => {
if (resetTransitionTimeoutRef.current) clearTimeout(resetTransitionTimeoutRef.current);
setShouldTransition(false);
resetTransitionTimeoutRef.current = setTimeout(() => {
setShouldTransition(true);
}, 0);
}, []);
const handleResetSize = useCallback(() => {
if (!canResizing) return;
const resetSize = captureInitialExpandedSize();
if (!resetSize) return;
triggerResetWithoutTransition();
const rect = resizableRef.current?.resizable?.getBoundingClientRect();
const prevMainSize = rect ? isVertical ? rect.height : rect.width : 0;
const resetMainSize = isVertical ? resetSize.height : resetSize.width;
const nextMainSize = typeof resetMainSize === "number" ? resetMainSize : prevMainSize;
resizableRef.current?.updateSize(resetSize);
setExpandedMainSize(resetSize);
onSizeChange?.(isVertical ? {
height: nextMainSize - prevMainSize,
width: 0
} : {
height: 0,
width: nextMainSize - prevMainSize
}, resetSize);
}, [
canResizing,
captureInitialExpandedSize,
isVertical,
onSizeChange,
setExpandedMainSize,
triggerResetWithoutTransition
]);
const handleResizeStart = useCallback((event) => {
if (event.detail === 2) {
handleResetSize();
return false;
}
if (resetTransitionTimeoutRef.current) {
clearTimeout(resetTransitionTimeoutRef.current);
resetTransitionTimeoutRef.current = void 0;
}
setShouldTransition(false);
setShowExpand(false);
}, [handleResetSize]);
const handleResizeStop = useCallback((_event, _direction, el, delta) => {
const nextSize = clampResizeSize(el);
setExpandedMainSize(nextSize);
setShouldTransition(true);
setShowExpand(true);
onSizeChange?.(delta, nextSize);
}, [
clampResizeSize,
onSizeChange,
setExpandedMainSize
]);
const resizeHandleClassName = useMemo(() => cx(handleVariants({ placement: reversed }), showHandleHighlight && styles.handleHighlight), [reversed, showHandleHighlight]);
if (fullscreen) return /* @__PURE__ */ jsx("div", {
className: cx(styles.fullscreen, className),
style: cssVariables,
children
});
const Arrow = ARROW_MAP[internalPlacement] ?? ChevronLeft;
const stableOuterFlex = stableLayout ? {
display: "flex",
flexDirection: "column",
minHeight: 0
} : {};
const sidebarOuterStyle = isVertical ? {
height: isExpand ? expandedOuterSize : 0,
overflow: "hidden",
transition: shouldTransition ? "height 0.2s var(--ant-motion-ease-out, ease)" : "none",
width: "100%",
...stableOuterFlex
} : {
overflow: "hidden",
transition: shouldTransition ? "width 0.2s var(--ant-motion-ease-out, ease)" : "none",
width: isExpand ? expandedOuterSize : 0,
...stableLayout ? {
...stableOuterFlex,
flex: 1,
minWidth: 0,
height: "100%"
} : {}
};
const sidebarInnerStyle = stableLayout ? {
display: "flex",
flex: 1,
flexDirection: "column",
height: "100%",
minHeight: 0,
minWidth: 0,
width: "100%"
} : isVertical ? {
height: "100%",
width: "100%"
} : { width: "100%" };
const stableAsideStyle = stableLayout ? {
display: "flex",
flexDirection: "column",
minHeight: 0,
...mode === "fixed" ? { height: "100%" } : {}
} : {};
const stableResizableStyle = {
display: "flex",
flex: 1,
flexDirection: "column",
height: "100%",
minHeight: 0,
minWidth: 0,
width: "100%"
};
const panelNode = (!destroyOnClose || isExpand) && /* @__PURE__ */ jsx(Resizable, {
ref: resizableRef,
...sizeProps,
className: cx(styles.panel, classNames?.content),
enable: canResizing ? resizing : DISABLED_RESIZING,
handleClasses: canResizing ? { [reversed]: resizeHandleClassName } : {},
style: {
...cssVariables,
transition: shouldTransition ? void 0 : "none",
...stableLayout ? stableResizableStyle : {},
...style
},
onResize: handleResize,
onResizeStart: handleResizeStart,
onResizeStop: handleResizeStop,
children: stableLayout ? /* @__PURE__ */ jsx("div", {
style: sidebarInnerStyle,
children
}) : children
});
return /* @__PURE__ */ jsxs("aside", {
dir,
ref,
style: {
...cssVariables,
...stableAsideStyle
},
className: cx(panelVariants({
isExpand,
mode,
placement: internalPlacement,
showBorder
}), className),
children: [expandable && showExpand && /* @__PURE__ */ jsx(Center, {
className: toggleVariants({
placement: internalPlacement,
showHandleWideArea
}),
style: { opacity: isExpand ? pin ? void 0 : 0 : showHandleWhenCollapsed ? 1 : 0 },
children: /* @__PURE__ */ jsx(Center, {
className: classNames?.handle,
style: customStyles?.handle,
onClick: toggleExpand,
children: /* @__PURE__ */ jsx(Icon, {
className: styles.handlerIcon,
icon: Arrow,
size: 16,
style: {
...MARGIN_MAP[internalPlacement],
transform: `rotate(${isExpand ? 180 : 0}deg)`,
transition: "transform 0.3s ease"
}
})
})
}), stableLayout ? /* @__PURE__ */ jsx("div", {
style: sidebarOuterStyle,
children: panelNode
}) : panelNode]
});
}, isEqual);
DraggablePanel.displayName = "DraggablePanel";
//#endregion
export { DraggablePanel as default };
//# sourceMappingURL=DraggablePanel.mjs.map