关于前端:实现一个可拖拽分栏组件

34次阅读

共计 5378 个字符,预计需要花费 14 分钟才能阅读完成。

初步尝试

在实现之初的想法很简略,先实现一个二分栏性能的组件,页面次要元素有三个:左分栏,右分栏,分割线,全副应用 absolute 定位。

实现款式预览

import {FC, useState} from 'react';
import styles from './index.module.scss';
import cn from 'classnames';

const ResizableCol: FC = () => {const [width, setWidth] = useState(100);

  return (<div className={styles.container}>
      {/** 左分栏 */}
      <div className={cn(styles.leftside, styles.block)} style={{width: `${width}px` }}></div>
      {/** 右分栏 */}
      <div className={cn(styles.rightside, styles.block)} style={{left: `${width}px` }}></div>
      {/** 分割线 */}
      <div
        className={styles.divider}
        style={{left: `${width}px` }}
      />
    </div>
  );
};

export default ResizableCol;
.container {
  position: relative;
  height: 100%;
}

.divider {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  height: 100%;
  background-color: #000;
  cursor: col-resize;
  z-index: 1;
}

.block {
  position: absolute;
  top: 0;
  bottom: 0;
}

.leftside {
  left: 0;
  background-color: #ddd;
}

.rightside {
  right: 0;
  background-color: #bbb;
}

增加交互、优化款式

在实现款式后,为组件补充相干交互代码,次要是为 onMouseDown / onMouseMove / onMouseUp 增加相应的事件处理函数。

其中:

  1. onMouseDown: 记录用户的点击地位,同时,如果发现有 onMouseUp 未失常触发的状况下,调用相干处理函数 handleMouseUp
  2. onMouseMove: 依据用户以后鼠标地位计算左右分栏的宽度,以及分割线的地位。
  3. onMouseUp: 清理数据。

另外 onMouseMoveonMouseUp 事件由外层的容器元素进行解决,次要是因为当用户鼠标滑动较快时,如果鼠标脱离了分割线元素,那么这两个事件就不会再持续触发了,因为分割线很窄,只有几个像素宽,所以这种状况是极有可能产生的,因而须要将这两个事件晋升到父级容器来解决。

// 记录点击开始地位
const startXRef = useRef<number | null>(null)
// 记录左分栏的宽度
const [width, setWidth] = useState(100);
// 当分割线开始挪动时,记录此时的左分栏宽度
const oldWidthRef = useRef(100);

// onMouseDown 处理函数
const handleMouseDown = useCallback((e: React.MouseEvent) => {if (e.button === 0) {if (startXRef.current !== null) {handleMouseUp(e);
    }
    startXRef.current = e.clientX;
  }
}, []);

// onMouseMove 处理函数
const handleMouseMove = useCallback((e: React.MouseEvent) => {if (startXRef.current === null) {return;}
  setWidth(e.clientX - startXRef.current + oldWidthRef.current);
}, [])

// onMouseUp 处理函数
const handleMouseUp = useCallback((e: React.MouseEvent) => {if (e.button === 0) {
    startXRef.current = null;
    oldWidthRef.current = width;
  }
}, [width])

return (<div className={styles.container} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} style={{'cursor': startXRef.current !== null ? 'col-resize' : 'default'}}>
    <div className={cn(styles.leftside, styles.block)} style={{width: `${width}px` }}></div>
    <div className={cn(styles.rightside, styles.block)} style={{left: `${width}px` }}></div>
    <div
      className={styles.divider}
      style={{left: `${width - 3}px` }}
      onMouseDown={handleMouseDown}
      draggable={false} />
  </div>
);

同时,如果分割线元素太窄(例如 1 个像素),用户很难选中分割线,因而将其宽度批改为 7 像素大小。

.divider {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  padding: 0 3px;
  height: 100%;
  cursor: col-resize;
  z-index: 1;

  &:after {
    display: inline-block;
    content: '';
    position: absolute;
    left: 3px;
    top: 0;
    bottom: 0;
    width: 1px;
    background-color: #000;
    z-index: -1;
  }
}

实现多分栏

将组件从二分栏拓展到三分栏、四分栏,在实现思路上和二分栏没有什么区别,同样是响应用户的交互后,去更新多个分栏的宽度。

同时这次,将不同分栏的内容改为由父组件传递的模式,因而 ResizableCol 当初能够承受以下的 props

export interface Props {
  /** 不同分栏的内容 */
  content: JSX.Element[];
  /** 不同分栏的默认宽度 */
  defaultWidth?: number[];}

因为逻辑上并没有大的不同,所以就间接贴 ResizableCol 的代码了

import React, {FC, useState, useCallback, useRef} from 'react';
import styles from './index.module.scss';
import {Props} from './type';

const DefaultWidth = 100;

function isValidWidth(width: number) {return width > 0;}

function cumsum(arr: number[], start: number, end?: number) {
  let result = 0;
  for (let i = start, j = end == null ? arr.length : end; i < j; i++) {result += arr[i];
  }
  return result;

}

function isUndef(val: any): val is (null | undefined) {return val === null || val === undefined;}

/**
 * 可多分栏
 */
const ResizableCol: FC<Props> = props => {const { content, defaultWidth} = props;
  const colCount = content.length;

  const validDefaultWidth = (defaultWidth || []).map(width => isValidWidth(width) ? width : DefaultWidth);
  for (let i = validDefaultWidth.length; i < colCount - 1; i++) {validDefaultWidth.push(DefaultWidth)
  }

  const indexRef = useRef<number | null>(null);
  const [widthList, setWidthList] = useState(validDefaultWidth);
  const oldWidthRef = useRef(validDefaultWidth);
  const startClientXRef = useRef<number | null>(null)

  const handleMouseDown = useCallback((e: React.MouseEvent) => {if (e.button === 0) {if (startClientXRef.current !== null) {handleMouseUp(e);
      }

      startClientXRef.current = e.clientX;

      // 记录 index
      const dividerEl = e.target as HTMLDivElement;
      const indexStr = dividerEl.dataset.index;
      if (!indexStr) {return;}

      const indexNum = Number(indexStr);
      if (isNaN(indexNum)) {return;}
      indexRef.current = indexNum;
    }
  }, []);

  const handleMouseMove = useCallback((e: React.MouseEvent) => {if (startClientXRef.current === null) {return;}
    const indexNum = indexRef.current;
    if (isUndef(indexNum)) {return;}

    setWidthList(widthList => {let newWidth = e.clientX - startClientXRef.current! + oldWidthRef.current[indexNum];
      newWidth = Math.max(Math.min(newWidth, 200), 100);

      if (newWidth === widthList[indexNum]) {return widthList;}

      const newList = [...widthList];
      newList[indexNum] = newWidth;
      return newList;
    });
  }, []);

  const handleMouseUp = useCallback((e: React.MouseEvent) => {if (e.button === 0) {
      startClientXRef.current = null;
      oldWidthRef.current = widthList;
    }
  }, [widthList])

  return (<div className={styles.container} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} style={{'cursor': startClientXRef.current !== null ? 'col-resize' : 'default'}}>
      {content.map((col, index) => {const left = cumsum(widthList, 0, index);
          const width = widthList[index];

          return (
            <>
              <div
                className={styles.block}
                style={{left: `${left}px`,
                  width: index === colCount - 1 ? 'auto' : `${width}px`,
                  right: index === colCount - 1 ? '0px' : 'auto'
                }}
              >
                {col}
              </div>

              {
                index !== colCount - 1 ? (
                  <div
                    data-index={index}
                    className={styles.divider}
                    style={{left: `${left + width - 3}px` }}
                    onMouseDown={handleMouseDown}
                    draggable={false} />
                ) : null
              }
            </>
          );
        })
      }
    </div>
  );
};

export default ResizableCol;

后续

在多分的根底上,仍旧须要补充一些组件交互上的限度,例如对于不同分栏的宽度限度(下面代码中将分栏的宽度限度在 100px 到 200px 之间),这些限度以及不同分栏之间宽度可能存在的联动关系能够依照本人的需要去实现。

以及须要思考在性能上,目前这样的实现是否满足要求。

正文完
 0