前端高级-前端拖拽技术

拖拽有两种实现方式:

  1. H5 Drag And Drop
  2. 鼠标拖放事件

其中,H5 不仅移动 html 元素,还移动数据对象。要移动元素,推荐使用 MouseEvents:

  • mousedown:选中元素
  • mousemove:移动元素
  • mouseup:释放元素

对 H5 DND 的误解

MouseEvent 方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 简单实现一个拖拽
function oDrag(handle) {
const el = document.querySelector(handle);
let _ox, _oy;
el.addEventListener('mousedown', handleMouseDown, false);
function handleMouseDown(e) {
// 记录初始偏移量,也可以
_ox = e.clientX - el.getBoundingClientRect().left; // 或 e.offsetX(兼容性不好)
_oy = e.clientY - el.getBoundingClientRect().top; // 或 e.offsetY
document.addEventListener('mousemove', handleMouseMove, false);
document.addEventListener('mouseup', handleMouseUp, false);
}
function handleMouseMove(e) {
const left = e.clientX - _ox;
const top = e.clientY - _oy;
el.style.left = left + 'px';
el.style.top = top + 'px';
}
function handleMouseUp(e) {
document.removeEventListener('mousemove', handleMouseMove, false);
document.removeEventListener('mouseup', handleMouseUp, false);
}
}

最佳实践见:鼠标拖放事件

H5 Drag&Drop API

开始拖拽

设置 draggable 属性,监听 dragstart 事件。

draggable 属性

所有 HTML 元素都可以设置 draggable 属性。

取值:true、false、auto(取用户代理默认值,一般情况下选中文本、图片、链接默认可拖拽)。

具有 draggable 属性的元素还应该具有 title 属性,该标题属性用于非视觉交互的目的。

拖拽数据

event.dataTransfer接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[Exposed=Window,Constructor] // 暴露给 window 对象,
interface DataTransfer {
/** dropEffect
返回当前选择的操作类型。如果该操作类型不是 effectAllowed 属性所允许的一种操作,则该操作将失败。
可以设置,以更改所选的操作。
可能的值为 “none”,“copy”,“link” 和 “move”(设置其他值将被忽略)。
*/
attribute DOMString dropEffect;
/** effectAllowed
返回允许的操作类型。
可以设置(在dragstart事件期间),以更改允许的操作。
可能的值为 “none”,“copy”,“copyLink”,“copyMove”,“link”,“linkMove”,“move”,“all” 和 “uninitialized”。
*/
attribute DOMString effectAllowed;
/** items
返回带有拖动数据的 DataTransferItemList 对象。
*/
[SameObject] readonly attribute DataTransferItemList items;
/** setDragImage
使用给定的元素更新拖动反馈,以替换任何先前指定的反馈。
*/
void setDragImage(Element image, long x, long y);

/* old interface */
/** types
返回一个冻结的数组,列出在 dragstart 事件中设置的格式。此外,如果要拖动任何文件,则类型之一将是字符串 “Files”。
*/
readonly attribute FrozenArray<DOMString> types;
/** getData
返回指定的数据。如果没有这样的数据,则返回空字符串。
只能在 drop 事件中获取。
*/
DOMString getData(DOMString format);
/** setData
添加指定的数据。
只能在 dragstart 事件中设置。
*/
void setData(DOMString format, DOMString data);
/** clearData
删除指定格式的数据。如果省略该参数,则删除所有数据。
*/
void clearData(optional DOMString format);
/** files
返回要拖动的文件的FileList(如果有)。
*/
[SameObject] readonly attribute FileList files;
};

拖拽事件

事件名 e.target 是否可取消 数据存储模式 dropEffect 默认行为
dragstart 源节点 可取消 read/write “none” 启动拖放操作
drag 源节点 可取消 protected “none” 继续拖放操作
dragenter 用户直接指定的可drop元素或body元素 可取消 protected 取决于 effectAllowed 拒绝用户直接指定的可drop元素作为潜在目标元素
dragexit 上一个目标元素 - protected “none” None
dragleave 上一个目标元素 - protected “none” None
dragover 当前目标节点 可取消 protected 取决于 effectAllowed 将当前拖动操作重置为”none”
drop 当前目标节点 可取消 read 当前拖动操作 不定
dragend 源节点 - protected 当前拖动操作 不定

说明:

  • 是否可取消:e.cancelable只读属性,如果事件不可取消,则其cancelable属性将为false,并且事件侦听器无法停止事件的发生;要取消事件,请对该事件调用preventDefault()方法;取消后事件相当于没发生一样。
  • DragEvent 对象,继承自 MouseEvent,只多了一个独有的 DataTransfer 属性。
  • dragenter 只在进入可 drop 元素时触发一次;dragover 只要没离开可 drop 元素,就会一直触发。
  • dragleave 与 dragenter 对应。
  • dragexit:当元素不再是拖动操作的立即选择目标时,将触发dragexit事件。(????)
  • 所有这些事件都会冒泡,并且 effectAllowed 属性始终具有 dragstart 事件之后的值,在 dragstart 事件中默认为“uninitialized”。

结束拖拽

当拖动一个项目到 HTML 元素中时,浏览器默认不会有任何响应。想要让一个元素变成可释放区域,该元素必须设置 dragover 和 drop 事件处理程序属性。且 dragover 监听时要调用 e.preventDefault()(注意每个处理程序调用 preventDefault() 来阻止对这个事件的其它处理过程)。

在网页中,如果监听了 drop 事件,应该调用事件的 preventDefault() 来阻止默认操作。例如,当将链接拖到网页上时,Firefox将打开该链接。通过取消事件,将防止此行为。

dragend 事件:源节点上触发。不管拖动是完成还是被取消这个事件都会被触发。

drop 事件:目标节点上触发。该元素必须设置 dragover 和 drop 事件处理程序属性,且阻止了 dragover 默认行为才会触发。

1
2
3
4
5
6
7
8
function dragOverHandler(e) {
e.preventDefault();
}

function dropHandler(e) {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
}

何时使用

如果你只需要移动 HTML 元素,请使用 MouseEvent 方式。

如果你想转移数据对象,请使用 HTML5 Drag & Drop。

一些注意的点

  1. 为了拖拽时不触发拖拽内容的事件或者能正确的结束拖拽操作(如拖拽内容是嵌入的 iframe 时,鼠标在里面释放触发不了 mouseup 事件),一般在拖拽开始时加一个全透明的拖拽覆盖层来屏蔽事件和冒泡结束事件,拖拽结束时隐藏该层。

  2. 在 firefox 上拖拽结束时会打开一个新的标签页,可做如下处理:

1
2
3
document.body.ondrop = function (e) {
e.preventDefault();
}

第三方拖拽库

https://github.com/SortableJS/Sortable

https://github.com/SortableJS/Vue.Draggable

参考