使用react实现封装拖拽

本文意在记录一次实现拖拽封装的过程,不保证文中存在纰漏,欢迎交流学习哦~

拖拽的基本实现

首先需要了解到,拖拽所使用到的相关事件:

PC端:onmousedown、 onmousemove、onmouseup
移动端:ontouchstart、ontouchmove、ontouchend

pc端和移动端的实现有部分差别,原理基本一致。以下先单独讲PC端的拖拽实现:

第一步:将onmousedown绑定到目标元素上,获取当前的偏移值
element.onmousedown = function (e) {
    e = e || window.e;
    // 偏移位置 = 元素的X - 元素的offset
    let disX = e.clientX - element.offsetLeft;
    let disY = e.clientY - element.offsetTop;
}

在元素上按下鼠标,可以查看到onmousedown的所有事件属性。拖拽需要使用其中的clientX(水平坐标)和clientY(垂直坐标)。

除此之外还需要获取目标元素的offsetLeft(距离左偏移的值)和offsetTop(距离顶部偏移的值),用于在每次onmousedown的时候,获取当前偏移位置,这是一个相对值,将在onmousemove中使用。

第二步:将onmousemove绑定到document上,在onmousedown中执行
document.onmousemove = function(e) { 
  e = e || window.e;
  // 元素位置 = 现在鼠标位置 - 元素偏移值
  let left  = e.clientX - disX; 
  let top = e.clientY -  disY;
}

在按下鼠标移动的时候,同样需要取到当前新的clientX和clientY,每当鼠标拖动到新的位置,就重新计算现在的鼠标位置 - 初始的元素偏移值,得元素新的位置坐标,这就拖拽的基本原理。

但现在的实现,存在拖动元素会超出当前的可视窗口的情况,因此需要在onmousemove中设置可拖动的范围。此外需要注意的是,onmousemove方法需要在onmousedown中使用,保证disX和disY不被销毁,onmousemove中能正常取到初始的偏移值。具体实现将在最后的完整版代码中展示。

第三步:结束拖拽
document.onmouseup = function(){
  document.onmousemove = null;
  document.onmouseup = null;
}

鼠标抬起时,元素不再需要跟随鼠标移动,要将onmousemove和onmouseup 事件清除,使用赋值为null的方式即可。如果使用addEventListener绑定事件,需要使用removeEventListener解绑事件。

移动端实现方式的区别

touch事件需都绑定在目标元素上,通过取得changedTouches[0]中的pageX和pageY计算新的坐标。其余的实现原理基本一致。

在react中封装拖拽

var isMoblie = 'ontouchstart' in window; // 是否为移动端
class Drag extends React.Component {
  constructor(props) {
    super(props);
    // props传递配置项
    this.elementWid = props.width || 100; 
    this.elementHeight = props.height || 100; 
    this.left = props.left || 0; 
    this.top =  props.top || 0; 
    this.zIndex = props.zIndex || 0;
    this.clientWidth = props.maxWidth;
    this.clientHeight = props.maxHeight;

    this._dragStart = this.dragStart.bind(this);
    this.state = {
      left: this.left,
      top: this.top
    };

  }

  dragStart(ev) {
    let target = ev.target; 
    if(isMoblie && ev.changedTouches) {
      this.startX = ev.changedTouches[0].pageX;
      this.startY = ev.changedTouches[0].pageY;
    } else {
      this.startX = ev.clientX;
      this.startY = ev.clientY;
    }

    // 偏移位置 = 鼠标的初始值 - 元素的offset
    this.disX = this.startX - target.offsetLeft;
    this.disY = this.startY - target.offsetTop;
    this.zIndex += 1;
    this._dragMove = this.dragMove.bind(this);
    this._dragEnd = this.dragEnd.bind(this);

    if(!isMoblie) {
      document.addEventListener('mousemove', this._dragMove, false);
      document.addEventListener('mouseup', this._dragEnd, false);
    } 

  }

  dragMove(ev) {
    if(isMoblie && ev.changedTouches) {
      this.clientX = ev.changedTouches[0].pageX;
      this.clientY = ev.changedTouches[0].pageY;
    } else {
      this.clientX = ev.clientX;
      this.clientY = ev.clientY;
    } 

    // 元素位置 = 现在鼠标位置 - 元素的偏移值
    let left = this.clientX - this.disX;
    let top = this.clientY - this.disY;

    // 处理不可超出规定拖拽范围
    if (left < 0) {
      left = 0;
    }

    if (top < 0) {
      top = 0;
    }

    if (left > this.clientWidth - this.elementWid) {
      left = this.clientWidth - this.elementWid;
    }

    if (top > this.clientHeight - this.elementHeight) {
      top = this.clientHeight - this.elementHeight;
    }

    this.setState({
      left: left,
      top: top
    });

  }

  dragEnd(e) {
    const {onDragEnd} = this.props;
    document.removeEventListener('mousemove', this._dragMove);
    document.removeEventListener('mouseup', this._dragEnd);
    onDragEnd && onDragEnd({
      X: this.startX - this.clientX,
      Y: this.startY - this.clientY
    })
  }

  render() {
    const { className, width, height} = this.props;
    const { left, top } = this.state;

    let styles = {
      width,
      height
    }

    // 根据组件配置,为元素添加对应的样式
    if(this.props.left) {
      styles['left'] = this.state.left;
    }

    if(this.props.top) {
      styles['top'] = this.state.top;
    }

    if (this.props.zIndex) {
      styles['zIndex'] = this.zIndex;
    }

    const cls = classnames('dragbox', {
      [className]: !!className
    })

    return (
      
this._dragMove(e)} onTouchEnd = {this._dragEnd} onMouseDown = {this._dragStart} onMouseUp = {this._dragEnd} style={styles} ref="dragElement" > {this.props.children}
) } } export default Drag;

Drag是一个类,在它的构造函数中存放着配置项,这些配置项只能被当前实例访问,每次实例化这个类,构造函数中的值都会被重新创建一次。因此,将基本配置项放在其中较为合理。

state中的值每次修改都会重新render虚拟DOM,而拖拽需要的是不断改变元素的位置,因此将位置信息left和top放在state中处理较为合理。由于state的特性,其他值尽量可以不放在state中处理,从而避免高频率渲染。

以上的封装基本实现了移动端和PC端拖拽的兼容。封装的组件配置项有:目标元素的宽度和高度、拖拽方向的选择left和top、目标元素的层级、目标元素的拖拽范围。如在运行过程中发现问题,还请斧正。

你可能感兴趣的:(使用react实现封装拖拽)