React组件拖拽模板(解决高度定制化需求)

148 阅读3分钟

故事背景

  • 手写一个元素拖拽的模板
  • 作用是在子元素中为其某个父元素加上拖拽功能
  • 之所以这么做是因为业务代码嵌套层数太深无法重构
  • 文中的目标元素XDom<EnableDrag />的关系是父子关系

先看代码

import React from 'react';  
import { injectIntl } from "react-intl";  
import './EnableDrag.less';  
interface IProps {  
  // 父组件控制此组件的可见性  
  visible: boolean;  
  intl: any;  
}  
  
interface IStates {  
  // 组件可见性  
  visible: boolean;  
  // 添加拖拽的目标dom的引用变量  
  XDom: HTMLDivElement;  
  // 鼠标是否落在XDom及其子元素上  
  isMouseDown: boolean;  
  // 鼠标最近一次落下的时候的位置  
  clientX: number;  
  clientY: number;  
  // XDom最新的left和top值  
  lastDownX: number;  
  lastDownY: number;  
}  
  
class EnableDrag extends React.Component<IProps, IStates> {  
  state = {  
    visible: false,  
    XDom: null,  
    isMouseDown: false,  
    clientX: 0,  
    clientY: 0,  
    lastDownX: 0,  
    lastDownY: 0,  
  }  
  constructor(props: IProps) {  
    super(props);  
  }  
  
  // 组件构建完毕之后找到目标元素然后将其引用存储在state的XDom变量中  
  componentDidMount(): void {  
    const XDom = document.querySelector('.target-draggable-dom') as HTMLDivElement;  
    this.setState({  
      XDom,  
    })  
    // 如果找到了目标元素那么就在window上绑定鼠标处理事件以完成拖拽功能  
    if (XDom) {  
      window.addEventListener('mousedown', this.handleMouseDown);  
      window.addEventListener('mousemove', this.handleMouseMove);  
      window.addEventListener('mouseup', this.handleMouseUp);  
      // 如果鼠标到窗口外面则视为抬起  
      window.addEventListener('mouseleave', this.handleMouseUp);  
    }  
  }  
  
  // 组件卸载之前清除事件监听  
  componentWillUnmount(): void {  
    window.removeEventListener('mousedown', this.handleMouseDown);  
    window.removeEventListener('mousemove', this.handleMouseMove);  
    window.removeEventListener('mouseup', this.handleMouseUp);  
    window.removeEventListener('mouseleave', this.handleMouseUp);  
  }  
  
  // window监听了mousedown鼠标事件,当鼠标落下之后  
  // 首先判断落点是否是在XDom上  
  // 然后再做一些细致的判断:例如落点是在input框中,这个时候就不要有移动功能  
  handleMouseDown = (e) => {  
    if(!this.state.XDom.contains(e?.target)) return;  
    if(e?.target?.nodeName === 'INPUT') return;  
    e.stopPropagation();  
    if (!this.state.XDom) return;  
    // 得到鼠标落点位置  
    const { clientX, clientY } = e;  
    // 得到目标元素style上的left值和top值  
    const { left, top } = getComputedStyle(this.state.XDom);  
    const lastDownX = parseFloat(left);  
    const lastDownY = parseFloat(top);  
    // 更新位置信息  
    this.setState({  
      isMouseDown: true,  
      clientX,  
      clientY,  
      lastDownX,  
      lastDownY,  
    })  
  }  
  
  // 首先判断鼠标是否真的在目标元素上落下  
  // 然后根据初始位置和位移计算当前位置  
  // 使用transform移动元素,而不是随时修改style  
  handleMouseMove = (e) => {  
    e.stopPropagation();  
    // 如果鼠标没有真正落下就不再执行  
    if (!this.state.isMouseDown) return;  
    if (!this.state.XDom) return;  
    // 拿到当前鼠标落点  
    const { clientX, clientY } = e;  
    // 拿到目标元素的left和top值  
    const { clientX: lastX, clientY: lastY } = this.state;  
    // 计算位移值  
    const delX = clientX - lastX;  
    const delY = clientY - lastY;  
    // 使用transform更新位置  
    this.state.XDom.style.transform = `translate(${delX}px, ${delY}px)`;  
  }  
  
  // 当鼠标出界或者抬起之后需要将transform样式转换成style的left和top  
  handleMouseUp = (e) => {  
    e.stopPropagation();  
    if (!this.state.isMouseDown) return;  
    // 拿到此时鼠标位置  
    const { clientX, clientY } = e;  
    // 取出状态信息  
    const { clientX: lastX, clientY: lastY, lastDownX, lastDownY } = this.state;  
    // 计算鼠标抬起和落下的位移  
    const delX = clientX - lastX;  
    const delY = clientY - lastY;  
    // 计算新的left和top的值  
    const newPositionX = lastDownX + delX;  
    const newPositionY = lastDownY + delY;  
    // 更新样式,并且将transform:translate复原(这里直接复原到0 0了)  
    this.state.XDom.style.left = `${newPositionX}px`;  
    this.state.XDom.style.top = `${newPositionY}px`;  
    this.state.XDom.style.transform = `translate(${0}px, ${0}px)`;  
    // 修改isMouseDown的值表示鼠标抬起  
    this.setState({  
      isMouseDown: false,  
    })  
  }  
  
  render(): React.ReactNode {  
    const { visible } = this.props;  
    return (  
      <div  
        style={{ display: `${visible ? 'block' : 'none'}` }}  
        className='enableDrag'  
      >  
        <div  
          className='content-area'  
          style={{  
            position: 'relative',  
          }}  
        >  
        </div>  
      </div>  
    )  
  }  
}  
  
export default injectIntl(EnableDrag);

亮点总结

1. 使用contains判断鼠标落点是否在目标元素及其子元素上

见:判断鼠标落点是否在目标DOM元素上的方法

2. 使用transform:translate和style:left/top搭配使拖拽更加丝滑

见:handleMouseMovehandleMouseUp方法的具体内容