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 及其他配合属性进行设置
    1. .panel {
    2. flex-basic: 0;
    3. flex-shrink: 0;
    4. }

    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( (props, ref) => { const { className, style, …restProps } = props; const cls = useMemo(() => cs(“nz-split-resize”, className), []);

  1. return (
  2. <span ref={ref} className={cls} style={style} {...restProps}>
  3. {props.children && (
  4. <span className="nz-spliter-resize-content">{props.children}</span>
  5. )}
  6. </span>
  7. );

} );

export default Spliter;

  1. <a name="Zu58u"></a>
  2. #### Pane
  3. ```typescript
  4. import React, { useMemo } from "@alipay/bigfish/react";
  5. import { PanelProps } from "./interface";
  6. const Pane = React.forwardRef<HTMLDivElement, PanelProps>((props, ref) => {
  7. const { style, className } = props;
  8. const newStylie = useMemo(() => {
  9. return Object.assign({}, { flex: 1 }, style);
  10. }, [style]);
  11. return (
  12. <div ref={ref} style={newStylie} className={className}>
  13. {props.children}
  14. </div>
  15. );
  16. });
  17. Pane.displayName = "Pane";
  18. 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 = ( initialState: UsePositionInitState ) => { const { allowResize, onDragFinished, sizeType = “width” } = initialState; const clientType = sizeType === “width” ? “clientX” : “clientY”; const domRef = useRef(null); const firstRef = useRef

(null); const stateRef = useRef(false); const [state, setState] = useSetState({ size: -1, prevSize: -1, });

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

    • 使用时偶尔会因为 display: flex 导致子组件中的滚动条展示不正确

      知识点

  • event: 事件对象 event.clientX event.clientY