html5 drag and drop events
html5中的拖放事件有以下几个:
- dragstart
- drag
- dragleave
- dragenter
- dragover
- drop
- dragend
我们将从以下几个方面依次讲解这些事件,包括事件的触发条件,事件的触发源头,事件的触发频率等
dragstart
触发条件:当用户开始拖动一个元素时,会触发dragstart
事件。
触发源头:被拖动的元素。
触发频率:一次。
drag
触发条件:当用户拖动一个元素时,会触发drag
事件。
触发源头:被拖动的元素。
触发频率:多次。
dragleave
触发条件:当用户拖动一个元素,离开了一个可放置的目标元素时,会触发dragleave
事件。
触发源头:目标区域(松开鼠标后,元素放置的区域)。
触发频率:一次。
dragenter
触发条件:当用户拖动一个元素,进入了一个可放置的目标元素时,会触发dragenter
事件。
触发源头:目标区域(松开鼠标后,元素放置的区域)。
触发频率:一次。
dragover
触发条件:当用户拖动一个元素,悬停在一个可放置的目标元素上时,会触发dragover
事件。
触发源头:目标区域(松开鼠标后,元素放置的区域)。
触发频率:多次。
drop
触发条件:当用户拖动一个元素,释放鼠标按钮时,会触发drop
事件。
触发源头:目标区域(松开鼠标后,元素放置的区域)。
触发频率:一次。
dragend
触发条件:当用户拖动一个元素,释放鼠标按钮时,会触发dragend
事件。
触发源头:被拖动的元素。
触发频率:一次。
实例讲解
下面以一个实际例子来讲解事件发生的顺序,如下图,页面上有两个容器,Container A和Container B,container A中有一个元素Child,container B中没有元素。我们将container A中的Child元素拖动到container B中,观察事件的触发顺序。
1 |
|
- 首先触发的事件是 dragstart,触发源头是被拖动的元素,触发频率是一次。我们可以通过打印
event.target
来验证这一点。
1 | function dragStart(e) { |
事件处理函数中的e.target
是指触发该事件的元素,比如当用户点击某一个按钮时会触发click
事件,那么按钮就是click事件的e.target。而this
是指注册事件处理函数的元素,大部分情况下这二者是相同的,但是也有例外,详情请看这里。
dragstart
不同于其他拖拽处理函数的地方是,它是唯一可以设置transfer data的地方。我们可以通过e.dataTransfer.setData
方法来设置transfer data,该方法接受两个参数,第一个参数是transfer data的类型,第二个参数是transfer data的值。这里我们设置transfer data的类型为text
,值为被拖动元素的id。
然后触发的事件是
drag
,触发源头是被拖动的元素,该事件会在拖动过程中一直触发。在实际应用中,一般不监听这个事件,因为它没啥用,而且触发太频繁。接下来触发的事件是
dragenter
,当用户拖动一个元素到一个可放置的目标时,会触发该事件,只会触发一次。由于Container A也是一个可放置的区域,而刚开始拖动时Child就已经位于Container A中,所以会触发dragenter
事件。然后触发的事件是
dragover
,当用户拖动一个元素,悬停在一个可放置的目标元素上时,会触发该事件,会在拖动过程中一直触发。与其他事件处理函数不同,该函数有如下两个特点。- 该事件在放置目标区域上触发,而不是被拖拽的元素上触发。
- 这个事件是需要阻止默认行为的,否则会导致
drop
事件无法触发。我们可以通过e.preventDefault()
来阻止默认行为。
接下来,如果用户在目标区域释放了鼠标,那么会触发
drop
事件,该事件会在放置目标区域上触发,只会触发一次。我们可以通过e.dataTransfer.getData
来获取transfer data,该方法接受一个参数,表示要获取的transfer data的类型,这里我们传入text
,表示获取类型为text
的transfer data。如果我们在dragstart
事件处理函数中设置了transfer data,那么在drop
事件处理函数中就可以获取到transfer data了。如果用户在目标区没有释放鼠标,而是继续拖动,直到离开目标区域,那么会触发
dragleave
事件,该事件会在放置目标区域上触发,只会触发一次。反之,如果用户在目标区域释放了鼠标,那么该事件不会触发。也就是说,对于同一个目标区域而言,dragleave
事件和drop
事件是互斥的,只会触发其中一个。
注意:dragenter
, dragleave
, dragover
- 这里都是指鼠标的进入,离开,悬停,而不是指被拖动的元素的进入,离开,悬停。
总结如下:
事件 | 触发条件 | 触发源头 | 触发频率 |
---|---|---|---|
dragstart | 当用户开始拖动一个元素时 | 被拖动的元素 | 一次 |
drag | 当用户拖动一个元素时 | 被拖动的元素 | 多次 |
dragleave | 当用户拖动一个元素,离开了一个可放置的目标元素时 | 目标区域对应的元素 | 一次 |
dragenter | 当用户拖动一个元素,进入了一个可放置的目标元素时 | 目标区域对应的元素 | 一次 |
dragover | 当用户拖动一个元素,悬停在一个可放置的目标元素上时 | 目标区域对应的元素 | 多次 |
drop | 当用户拖动一个元素,释放鼠标按钮时 | 目标区域对应的元素 | 一次 |
dragend | 当用户拖动一个元素,释放鼠标按钮时 | 被拖动的元素 | 一次 |
注意事项:
dragenter
,dragleave
,dragover
- 这里都是指鼠标的进入,离开,悬停,而不是指被拖动的元素的进入,离开,悬停。在处理从一个区域拖拽到另一个区域的情况时比较简单,比如常见的左右两个列表,将一个元素从左侧列表拖拽到右侧列表。这时候被拖拽的元素和放置目标之间的界限比较明显,处理起来比较容易,也就是说被拖拽的元素响应以下事件即可:
- dragstart
- drag
- dragend
而放置目标响应以下事件即可:
- dragenter
- dragover
- drop
- dragleave
但是如果处理一组元素之间的互相拖拽及排序,那就比较麻烦,比如一个相册列表,或者一个瀑布流布局,或者masonry布局,就是一个大容器,里面有如果子元素,我们可以拖拽任意一个元素到其他位置,在拖拽过程中要有placeholder,其他元素要给被拖拽的元素让路。这种情况下,每个元素都要响应所有事件,所以一个必要的操作就是判断被拖拽的元素和放置目标是否是同一个元素,如果是同一个元素,那么就不需要做任何处理,否则就需要做相应的处理。这里有一个技巧,可以使用
e.target
和e.currentTarget
来判断被拖拽的元素和放置目标是否是同一个元素,常见的做法如下:
- 在
dragstart
函数中记录被拖拽的元素1
2
3
4
5let dragSrcEl = null;
function handleDragStart(e) {
dragSrcEl = this; // record the dragged element
dragSrcEl.classList.add("dragging");
} - 在
dragenter
,dragover
,dragleave
,drop
等方法中判断被拖拽元素是否等于this,如果是,那么说明是同一个元素,直接return,如果不是,再进行具体的处理工作。1
2
3
4
5
6
7
8function handleDragEnter(e) {
// this event also triggered on the dragged item itself, we should ignore this case.
if (dragSrcEl === this) {
return;
}
// do something...
}
在
dragover
事件处理函数中,需要阻止默认行为,否则会导致drop
事件无法触发。我们可以通过e.preventDefault()
来阻止默认行为。1
2
3function handleDragOver(e) {
e.preventDefault(); // prevent default behavior
}在
drop
事件处理函数中,需要阻止默认行为,否则会导致浏览器打开被拖拽的元素。我们可以通过e.preventDefault()
来阻止默认行为。1
2
3function handleDrop(e) {
e.preventDefault(); // prevent default behavior
}