buster/web/src/components/layout/AppSplitter/AppSplitter.tsx

267 lines
7.8 KiB
TypeScript
Raw Normal View History

2025-01-16 02:51:59 +08:00
'use client';
import { useMemoizedFn } from 'ahooks';
2025-01-16 02:51:59 +08:00
import React, { useEffect, useMemo, useState, forwardRef, useImperativeHandle } from 'react';
import SplitPane, { Pane } from './SplitPane';
import { createAutoSaveId } from './helper';
import Cookies from 'js-cookie';
2025-01-10 03:04:57 +08:00
import { createStyles } from 'antd-style';
2025-01-10 05:00:40 +08:00
// First, define the ref type
export interface AppSplitterRef {
setSplitSizes: (newSizes: (number | string)[]) => void;
2025-01-23 08:35:17 +08:00
animateWidth: (width: string, side: 'left' | 'right', duration?: number) => Promise<void>;
2025-01-10 05:00:40 +08:00
}
2025-01-10 05:00:40 +08:00
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,
2025-01-23 08:35:17 +08:00
className = '',
splitterClassName = '',
2025-01-10 05:00:40 +08:00
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]);
2025-01-10 05:00:40 +08:00
const _sizes = useMemo(
() => (hasHidden ? (leftHidden ? ['0px', 'auto'] : ['auto', '0px']) : sizes),
[hasHidden, leftHidden, sizes]
);
2025-01-10 05:00:40 +08:00
const memoizedLeftPaneStyle = useMemo(() => {
return {
display: leftHidden ? 'none' : undefined
};
}, [leftHidden]);
2025-01-10 05:00:40 +08:00
const memoizedRightPaneStyle = useMemo(() => {
return {
display: rightHidden ? 'none' : undefined
};
}, [rightHidden]);
2025-01-10 05:00:40 +08:00
const sashRender = useMemoizedFn((_: number, active: boolean) => (
<AppSplitterSash
hideSplitter={hideSplitter}
active={active}
splitterClassName={splitterClassName}
splitDirection={split}
/>
));
2025-01-10 05:00:40 +08:00
const onDragEnd = useMemoizedFn(() => {
setIsDragging(false);
});
2025-01-10 05:00:40 +08:00
const onDragStart = useMemoizedFn(() => {
setIsDragging(true);
});
2025-01-10 05:00:40 +08:00
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 });
});
2025-01-10 05:00:40 +08:00
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]);
2025-01-23 08:35:17 +08:00
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)
);
2025-01-10 05:00:40 +08:00
}
}
2025-01-23 08:35:17 +08:00
);
// Add useImperativeHandle to expose the function
useImperativeHandle(ref, () => ({
setSplitSizes,
animateWidth
2025-01-10 05:00:40 +08:00
}));
2025-01-10 05:00:40 +08:00
return (
2025-01-23 08:35:17 +08:00
<div className="h-full w-full">
2025-01-10 05:00:40 +08:00
<SplitPane
split={split}
className={`${className}`}
sizes={_sizes}
style={style}
allowResize={_allowResize}
onChange={onChangePanels}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
resizerSize={3}
sashRender={sashRender}>
<Pane
style={memoizedLeftPaneStyle}
2025-01-23 08:35:17 +08:00
className="left-pane flex h-full flex-col"
2025-01-10 05:00:40 +08:00
minSize={leftPanelMinSize}
maxSize={leftPanelMaxSize}>
{leftHidden ? null : leftChildren}
</Pane>
<Pane
2025-01-23 08:35:17 +08:00
className="right-pane flex h-full flex-col"
2025-01-10 05:00:40 +08:00
style={memoizedRightPaneStyle}
minSize={rightPanelMinSize}
maxSize={rightPanelMaxSize}>
{rightHidden ? null : rightChildren}
</Pane>
</SplitPane>
</div>
);
}
);
AppSplitter.displayName = 'AppSplitter';
const AppSplitterSash: React.FC<{
active: boolean;
splitterClassName?: string;
hideSplitter?: boolean;
2025-01-10 03:04:57 +08:00
splitDirection?: 'vertical' | 'horizontal';
}> = React.memo(
({ active, splitterClassName = '', hideSplitter = false, splitDirection = 'vertical' }) => {
const { styles, cx } = useStyles();
2025-01-10 03:04:57 +08:00
return (
<div
className={cx(
splitterClassName,
styles.splitter,
'absolute transition',
`cursor-${splitDirection}-resize`,
splitDirection === 'vertical' ? 'h-full w-[0.5px]' : 'h-[0.5px] w-full',
hideSplitter && 'hide',
active && 'active',
!active && 'inactive'
)}
/>
);
}
);
AppSplitterSash.displayName = 'AppSplitterSash';
2025-01-10 03:04:57 +08:00
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};
}
}
`
}));