使用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 (
      <div
        className = {cls}
        onTouchStart = {this._dragStart}
        onTouchMove = {(e)=>this._dragMove(e)}
        onTouchEnd = {this._dragEnd}
        onMouseDown = {this._dragStart}
        onMouseUp = {this._dragEnd}
        style={styles}
        ref="dragElement"
      >
        {this.props.children}
      </div>
    )
  }
}

export default Drag;

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352