'use client'; import { useMemoizedFn } from 'ahooks'; import React, { useEffect, useMemo, useState, forwardRef, useImperativeHandle } from 'react'; import SplitPane, { Pane } from './SplitPane'; import { createAutoSaveId } from './helper'; import Cookies from 'js-cookie'; import { createStyles } from 'antd-style'; // First, define the ref type export interface AppSplitterRef { setSplitSizes: (newSizes: (number | string)[]) => void; animateWidth: (width: string, side: 'left' | 'right', duration?: number) => Promise; } export const AppSplitter = forwardRef< AppSplitterRef, { leftChildren: React.ReactNode; rightChildren: React.ReactNode; autoSaveId: string; defaultLayout: (string | number)[]; leftPanelMinSize?: number | string; rightPanelMinSize?: number | string; leftPanelMaxSize?: number | string; rightPanelMaxSize?: number | string; className?: string; allowResize?: boolean; split?: 'vertical' | 'horizontal'; splitterClassName?: string; preserveSide: 'left' | 'right' | null; rightHidden?: boolean; leftHidden?: boolean; style?: React.CSSProperties; hideSplitter?: boolean; } >( ( { style, leftChildren, preserveSide, rightChildren, autoSaveId, defaultLayout, leftPanelMinSize, rightPanelMinSize, split = 'vertical', leftPanelMaxSize, rightPanelMaxSize, allowResize, className = '', splitterClassName = '', leftHidden, rightHidden, hideSplitter }, ref ) => { const [isDragging, setIsDragging] = useState(false); const [sizes, setSizes] = useState<(number | string)[]>(defaultLayout); const hasHidden = useMemo(() => leftHidden || rightHidden, [leftHidden, rightHidden]); const _allowResize = useMemo(() => (hasHidden ? false : allowResize), [hasHidden, allowResize]); const _sizes = useMemo( () => (hasHidden ? (leftHidden ? ['0px', 'auto'] : ['auto', '0px']) : sizes), [hasHidden, leftHidden, sizes] ); const memoizedLeftPaneStyle = useMemo(() => { return { display: leftHidden ? 'none' : undefined }; }, [leftHidden]); const memoizedRightPaneStyle = useMemo(() => { return { display: rightHidden ? 'none' : undefined }; }, [rightHidden]); const sashRender = useMemoizedFn((_: number, active: boolean) => ( )); const onDragEnd = useMemoizedFn(() => { setIsDragging(false); }); const onDragStart = useMemoizedFn(() => { setIsDragging(true); }); const onChangePanels = useMemoizedFn((sizes: number[]) => { if (!isDragging) return; setSizes(sizes); const key = createAutoSaveId(autoSaveId); const sizesString = preserveSide === 'left' ? [sizes[0], 'auto'] : ['auto', sizes[1]]; Cookies.set(key, JSON.stringify(sizesString), { expires: 365 }); }); const onPreserveSide = useMemoizedFn(() => { const [left, right] = sizes; if (preserveSide === 'left') { setSizes([left, 'auto']); } else if (preserveSide === 'right') { setSizes(['auto', right]); } }); useEffect(() => { if (preserveSide && !hideSplitter && split === 'vertical') { window.addEventListener('resize', onPreserveSide); return () => { window.removeEventListener('resize', onPreserveSide); }; } }, [preserveSide]); const setSplitSizes = useMemoizedFn((newSizes: (number | string)[]) => { setSizes(newSizes); if (preserveSide) { const key = createAutoSaveId(autoSaveId); const sizesString = preserveSide === 'left' ? [newSizes[0], 'auto'] : ['auto', newSizes[1]]; Cookies.set(key, JSON.stringify(sizesString), { expires: 365 }); } }); const easeInOutCubic = useMemoizedFn((t: number): number => { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }); const animateWidth = useMemoizedFn( async (width: string, side: 'left' | 'right', duration = 0.25) => { const leftPanelSize = _sizes[0]; const rightPanelSize = _sizes[1]; const currentSize = side === 'left' ? leftPanelSize : rightPanelSize; console.log(_sizes); // Convert percentage strings to numbers const currentSizeNumber = parseFloat(String(currentSize).replace('%', '')); const targetSizeNumber = parseFloat(width.replace('%', '')); const NUMBER_OF_STEPS = 30; const stepDuration = (duration * 1000) / NUMBER_OF_STEPS; const startTime = performance.now(); for (let i = 0; i < NUMBER_OF_STEPS + 1; i++) { await new Promise((resolve) => setTimeout(() => { const progress = i / NUMBER_OF_STEPS; // 0 to 1 const easedProgress = easeInOutCubic(progress); const newSizeNumber = currentSizeNumber + (targetSizeNumber - currentSizeNumber) * easedProgress; const newSize = `${newSizeNumber}%`; // Calculate the other side's size to maintain 100% total const otherSize = `${100 - newSizeNumber}%`; // Update both sides const newSizes = side === 'left' ? [newSize, otherSize] : [otherSize, newSize]; setSplitSizes(newSizes); resolve(true); }, stepDuration) ); } } ); // Add useImperativeHandle to expose the function useImperativeHandle(ref, () => ({ setSplitSizes, animateWidth })); return (
{leftHidden ? null : leftChildren} {rightHidden ? null : rightChildren}
); } ); AppSplitter.displayName = 'AppSplitter'; const AppSplitterSash: React.FC<{ active: boolean; splitterClassName?: string; hideSplitter?: boolean; splitDirection?: 'vertical' | 'horizontal'; }> = React.memo( ({ active, splitterClassName = '', hideSplitter = false, splitDirection = 'vertical' }) => { const { styles, cx } = useStyles(); return (
); } ); AppSplitterSash.displayName = 'AppSplitterSash'; const useStyles = createStyles(({ css, token }) => ({ splitter: css` background: ${token.colorPrimary}; left: 1px; &.hide { background: transparent; &.active { background: ${token.colorBorder}; } } &:not(.hide) { &.active { background: ${token.colorPrimary}; } &.inactive { background: ${token.colorBorder}; } } ` }));