0%

说说html5拖拽

前言

在HTML5之前,可以通过Jquery UI来实现页面元素的拖拽功能。

html5规范已经添加了对页面元素拖拽功能的支持,目前主流的桌面端浏览器也都已经支持了。点击这个链接可以查看具体的情况。

本文将以一个例子详细讲解实现拖拽的全部细节。

第一步,需要明确拖拽的参与对象。

Step1:定义被拖拽物和容器

尽管用书面语描述非常拗口,但其实是非常容易理解的。

我们实现的例子就好比现在有两个盘子,其中一个盘子中有一个苹果,我们可以把苹果从一个盘子中放到另外一个盘子中。

这里,被移动位置的苹果就是被拖拽物,而盘子就是容器。

在现实世界中,苹果的可以移动和盘子可以容纳物体都是自然赋予的;而在Html5的世界中,我们需要定义这两者,一个完整的拖拽流程中我们需要做以下设置。以下的例子中id为item的元素为被拖拽物;id为container的元素为容器,我们用它们的id名称称呼它们。

定义被拖拽物

  • 在节点声明能够拖拽,设置draggable属性为true
  • 监听ondragstart事件(告诉大家我准备移民了)
  • 监听ondragend事件(移民办完了,把国内户口销了吧)

定义容器

  • 监听ondragenter事件(似乎有什么进入我的地盘了)
  • 监听ondragover事件(进来还瞎出溜,想移民得过我这关)
  • 监听ondragleave事件(进来看看然后走了,看来没诚意)
  • 监听ondrop事件(安置移民)

此时的代码html和js部分:

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
<div class="containers">
<div class="container" >
<div id="item" class="item" draggable='true'
ondragstart="dragstartHandler(event)"
ondragend="dragendHandler(event)"
>
drag me
</div>
</div>
<div class="container" id="container"
ondragenter="dragenterHandler(event)"
ondragleave="dragleaveHandler(event)"
ondragover="dragoverHandler(event)" ondrop="dragHandler(event)"
>
Try drag something to here
</div>
</div>

// 被拖拽对象处理拖拽开始事件
function dragstartHandler(event){

}
// 被拖拽对象处理拖拽结束事件
function dragendHandler(event){

}
// 容器处理被拖拽对象拖拽进入事件
function dragenterHandler(event){

}
// 容器处理被拖拽对象离开自身事件
function dragleaveHandler(event){

}
// 容器处理被拖拽对象在自身中移动事件
function dragoverHandler(event){

}
// 容器处理拖拽对象在自身中被放下事件
function dragHandler(event){

}

虽然以上我们做了这么多,但是非常遗憾,被拖拽对象还是无法拖动。

step2:item能够移动

为了达到目的,我们要修改dragstartHandler方法。修改后的方法如下。

1
2
3
function dragstartHandler(event){
event.dataTransfer.setData("text/plain",event.target.id);
}

修改后,我们发现item可以被拖动了!添加到这行代码做了什么?

简单说来,添加的这行代码向外发出了一个消息,消息的内容现在不必关心,这个消息发出去就item就可以被拖动了。

细心的你可能发现了,鼠标按键放下后,一切又恢复了原样。item仅仅是能够移动,但不能被放置在continer中。

step3:container能够容纳

这里我们修改dragoverHandler代码如下:

1
2
3
function dragoverHandler(event){
event.preventDefault();
}

然后再试一试,结果真是令人沮丧。如果你用Firefox浏览器,你的页面甚至都莫名其妙的发生了跳转。这里先不解释,我们先解决这个页面跳转的问题。我们修改dragHandler方法如下,作用是阻止浏览器的默认行为:

1
2
3
function dragHandler(event){
event.preventDefault();
}

这时Firefox和Chrome的表现一致了。下面的修改是关键:

1
2
3
4
5
6
function dragHandler(event){
event.preventDefault();
let dragItemId=event.dataTransfer.getData("text/plain");
let dragItem=document.getElementById(dragItemId);
event.target.appendChild(dragItem);
}

以上的修改内容中,我们得到了dragItemId,再通过它得到了一个DOM节点,最后把这个DOM节点添加到了当前元素中。没错dragItemId就是item的id,虽然这个例子中我们事先就知道这个id,但这里我们没有直接用而是通过:event.dataTransfer.getData(“text/plain”)获得了它。在step2中,我们同样操作了dataTransfer这个对象。可以把这个对象看作一个邮包,在dragstart事件监听中,我们往邮包里面存了一封信:当前拖拽对象的id;在这里我们把这封信拿了出来。

完善

到现在,我们实现了最初的目标,但是它并不完整。比如,在拖拽经过容器时,没有任何视觉提示可以在容器释放,拖拽过程中,被拖拽对象也没有变化等等。接着我们就逐一完善这些。

拖拽经过和离开容器的效果

1
2
3
4
5
6
7
function dragenterHandler(event){
event.target.classList.add("dragenter");
}

function dragleaveHandler(event){
event.target.classList.remove("dragenter");
}

以上两个方法设置了拖拽进入容器和离开容器时要做的事情。在进入容器后,我们给容器添加一个名字为dragenter的样式,对照样式表可以发现这个样式修改了容器边框的外观为虚线。在离开容器后,我们把这个样式删除了。加上这些效果后,被拖拽物经过容器,容器的边框变为虚线,离开后,容器边框恢复正常。

在item被释放后,容器边框应该复原,所以在dragHandler方法中,我们也添加移除dragenter样式的代码。

拖拽过程中item的效果

这里我希望item在被拖拽时变虚,拖拽结束后复原。我们需要修改dragstartHandlerdragendHandler方法,在拖拽开始的时候添加一个样式,在拖拽结束后移除这个样式。

1
2
3
4
5
6
7
8
function dragstartHandler(event){
event.dataTransfer.setData("text/plain",event.target.id);
event.target.classList.add("draging");
}

function dragendHandler(event){
event.target.classList.remove("draging");
}

到现在,一个简单的拖拽样例完成了。

扩展

被拖拽物和容器匹配

通过以上例子,能够确定dragover事件是这个问题的关键。容器不监听这个事件或者不阻止默认行为,拖拽的后续流程就被打断不能进行了。所以我们可以在这个方法中添加判断,对于和容器不匹配的被拖拽物,中断后续流程就好了。

现在,问题的关键又被转移到根据什么判断二者匹配。因为dragover事件是被容器监听的,所以在这个事件处理方法中,我们可以根据事件的target获得容器信息。被拖拽物体的信息怎么获得呢?

还记得前文提到的邮包吗?这时我们可以从那个邮包中拿到在dragstart事件中存入的信件啊,里面可以有匹配容器的相关信息啊。太棒了!

但,上面的设想太天真是错误的

你可以尝试getData一下,啥都得不到。你甚至怀疑这个邮包是不是亲手寄出去的那个。仔细一看,原来为了安全,dragstart时存入信件后,邮包上了一把大锁头,要一直到drop才能打开。为了安全,我还能说什么?难道这个问题就无解了吗?我不相信制定标准的那些大脑袋没有想到这个需求。在这里找到了一个答案。原来他们发现,虽然这个时候得到了邮包不能读信,但是可以透过邮包的透明盒子看到信封的内容。问题一下明朗了,在dragstart时多发一封信,用信封的信息表示匹配信息就行了。

比如我在dragstart时

1
dragEvent.dataTransfer.setData('typeA','');

在dragover时不读取’typeA‘对应的内容,而是检查有没有’typeA‘:

1
2
3
4
if (dragEvent.dataTransfer.types.includes('typeA')) {
...
dragEvent.preventDefault();
}

这样,不包含’typeA‘内容的被拖拽物就被过滤掉了。

DataTransfer

没错,就是那个邮包。这个邮包可以做的事儿很多,前面说的可以存放信件以及额外信息。另外还可以设置拖拽过程中的鼠标样式、设置拖拽物的代理(就是鼠标真实拖动的内容,默认是拖拽物的截图)等等。

如果存在嵌套关系的DOM元素,也都有以上的拖拽逻辑。那么要小心了。子元素的dropstart会冒泡到外层,你亲手存信件到邮包中,等到你的drop事件处理时,你会发现邮包不对啊,targe不对啊。解决的方式是在封好邮包前阻止事件冒泡。