1. 拖放的基本概念

拖(drag)放(drop)是 HTML5 标准的组成部分,拖放是一种常见的特性,即抓取对象以后拖到另一个位置,任何元素都可以被拖放。
**

2. 拖放的几个事件

下面举一个简单的栗子来详细说明拖放的常用事件及其应用:
20200522150333.jpg
需求说明:如上图,这是一个常见的三栏布局,要求图中箭头部分可上下、左右拖动,使得三个box可以根据用户需要调整内容的长宽,从而扩大缩小展示区域。
特殊说明:以下代码片段均为react的伪代码,不保证正常运行哦。

第一步:骨架搭建

很明显的布局方式就是利用css3的Flex(弹性布局)来搭建,代码如下

  1. <style>
  2. .container{ width: 100%; height: 100vh; display: flex; flex-direction: row; }
  3. .panel-left{ width: 200px; position: relative; }
  4. .left-move-bar{ width: 6px; height: 100%; position: absolute; top: 0; right: -6px;
  5. background-color: '#eee'; cursor: 'ew-resize'; }
  6. .panel-right{ flex: 1; display: flex; flex-direction: column; }
  7. .panel-right-body{ flex: 1; }
  8. .panel-right-bottom{ position: relative; height: 400px }
  9. .bottom-move-bar{ width: 100%; height: 6px; position: absolute; top: 0; left: 0;
  10. background-color: '#eee'; cursor: 'ew-resize'; }
  11. </style>
  12. <div className="container">
  13. <div className="panel-left" style={{ width: this.state.leftWidth }}>
  14. <div className="left-move-bar"></div> // 可左右拖动bar
  15. </div>
  16. <div className="panel-right">
  17. <div className="panel-right-body"></div>
  18. <div className="panel-right-bottom" style={{ height: this.state.bottomHeight }}>
  19. <div className="bottom-move-bar"></div> // 可上下拖动bar
  20. </div>
  21. </div>
  22. </div>

第二步:处理被拖对象

设置被拖元素为可拖放
**
好了,我们的代码布局已经写好了,接下来就要一步步实现拖拽了;首先,为了使元素可拖动,把被拖对象的draggable 属性设置为 true:

  1. <div class="left-move-bar" draggable="true"></div>

被拖对象事件监听

  1. <div
  2. class="left-move-bar"
  3. draggable="true"
  4. onDragStart={handleLeftBarDragStart}>
  5. </div>
  6. <div
  7. class="bottom-move-bar"
  8. draggable="true"
  9. onDragStart={handleBottomBarDragStart}>
  10. </div>

handleDragStart方法如下,主要是获取被拖对象的鼠标位置

  1. handleLeftBarDragStart = (e) => {
  2. this.startX = e.clientX; // 获取鼠标起点位置
  3. e.dataTransfer.setData('target', 'x'); // 设置传输数据,用于区分拖拽的是横向bar还是竖向bar
  4. }
  5. handleBottomBarDragStart = (e) => {
  6. this.startY = e.clientY;
  7. e.dataTransfer.setData('target', 'y');
  8. }

ondragstart 表示被拖对象开始拖放时触发,全程只执行一次。

此外,还有2个不太常用的事件对象(存在兼容性问题,结尾有说明):
ondrag 表示被拖对象处于拖动过程中触发,全程一直触发,直到被成功放置。
ondragend 表示被拖对象被成功放置后触发,全程只执行一次

第三步:处理目标对象

允许放置
**
默认地,我们是无法将数据/元素放置到其他元素中。如果需要设置允许放置,我们必须阻止对元素的默认处理方式。这就要通过调用 ondragover 事件的 event.preventDefault() 方法:

  1. <div class="panel-left" onDragOver={e => e.preventDefault()}></div>
  2. <div class="panel-right" onDragOver={e => e.preventDefault()}></div>

ondragover 被拖对象接触到目标对象开始触发,只要不停止拖放动作,就会一直触发。

注意:因为我们拖动bar的可拖动范围是全屏幕,所以必须把左侧box和右侧box都要阻止默认事件

此外,还有2个过程对象的事件也可以了解一下:
ondragenter 被拖对象进入到目标对象触发,只执行一次。
ondragleave 被拖对象离开目标对象以后触发,全程一直执行,直到拖放结束。

进行放置
**
当放置被拖对象时,会发生 ondrop 事件,全程只执行一次。

  1. <div class="panel-left" onDragOver={e => e.preventDefault()} onDrop={handleOnDrop}></div>
  2. <div class="panel-right" onDragOver={e => e.preventDefault()} onDrop={handleOnDrop}></div>
  3. handleOnDrop = (e) => {
  4. const target = e.dataTransfer.getData('target');
  5. if (target === 'x') {
  6. this.setState({ // 计算左侧box的宽度
  7. leftWidth: this.state.leftWidth + (e.clientX - this.startX),
  8. });
  9. } else if (target === 'y') {
  10. this.setState({ // 计算底部box的高度
  11. bottomHeight: this.state.bottomHeight + (e.clientY - this.startY),
  12. });
  13. }
  14. }

经过上面简单的三个步骤,我们就基本实现了一个支持可拖拽的流式布局。在上面主要用到了拖拽的三个事件:

ondragstart 获取鼠标的起始位置
ondragover 阻止默认事件
ondrop 获取鼠标的终点位置,重新计算div的宽高

3. 总结说明

官方文档称 Internet Explorer 9、Firefox、Opera 12、Chrome 以及 Safari 5 均支持拖放。
注释:在 Safari 5.1.2 中不支持拖放。但在各个浏览器上仍然有不同的表现:

如何获取鼠标位置

在拖拽过程中,最重要的就是获取被拖对象、被拖对象的起始位置和放置位置,这些属性都可以通过 ondragstart 和 ondrop 来获取;而在某些业务场景中,我们是需要获取拖拽过程中鼠标的位置;但在Firefox中,ondrag、ondragover 的event对象中的clientX、clientY、offsetX、offsetY等值都是0,这个时候我们是可以通过监听document对象的dragover事件来实时获取鼠标所在的位置,代码示例如下:

  1. document.addEventListener(
  2. 'dragover',
  3. (e) => {
  4. e = e || window.event;
  5. const dragX = e.pageX;
  6. const dragY = e.pageY;
  7. console.log("X:" + dragX +"Y:" + dragY)
  8. },
  9. false
  10. )

拖拽元素为img对象

当你的拖放元素为img对象或者包含img对象时,在Firefox中总是会在新窗口中打开图片链接,而其他浏览器不会。解决办法是在ondrop事件中添加阻止事件冒泡的行为 event.stopPropagation()。