初步尝试
在实现之初的想法很简略,先实现一个二分栏性能的组件,页面次要元素有三个:左分栏,右分栏,分割线,全副应用 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
增加相应的事件处理函数。
其中:
- onMouseDown: 记录用户的点击地位,同时,如果发现有
onMouseUp
未失常触发的状况下,调用相干处理函数handleMouseUp
。 - onMouseMove: 依据用户以后鼠标地位计算左右分栏的宽度,以及分割线的地位。
- onMouseUp: 清理数据。
另外 onMouseMove
和 onMouseUp
事件由外层的容器元素进行解决,次要是因为当用户鼠标滑动较快时,如果鼠标脱离了分割线元素,那么这两个事件就不会再持续触发了,因为分割线很窄,只有几个像素宽,所以这种状况是极有可能产生的,因而须要将这两个事件晋升到父级容器来解决。
// 记录点击开始地位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 之间),这些限度以及不同分栏之间宽度可能存在的联动关系能够依照本人的需要去实现。
以及须要思考在性能上,目前这样的实现是否满足要求。