UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

425 lines (424 loc) 13.6 kB
"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