Vue中实现拖放

最近在学习Vue,边做个小demo边学习。其中有一个小功能需要使用到拖放,顺便还学一下拖放。拖放是HTML5的标准,对着教程在普通的页面上很容易就实现了,但是vue中基本都是数据驱动,不推荐直接操作DOM。

HTML5拖放

可拖动

首先,默认情况下,图像、链接和文本是可拖动的。而想让其他元素变为可拖动,需要设置draggable属性为true

1
<div draggable="true"></div>

拖放事件

拖放事件有的是在被拖动元素上触发的,而有的则是在放置目标上触发。当拖动某个元素时,将依次触发以下事件:

  1. dragstart: 被拖动元素开始被拖动是触发
  2. drag: 被拖动元素被拖动期间持续触发,类似于mousemove
  3. dragend: 停止拖动元素时触发,无论被拖动元素是否放置在有效的目标上

当某个元素被拖动放在一个目标元素上时,放置目标则依次放生以下事件:

  1. dragenter: 当拖动某个元素进入放置元素时触发
  2. dragoverdragenter触发后,会触发dragover事件,并且如果拖动元素继续在放置目标范围内移动,该事件会持续触发
  3. drop或dragleave: 在dragover事件后,如果是松开鼠标,被拖动元素放到放置目标上,触发drop事件;如果是继续拖动被拖动元素继续移动并离开了放置元素,这个放置目标元素就触发dragleave事件;

拖动元素经过各个元素时,这些元素虽然支持放置元素的事件,但是默认是不允许放置的,因此也就不会触发drop事件。需要在允许放置的元素中重写dragenterdragover事件的默认行为,即使用Event.preventDefault()方法。

有些浏览器drop事件默认行为打开放到放置目标的URL。如拖放一个图片,就直接转到图片文件。因此也需要取消drop事件的默认行为。

dataTransfer对象

在整个拖放过程中,需要进行数据交换的话,可以使用dataTransfer对象。dataTransfer对象作为event对象的属性,拥有两个主要方法setData()getData()分别设置数据和获取数据。语法

1
2
void dataTransfer.setData(format, data);
void dataTransfer.getData(format);

format是字符串类型,表示数据类型的;data也是字符串类型,即要保存的值。

dataTransfer对象的两个属性dropEffecteffectAllowed,分别设置被拖动元素的放置效果和指定当元素被拖放时允许的效果。
dataTransfer对象还有个files属性包含拖动到浏览器窗口的文件列表。可以用这个借口实现文件拖动上传。

简单的拖放例子可参考w3school的HTML5拖放

Vue拖放排序

小项目tomatodos中,todo列表我想加上一个拖放排序功能。就是todo列表中,允许用户直接拖动某一项,插入到另一项后。Vue中都是数据驱动的,因此在拖放后应该改变的是数据顺序,而不是直接操作dom。因此很自然想到的还是在drop后改变数据的顺序,拖动的元素则可以在dragstart中获取。我们的数据是

1
lists: ['1: apple', '2: banana', '3: orange', '4: melon']

在html使用v-for渲染,使用transition-group组件加入动画效果

1
2
3
4
5
6
7
<transition-group id='app' name="drog" tag="ul">
<li draggable="true" v-for="(item, index) in lists"
@dragstart="dragStart($event, index)" @dragover="allowDrop" @drop="drop($event, index)"
v-bind:key="item">
{{item}}
</li>
</transition-group>

每个li需要给定一个唯一的key,这样才能很好的使用过渡效果

索引在dragstart事件中传入,可以使用dataTransfer保存

1
2
3
4
//开始拖动
dragStart(e, index){
e.dataTransfer.setData('Text', index);
}

放置元素触发drop事件,在drop事件中,我们同时拥有放置目标元素的索引index,以及被拖动元素的索引dragIndex。比较简单的做法是使用一个新数组记录整个过程,最后把新数组赋给原数据变量。使用splice删除和插入,效果就是从前面拖到后面是插入放置元素后面,而从后面往前拖放是插入到放置元素前面。这样不需要再判断方向,也能得到比较好的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//放置
drop(e, index){
//取消默认行为
this.allowDrop(e);
//使用一个新数组重新排序后赋给原变量
let arr = this.lists.concat([]),
dragIndex = e.dataTransfer.getData('Text');
temp = arr.splice(dragIndex, 1);
arr.splice(index, 0, temp[0]);
this.lists = arr;
}

完整demo