https://github.com/reactjs/react-lifecycles-compat#readme
https://github.com/tomkp/react-split-pane/blob/fc641309a90d7620c61ab99bc26399b4142121b7/src/SplitPane.js#L36
https://leefsmp.github.io/Re-Flex/index.html
https://openbase.com/js/react-reflex/documentation
- 也可通过 flex-basic 及其他配合属性进行设置
.panel {
flex-basic: 0;
flex-shrink: 0;
}
Spliter
```typescript import React, { useMemo } from “@alipay/bigfish/react”; import cs from “@alipay/bigfish/util/classnames”; import { SpliterProps } from “./interface”;
import “./index.less”;
const Spliter = React.forwardRef
return (
<span ref={ref} className={cls} style={style} {...restProps}>
{props.children && (
<span className="nz-spliter-resize-content">{props.children}</span>
)}
</span>
);
} );
export default Spliter;
<a name="Zu58u"></a>
#### Pane
```typescript
import React, { useMemo } from "@alipay/bigfish/react";
import { PanelProps } from "./interface";
const Pane = React.forwardRef<HTMLDivElement, PanelProps>((props, ref) => {
const { style, className } = props;
const newStylie = useMemo(() => {
return Object.assign({}, { flex: 1 }, style);
}, [style]);
return (
<div ref={ref} style={newStylie} className={className}>
{props.children}
</div>
);
});
Pane.displayName = "Pane";
export default Pane;
interface.ts
/** 横向 | 纵向 */
export type SplitLayout = "horizontal" | "vertical";
export type Size = string | number;
interface BaseProps {
className?: string;
style?: React.CSSProperties;
children?: any;
}
/** 事件 */
export interface SupportEventProps<T = Element>
extends Omit<React.DOMAttributes<T>, "children"> {
onDragFinished?(size: any): void;
onDragStarted?(): void;
}
export interface SpliterProps extends BaseProps, SupportEventProps {
icon?: React.ReactNode;
onResizerClick?: (event: MouseEvent) => void;
onResizerDoubleClick?: (event: MouseEvent) => void;
}
type FooterRender = false | React.ReactNode | (() => JSX.Element)
export interface NzSplitProps extends BaseProps, SupportEventProps {
minSize?: Size;
maxSize?: Size;
defaultSize?: Size;
layout?: SplitLayout;
bodyStyle?: React.CSSProperties;
/** 分隔线样式 */
spliterStyle?: React.CSSProperties;
spliterClassName?: string;
/** 是否允许缩放 */
allowResize?: boolean;
onResizerClick?: (event: MouseEvent) => void;
onResizerDoubleClick?: (event: MouseEvent) => void;
footer?: FooterRender;
}
export interface PanelProps extends BaseProps, SupportEventProps {
size?: Size;
}
export interface PositionInfo {
clientX: number;
clientY: number;
}
export interface IState {
size: number;
/** 是否已经按下鼠标 */
touched?: boolean;
/** 鼠标第一次按下位置 */
prevSize?: number;
}
/** 设置 style */
type SizeType = 'width' | 'height';
export interface UsePositionInitState {
/** 是否允许缩放 */
allowResize?: boolean;
sizeType?: SizeType;
onDragFinished?(size?: any): void;
onDragStarted?(): void;
}
NzSplit
class
import React, {
RefObject,
Fragment,
PureComponent,
} from "@alipay/bigfish/react";
import cs from "@alipay/bigfish/util/classnames";
import Spliter from "./Spliter";
import { IState, NzSplitProps } from "./interface";
import "./index.less";
import Pane from "./Pane";
import { getTouchEventObj } from "./helper";
const prefixCls = "nz-split";
class NzSplit extends PureComponent<NzSplitProps, IState> {
static defaultProps: NzSplitProps = {
allowResize: true,
};
firstRef: RefObject<HTMLDivElement>;
constructor(props: NzSplitProps) {
super(props);
this.state = {
size: -1,
prevSize: undefined,
};
this.firstRef = React.createRef<HTMLDivElement>();
}
componentDidMount() {
document.addEventListener("mouseup", this.onMouseUp);
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("touchmove", this.onTouchMove);
}
componentWillUnmount() {
document.removeEventListener("mouseup", this.onMouseUp);
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("touchmove", this.onTouchMove);
}
onMouseDown = (event: MouseEvent) => {
this.onTouchStart(getTouchEventObj(event));
};
onTouchStart = (event: TouchEvent) => {
if (this.props.allowResize) {
this.setState({
touched: true,
prevSize:
this.props.layout === "vertical"
? event.touches[0].clientX
: event.touches[0].clientY,
});
}
};
onMouseMove = (event: MouseEvent) => {
this.onTouchMove(getTouchEventObj(event));
};
onTouchMove = (event: TouchEvent) => {
if (this.props.allowResize && this.state.touched) {
const node = this.firstRef.current;
if (typeof node?.getBoundingClientRect === "function") {
const { prevSize } = this.state;
const currentSize =
this.props.layout === "vertical"
? event.touches[0].clientX
: event.touches[0].clientY;
const nodeSize = node?.getBoundingClientRect();
const size = this.props.layout === 'vertical' ? nodeSize.width : nodeSize.height;
const diffSize = currentSize - Number(prevSize);
this.setState((prevState) => ({
...prevState,
// size: size - Math.abs(diffSize),
size: currentSize,
}));
}
}
};
onMouseUp = (event: MouseEvent) => {
if (this.props.allowResize && this.state.touched) {
this.setState((prevState) => ({
...prevState,
touched: false,
}));
}
};
render() {
const {
layout,
style,
onResizerClick,
onResizerDoubleClick,
className,
} = this.props;
const kls = cs(
`${prefixCls}-container`,
className,
`${prefixCls}-${layout}`
);
console.log(this.state.size, "this.state>>>>>>>");
const renderChild = () => {
const childCount = React.Children.count(this.props.children);
if (childCount <= 1) {
return this.props.children;
}
if (childCount === 2) {
const [child1, child2] = this.props.children;
return (
<Fragment>
<Pane style={{ width: this.state.size }} ref={this.firstRef}>
{child1}
</Pane>
<Spliter
key="resizer"
// ref={domRef}
className={`nz-split-resize-${layout}`}
onMouseDown={(event: any) => {
this.onMouseDown(event);
}}
onTouchStart={(event) => {
event.stopPropagation();
this.onTouchStart(event as any);
}}
onDoubleClick={(event: any) => {
if (onResizerDoubleClick) {
event.stopPropagation();
onResizerDoubleClick(event);
}
}}
onTouchEnd={(event) => {
event.stopPropagation();
this.onMouseUp(event as any);
}}
onClick={(event: any) => {
if (onResizerClick) {
event.stopPropagation();
onResizerClick(event);
}
}}
/>
<Pane>{child2}</Pane>
</Fragment>
);
}
};
return (
<div className={kls} style={style}>
<Fragment>{renderChild()}</Fragment>
</div>
);
}
}
export default NzSplit;
function
import React, { FC, useMemo, Fragment } from "@alipay/bigfish/react";
import cs from "@alipay/bigfish/util/classnames";
import Spliter from "./Spliter";
import { NzSplitProps } from "./interface";
import "./index.less";
import Pane from "./Pane";
import usePosition from "./usePosition";
const prefixCls = "nz-split";
const NzSplit: FC<NzSplitProps> = (props) => {
const {
layout,
style,
className,
allowResize,
onResizerClick,
onDragFinished,
footer: propsFooter,
onResizerDoubleClick,
} = props;
const sizeType = layout === "vertical" ? "height" : "width";
const [
state,
{ domRef, firstRef, secondRef },
{ onMouseDown, onTouchStart, onMouseUp },
] = usePosition<HTMLSpanElement, HTMLDivElement>({
allowResize,
onDragFinished,
sizeType,
});
const kls = useMemo(() => {
return cs(`${prefixCls}-container`, className, `${prefixCls}-${layout}`);
}, [layout]);
const { size } = state;
const renderChild = () => {
const childCount = React.Children.count(props.children);
if (childCount <= 1) {
return props.children;
}
if (childCount === 2) {
const [child1, child2] = props.children;
return (
<Fragment>
<Pane
className={`nz-split-pane nz-split-pane-${layout}`}
style={{ [sizeType]: size }}
ref={firstRef}
>
{child1}
</Pane>
<Spliter
key="resizer"
ref={domRef}
className={`nz-split-resize-${layout}`}
// @ts-ignore
onMouseDown={onMouseDown}
onTouchStart={(event) => {
event.stopPropagation();
onTouchStart(event as any);
}}
onDoubleClick={(event: any) => {
if (onResizerDoubleClick) {
event.stopPropagation();
onResizerDoubleClick(event);
}
}}
onTouchEnd={(event) => {
event.stopPropagation();
onMouseUp(event as any);
}}
onClick={(event: any) => {
if (onResizerClick) {
event.stopPropagation();
onResizerClick(event);
}
}}
/>
<Pane
className={`nz-split-pane nz-split-pane-${layout}`}
ref={secondRef}
>
{child2}
</Pane>
</Fragment>
);
}
};
const footer = useMemo(() => {
if (!propsFooter) {
return null;
}
if (typeof propsFooter === "function") {
return propsFooter();
}
return propsFooter;
}, [propsFooter]);
return (
<div className={kls} style={style}>
<div className={`${prefixCls}-content`} style={props.bodyStyle}>
<Fragment>{renderChild()}</Fragment>
</div>
{footer && <div className={`${prefixCls}-footer`}>{footer}</div>}
</div>
);
};
NzSplit.defaultProps = {
layout: "horizontal",
allowResize: true,
footer: false
};
export default NzSplit;
less
@nz-split-prefix-cls: ~"nz-split";
@nz-split-resize-prefix-cls: ~"@{nz-split-prefix-cls}-resize";
@nz-split-content-prefix-cls: ~"@{nz-split-prefix-cls}-content";
@resize-size: 2px;
@resize-height-width: @resize-size * 2 + 1px;
.@{nz-split-prefix-cls} {
&-container {
position: relative;
display: flex;
flex: 1;
width: 100%;
height: 100%;
outline: none;
user-select: text;
.@{nz-split-resize-prefix-cls} {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0 -@resize-size;
background-color: red;
border: @resize-size transparent solid;
cursor: auto;
&-horizontal {
width: 100%;
height: @resize-height-width;
&:hover {
border-top-color: violet;
border-bottom-color: violet;
cursor: col-resize;
}
}
&-vertical {
width: 100%;
height: @resize-height-width;
&:hover {
border-right-color: violet;
border-left-color: violet;
cursor: row-resize;
}
}
}
}
&-content {
position: relative;
display: flex;
flex: 1;
width: 100%;
height: 100%;
background-color: blue;
}
&-horizontal,
&-horizontal.@{nz-split-content-prefix-cls} {
top: 0;
bottom: 0;
flex-direction: row;
// height: 100%;
// min-height: 100%;
// .@{nz-split-prefix-cls}-footer {
// // background-color: blue;
// // border-top: 1px red solid;
// }
}
&-vertical,
&-vertical .@{nz-split-content-prefix-cls} {
right: 0;
left: 0;
flex-direction: column;
}
&-pane {
position: relative;
flex: 0 0 auto;
overflow: hidden;
scroll-behavior: smooth;
}
&-footer {
padding: 8px 12px;
}
}
- usePosition ```typescript import { useSetState } from “@/hooks”; import { useCallback, useEffect, useRef } from “@alipay/bigfish/react”; import { getTouchEventObj } from “./helper”;
import { IState, UsePositionInitState } from “./interface”;
const usePosition =
(null);
const stateRef = useRef
const onTouchStart = useCallback( (evt: TouchEvent) => { if (allowResize) { stateRef.current = true; setState({ touched: true, prevSize: evt.touches[0][clientType], }); } }, [allowResize] );
const onMouseDown = (event: MouseEvent) => { onTouchStart(getTouchEventObj(event)); };
const onMouseUp = (evt: MouseEvent) => { if (allowResize) { stateRef.current = false; if (typeof onDragFinished === “function”) { onDragFinished(state.size); } setState({ touched: false, }); } };
/* 使用 setState 定义的 touched 无法作为判断条件(失效) / const onTouchMove = (evt: TouchEvent) => { if ( allowResize && stateRef.current && firstRef.current && firstRef.current.getBoundingClientRect ) { const nodeSize = firstRef.current.getBoundingClientRect(); const currentSize = evt.touches[0][clientType]; setState({ size: currentSize, }); } };
const onMouseMove = (evt: MouseEvent) => { onTouchMove(getTouchEventObj(evt)); };
useEffect(() => { document.addEventListener(“mouseup”, onMouseUp); document.addEventListener(“mousemove”, onMouseMove); document.addEventListener(“touchmove”, onTouchMove);
return () => {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("touchmove", onTouchMove);
};
}, []);
return [ state, { domRef, firstRef }, { onMouseDown, onMouseUp, onTouchStart, onMouseMove }, ] as const; };
export default usePosition;
参考
- Split.js: 同时也有 react-split 版本支持
- vue-split-pane
react-split-pane
event: 事件对象 event.clientX event.clientY