前言
在HTML5之前,可以通过Jquery UI来实现页面元素的拖拽功能。
html5规范已经添加了对页面元素拖拽功能的支持,目前主流的桌面端浏览器也都已经支持了。点击这个链接可以查看具体的情况。
本文将以一个例子详细讲解实现拖拽的全部细节。
第一步,需要明确拖拽的参与对象。
Step1:定义被拖拽物和容器
尽管用书面语描述非常拗口,但其实是非常容易理解的。
我们实现的例子就好比现在有两个盘子,其中一个盘子中有一个苹果,我们可以把苹果从一个盘子中放到另外一个盘子中。
这里,被移动位置的苹果就是被拖拽物,而盘子就是容器。
在现实世界中,苹果的可以移动和盘子可以容纳物体都是自然赋予的;而在Html5的世界中,我们需要定义这两者,一个完整的拖拽流程中我们需要做以下设置。以下的例子中id为item的元素为被拖拽物;id为container的元素为容器,我们用它们的id名称称呼它们。
定义被拖拽物
- 在节点声明能够拖拽,设置draggable属性为true
- 监听ondragstart事件(告诉大家我准备移民了)
- 监听ondragend事件(移民办完了,把国内户口销了吧)
定义容器
- 监听ondragenter事件(似乎有什么进入我的地盘了)
- 监听ondragover事件(进来还瞎出溜,想移民得过我这关)
- 监听ondragleave事件(进来看看然后走了,看来没诚意)
- 监听ondrop事件(安置移民)
此时的代码html和js部分:
1 | <div class="containers"> |
虽然以上我们做了这么多,但是非常遗憾,被拖拽对象还是无法拖动。
step2:item能够移动
为了达到目的,我们要修改dragstartHandler方法。修改后的方法如下。
1 | function dragstartHandler(event){ |
修改后,我们发现item可以被拖动了!添加到这行代码做了什么?
简单说来,添加的这行代码向外发出了一个消息,消息的内容现在不必关心,这个消息发出去就item就可以被拖动了。
细心的你可能发现了,鼠标按键放下后,一切又恢复了原样。item仅仅是能够移动,但不能被放置在continer中。
step3:container能够容纳
这里我们修改dragoverHandler代码如下:
1 | function dragoverHandler(event){ |
然后再试一试,结果真是令人沮丧。如果你用Firefox浏览器,你的页面甚至都莫名其妙的发生了跳转。这里先不解释,我们先解决这个页面跳转的问题。我们修改dragHandler方法如下,作用是阻止浏览器的默认行为:
1 | function dragHandler(event){ |
这时Firefox和Chrome的表现一致了。下面的修改是关键:
1 | function dragHandler(event){ |
以上的修改内容中,我们得到了dragItemId,再通过它得到了一个DOM节点,最后把这个DOM节点添加到了当前元素中。没错dragItemId就是item的id,虽然这个例子中我们事先就知道这个id,但这里我们没有直接用而是通过:event.dataTransfer.getData(“text/plain”)获得了它。在step2中,我们同样操作了dataTransfer这个对象。可以把这个对象看作一个邮包,在dragstart事件监听中,我们往邮包里面存了一封信:当前拖拽对象的id;在这里我们把这封信拿了出来。
完善
到现在,我们实现了最初的目标,但是它并不完整。比如,在拖拽经过容器时,没有任何视觉提示可以在容器释放,拖拽过程中,被拖拽对象也没有变化等等。接着我们就逐一完善这些。
拖拽经过和离开容器的效果
1 | function dragenterHandler(event){ |
以上两个方法设置了拖拽进入容器和离开容器时要做的事情。在进入容器后,我们给容器添加一个名字为dragenter的样式,对照样式表可以发现这个样式修改了容器边框的外观为虚线。在离开容器后,我们把这个样式删除了。加上这些效果后,被拖拽物经过容器,容器的边框变为虚线,离开后,容器边框恢复正常。
在item被释放后,容器边框应该复原,所以在dragHandler方法中,我们也添加移除dragenter样式的代码。
拖拽过程中item的效果
这里我希望item在被拖拽时变虚,拖拽结束后复原。我们需要修改dragstartHandler和dragendHandler方法,在拖拽开始的时候添加一个样式,在拖拽结束后移除这个样式。
1 | function dragstartHandler(event){ |
到现在,一个简单的拖拽样例完成了。
扩展
被拖拽物和容器匹配
通过以上例子,能够确定dragover事件是这个问题的关键。容器不监听这个事件或者不阻止默认行为,拖拽的后续流程就被打断不能进行了。所以我们可以在这个方法中添加判断,对于和容器不匹配的被拖拽物,中断后续流程就好了。
现在,问题的关键又被转移到根据什么判断二者匹配。因为dragover事件是被容器监听的,所以在这个事件处理方法中,我们可以根据事件的target获得容器信息。被拖拽物体的信息怎么获得呢?
还记得前文提到的邮包吗?这时我们可以从那个邮包中拿到在dragstart事件中存入的信件啊,里面可以有匹配容器的相关信息啊。太棒了!
但,上面的设想太天真是错误的!
你可以尝试getData一下,啥都得不到。你甚至怀疑这个邮包是不是亲手寄出去的那个。仔细一看,原来为了安全,dragstart时存入信件后,邮包上了一把大锁头,要一直到drop才能打开。为了安全,我还能说什么?难道这个问题就无解了吗?我不相信制定标准的那些大脑袋没有想到这个需求。在这里找到了一个答案。原来他们发现,虽然这个时候得到了邮包不能读信,但是可以透过邮包的透明盒子看到信封的内容。问题一下明朗了,在dragstart时多发一封信,用信封的信息表示匹配信息就行了。
比如我在dragstart时
1 | dragEvent.dataTransfer.setData('typeA',''); |
在dragover时不读取’typeA‘对应的内容,而是检查有没有’typeA‘:
1 | if (dragEvent.dataTransfer.types.includes('typeA')) { |
这样,不包含’typeA‘内容的被拖拽物就被过滤掉了。
DataTransfer
没错,就是那个邮包。这个邮包可以做的事儿很多,前面说的可以存放信件以及额外信息。另外还可以设置拖拽过程中的鼠标样式、设置拖拽物的代理(就是鼠标真实拖动的内容,默认是拖拽物的截图)等等。
坑
如果存在嵌套关系的DOM元素,也都有以上的拖拽逻辑。那么要小心了。子元素的dropstart会冒泡到外层,你亲手存信件到邮包中,等到你的drop事件处理时,你会发现邮包不对啊,targe不对啊。解决的方式是在封好邮包前阻止事件冒泡。