事宜名产生事宜的元素事宜解释dragstart被拖拽的元素拖拽开始drag被拖拽的元素拖拽过程中dragover拖拽时鼠标经由的元素被拖拽元素在当前元素上方移动dragenter拖拽时鼠标经由的元素被拖拽元素进入当前元素区域dragleave拖拽时鼠标经由的元素被拖拽元素离开当前元素区域drop拖放的目标元素有元素被拖放到了当前元素中dragend拖放的工具元素拖拽结束
上述事宜的触发流程为:
当鼠标点击一个元素并移动,这会在该元素上触发dragstart和drag事宜(如果元素不是链接或图片,则须要设置draggable=“true”,否则不许可拖动)。当被拖拽元素进入某个元素的区域时,会触发该区域元素的dragenter事宜。当被拖拽元素在某个元素上方移动时,会触发该区域元素的dragover事宜。当被拖拽元素离开某个元素时,会触发该区域元素的dragleave事宜。开释鼠标时,会触发目标元素的drop事宜,同时会触发被拖拽元素的dragend事宜。这些事宜构成了一次拖拽完全的生命周期,拖拽过程中的所有行为都该当在这些事宜中定义。拖拽最为核心的流程便是:在拖拽开始时,将我们须要通报的数据写入一个拖拽事宜工具内;当开释鼠标时,由目标元素得到这个事宜工具,并取出写入的数据实行操作。这个过程中,鼠标所经由的元素都具备获取该事宜工具的能力。
现在我们以一个最基本的原生拖拽为例,来讲解拖拽的大致流程。该例子来自W3School(这里是截图,如需体验效果,请移步网页示例原生拖拽):
这里是两个div,个中左边的div里含有一张图片,现在我们希望实现将图片自由在两个div内拖拽。下面是页面的HTML构造(这里省略了css样式代码):
<div id="div1"> <img id="drag1" src="/i/eg_dragdrop_w3school.gif"/></div><div id="div2"></div>
上面对时省略了拖拽干系的事宜,我们将通过一步步为元素添加事宜,来讲解这些事宜详细的含义和用法。
首先第一步,我们须要为图片(img元素)设置draggable=“true”(实际上img和a元素默认便是可拖拽的,不须要设置该参数。这里为了讲解事理,我们暂且把图片当做一个普通元素看待)。于是img标签就变成了下面的样子:
<img id="drag1" src="/i/eg_dragdrop_w3school.gif" draggable="true"/>
现在img就成了一个可拖拽的元素。当你用鼠标在该元素上点击并移动时,鼠标的下方就会涌现一张浅色的图片跟随鼠标移动,这是浏览器的默认行为,它表示当前你正在拖拽该图片。
虽然浏览器为鼠标下方添加了一张图片,让用户在视觉上认为图片已经随着鼠标移动了,但事实并不是这样。如果只是添加这一个属性,当你把图片拖拽到右侧容器并开释鼠标后,你会创造什么都没有发生 – 图片仍旧在原来的位置。这解释拖拽并不是只开启元素的拖拽功能就可以。想要实现拖拽功能,最主要的是依赖一个事宜工具,这个事宜工具可以看做一个数据载体,而上面讲到的7个拖拽事宜便是许可我们在不同阶段操作这个事宜工具。
我们来看这个事宜工具在拖拽的过程中的详细行为。
上面我们说到,dragstart在被拖拽元素上触发,于是我们可以为被拖拽元素(也便是img图片)注册一个回调函数,它卖力在拖拽开始时向事宜工具写入数据:
<img id="drag1" src="/i/eg_dragdrop_w3school.gif" draggable="true" ondragstart="drag(event)"/> <script> function drag(event){ event.dataTransfer.setData("Text",event.target.id); } </script>
现在img上注册了一个dragstart回调函数。一旦我们对该图片实行了拖拽,浏览器就会封装一个拖拽工具(我们记为event),并触发该元素的dragstart事宜,并将该工具传入我们的回调函数。得到这个工具后,我们在回调函数内只定义了一行代码,便是将被拖拽元素的id保存在该工具的dataTransfer属性内。由于拖拽事宜是在图片元素上触发的,以是event.target便是这个图片元素,此时dataTransfer内保存的便是img的id:“drag1”。setData的第一个参数“Text”表示当前存储的数据类型是文本(或者说是普通的字符串)。如果你想看一下这个事宜工具是什么样的,它大概长这样:
OK,现在让我们跳过这个令人眼花的工具。我们只须要知道这个工具里存储了与本次拖拽干系的所有参数,而我们想要通报的数据也已经写在了它的dataTransfer属性里。
在默认情形下,所有的DOM元素都不接管开释行为。从视觉效果上来看,当你拖拽该图片到某块空缺区域时,你可能会创造鼠标变成了禁用图标(一样平常为一个圆圈带一个斜线),这表示当前区域不许可开释拖拽元素。我们可以通过事宜工具的preventDefault()方法来禁止这种默认行为。比如我们现在想让上面例子中右侧的那个div许可开释被拖拽元素,我们就可以给它注册一个ondragover(鼠标拖拽时在当前元素上方移动会触发该事宜)回调函数,在该函数内取消浏览器的默认行为。代码如下:
<div id="div2" ondragover="allowDrop(event)"></div><script> function allowDrop(event){ event.preventDefault(); }</script>
取消了浏览器的默认行为后,当鼠标拖拽图片移动到右侧的div时,鼠标就会变成可开释的图标(详细图标因浏览器而异),它表示当前区域接管开释。对付上面的代码,如果你考试测验拖拽,你会创造虽然从视觉上已经可拖拽了,但是一旦鼠标开释,仍旧什么都没有发生,图片并没有按我们所想被放置到右侧的容器中。
这是为什么呢?
由于浏览器没有为我们开释鼠标的事宜定义任何默认的行为,我们想做什么事必须自己手动定义。那么浏览器为什么不为我们供应自动移动DOM元素的默认行为呢?缘故原由也很大略:为了避免歧义。我们知道,HTML是一种嵌套的构造,假如有下面两个嵌套的div:
当你在内部的div内开释鼠标时,这个事宜不只会被内部的div元素捕获,还会被外部的div捕获(包括document元素也会捕获到这个事宜),那么浏览器把被拖拽元素添加到哪个元素上呢?浏览器无法做出选择,因此无法为开拓者供应默认的行为(当然可能还有其他缘故原由,但仅这一个缘故原由就已经很充分了)。
但是这个问题对开拓者来说就不存在任何歧义,由于开拓者可以选择为哪个元素绑定回调来处理这个事宜(当然也可以都绑定,它们都会得到实行)。回到上面的例子,现在我们希望把图片拖放到右侧div时移动图片,于是我们给右侧的div绑定一个ondrop事宜来监听鼠标的开释事宜。代码如下:
<div id="div2" ondrop="drop(event)" ondragover="allowDrop(event)"></div><script> function drop(event){ event.preventDefault(); var data=event.dataTransfer.getData("Text"); event.target.appendChild(getElementById(data)); }</script>
我们给右侧div绑定了ondrop事宜,当鼠标拖拽图片在该区域开释时就会实行该回调函数,浏览器会把拖拽开始时天生的事宜工具作为参数通报进来(还记得吗?我们在这个工具的dataTransfer属性里记录了被拖拽图片元素的id,现在要派上用场了)。
首先第一步,仍旧是用preventDefault()禁止浏览器的默认行为(我们在dragover里只是担保鼠标在移动时不涌现丑陋的禁用图标,但是鼠标开释时仍旧会涌现,虽然只有一瞬间,但这非常影响用户体验)。
第二步,取出我们在datatTransfer里写入的图片元素的id。
第三步,用原生选择器从DOM树中找到这个元素,利用appendChild方法添加到当前元素(此时的event.target指的是右侧的容器,由于该事宜是在右侧容器上触发的)上。
以是我们真正实行的操作无非便是把图片元素查出来,用原生的DOM方法添加到右侧容器中。但是被拖拽的图片和目标容器本身是相互独立的,只有借助一个事宜工具,目标容器才知道到底是哪个元素须要被添加进来。而这个事宜的dataTransfer属性便是数据通报的载体。
经由上面的修正,这个代码就可以实现把图片从左侧div拖拽到右侧div了。为了能够实现两个容器的相互拖拽,我们须要为左侧容器也写上同样的监听事宜,这样两个div就都具备了放置图片元素的能力,也便是W3School示例中的效果。一个最基本的拖拽也就实现了。
换个角度看拖拽(原创声明:如需引用该部分内容,请注明出处)
从更高的角度来说,拖拽是浏览器为开拓者封装的一个通道。这个通道的出发点是被拖拽的元素,终点是目标元素。浏览器用一个事宜工具用来描述与拖拽干系的参数,它产生于出发点,被通报到目标元素,而鼠标经由的元素也可以从这个通道中获取事宜工具。
用户将鼠标放到一个元素上,按下并拖动的行为将在该元素上开启这个通道。浏览器会天生一个用于描述该拖拽行为的事宜工具,我们可以通过该工具写入自己的数据,也可以设置与本次拖拽干系的参数(比如移动时鼠标的样式、许可的拖拽类型等),然后该工具将在通道内通报。
浏览器向我们供应了多少个原生事件来从通道中获取这个事宜工具,并实行须要实行的操作(比如当鼠标进入某个元素时,该元素可以通过ondragenter事宜接口从通道中获取事宜工具,如果你希望鼠标从当前元素上方掠过时变为可放置的样式,就可以通过event.preventDefault()来实现,就像我们上面做的那样)。让我们重新的角度重新来看浏览器为开拓者供应的7个原生事件:
事宜名产生事宜的元素角色事宜解释dragstart被拖拽的元素通道出发点开启通道,天生事宜工具,并写入数据或设置参数等drag被拖拽的元素通道出发点许可在全体拖拽过程中从通道里获取事宜工具dragend拖放的工具元素通道出发点开释鼠标时,浏览器通过该事宜关照出发点元素dragenter拖拽时鼠标经由的元素通道的路径许可当鼠标进入某个元素时,从通道中获取事宜工具dragover拖拽时鼠标经由的元素通道的路径许可当鼠标在某个元素上移动时,从通道中获取事宜工具dragleave拖拽时鼠标经由的元素通道的路径许可当鼠标离开某个元素时,从通道中获取事宜工具drop拖放的目标元素通道的终点开释鼠标时,目标元素从通道中得到事宜工具
从表格中可以看到,被拖拽的元素(通道的出发点)可以在拖拽的开始和结束,以及拖拽的全体过程(常日每隔350毫秒触发一次)中监听该事宜。鼠标经由的元素只能在鼠标从该元素上方经由时监听到拖拽事宜(这个过程又细分为进入、移动和离开)。而目标元素只能在鼠标开释时才能监听到该事宜(由于在用户开释鼠标之前,我们无法知道目标元素是谁)。
现在是不是对拖拽又有了新的认识?
既然拖拽只是建立一个通道,那么我们可以通报的又何止元素的id呢?实际上该工具支持写入四种数据类型:
“text/plain”:或简写为"text"。纯文本,也便是字符串。“text/html”:HTML格式的数据。“text/xml”:xml格式的数据。“text/url-list”:或简写为“url”。url列表。实际上第一种数据类型就可以知足大多数情形下的需求。对付非字符串类型的数据,只须要压缩成字符串,末了再解析为原数据构造即可(如json数据可以用JSON.stringify压缩成字符串,再用JSON.parse解析为工具)。
上面我们只是通报了被拖拽元素的id,实际上与该元素干系的任何参数,乃至与该元素无关的数据(只要我们认为它对本次拖拽有用),都可以写入通道。而且开释鼠标时也不一定要实行appendChild来添加元素,我们可以在开释鼠标时做任何我们想做的事(如弹出一个提示框,或者根据传过来的参数天生任意的DOM构造,乃至把被拖拽的元素添加到页面的任何地方(只要你认为须要这样做,浏览器都是许可的,哪怕用户以为很奇怪))。
下面我将自己写一个示例,来解释拖拽的灵巧性(代码在后面可以找到,可以直接保存为一个HTML文件双击运行)。
该例子中,我们把上面的一个可拖拽按钮的textContent(即:“可拖拽元素”这几个字)写入事宜工具。然后为第一个div定义的拖拽行为是添加到内部的一个ul中,为第二个定义的行为是利用alert弹出提示框,为第三个定义的行为是将其显示在第一个div内,同时在字符串前面拼上“来自第三个div的”这几个字。
现在当我们向第一个div内拖拽时,列表就会多出一项,笔墨内容为“可拖拽元素”。而向第二个div内拖拽时,就会涌现如图所示的网页提示信息。向第三个div内拖拽时,我们看到在第一个div内列表的末了面多了一项“来自第三个div的可拖拽元素”。js代码如下:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>HTML5原生拖拽</title> <style> .des{ width:300px; height:200px; float: left; border: 1px solid #e6e6e6; } p{ color: #a6a6a6; font-size: 12px; } </style></head><body><button id="src" draggable="true" class="nav">可拖拽元素</button><br/><br/><div id="des1" class="des"> <p>放进该区域会显示为列表<p/> <ul id="container"> </ul></div><div id="des2" class="des"> <p>放进该区域会得到一条提示<p/></div><div id="des3" class="des"> <p>放进该区域会输出在第一个div内<p/></div></body><script> var src = getElementById("src"); var des1 = getElementById("des1"); var des2 = getElementById("des2"); var des3 = getElementById("des3"); src.addEventListener("dragstart", function(e){ var dt = e.dataTransfer; dt.effectAllowed = 'all'; dt.setData("text/plain", e.target.textContent); }); des1.addEventListener("drop", function(e){ var dt = e.dataTransfer; var text = dt.getData("text/plain"); var container = getElementById("container"); var li = createElement("li"); li.textContent = text; container.appendChild(li); e.preventDefault(); e.stopPropagation(); }, false); des2.addEventListener("drop", function(e){ var dt = e.dataTransfer; var text = dt.getData("text/plain"); ; e.preventDefault(); e.stopPropagation(); }, false); des3.addEventListener("drop", function(e){ var dt = e.dataTransfer; var text = dt.getData("text/plain"); text = "来自第三个div的" + text; var container = getElementById("container"); var li = createElement("li"); li.textContent = text; container.appendChild(li); e.preventDefault(); e.stopPropagation(); }, false); des1.ondragover = function(e){e.preventDefault();} des1.ondrop = function(e){e.preventDefault();} des2.ondragover = function(e){e.preventDefault();} des2.ondrop = function(e){e.preventDefault();} des3.ondragover = function(e){e.preventDefault();} des3.ondrop = function(e){e.preventDefault();}</script></html>
虽然是很大略的示例,但是我想已经可以证明拖拽的灵巧性。其余这里只是在开释时有不同的行为,我们还可以对不同的元素在拖拽开始时向事宜工具写入任意的内容,这样组合起来拖拽就会变得相称灵巧。
下面我们先容一个将element-ui的el-tree上的节点拖拽到外部的例子,它更能解释原生拖拽的强大之处。
el-tree中节点拖拽的扩展el-tree是Vue的element-ui中的树组件,该组件供应了较为强大的拖拽功能,一个最根本的可拖拽树只须要写成下面这样即可(来自element-ui官网):
<el-tree :data="data" node-key="id" default-expand-all @node-drag-start="handleDragStart" @node-drag-enter="handleDragEnter" @node-drag-leave="handleDragLeave" @node-drag-over="handleDragOver" @node-drag-end="handleDragEnd" @node-drop="handleDrop" draggable :allow-drop="allowDrop" :allow-drag="allowDrag"></el-tree>
这里的事宜监听器就对应我们上面讲到的原生事件,它们对每个节点都是生效的。draggable属性表示开启树的拖拽功能,这样树的每个节点都会被添加draggable属性。
默认情形下,树上的节点只支持在树的内部拖拽。也便是说,你无法把树上的节点拖拽到树的表面。但是这又是一个非常常见的需求(github的issue中说el-tree具备这个能力,但是并没有找到干系示例)。作为前端开拓者,如果框架有一定的局限性,原生技能将是我们最强大的武器。下面我将大略先容我是如何将树上的节点拖拽到树的外部的,希望对感兴趣的同学有所启示。
假设我们现在有一个容器,我们希望可以把树上的某个节点拖拽到这个容器里形成一个列表,这个列表暂时只保留原树节点的文本内容和id(因详细需求而异)。我们连续利用上面的树作为拖拽源,并给出下面一个div作为容器:
<div class="menu-list" @drop="handleTargetDrop" @dragover="handleTargetDragOver"> <ul> <li v-for="item in menus" :key="item.id> <span>{{item.name}}</span> </li> </ul></div>
由于这是在Vue中,我们不须要直接操作DOM,只须要修正该ul对应的数据即可。在拖拽开始之前,menus值为空,以是该容器内不会显示任何内容。
现在我们先来处理el-tree。由于我们不须要改变树构造,因此须要屏蔽树自身的drop行为,这可以很随意马虎通过设置绑定的allow-drop来实现,同时须要设置allow-drag使节点可拖拽:
allowDrop(draggingNode, dropNode, type) { return false;},allowDrag(draggingNode) { return true;},
好的,现在树上的节点都无法在树上移动了,并且都是可拖拽的。接下来要处理向外部拖动的行为了。我们须要定义节点的node-drag-start事宜,它与原生事件的dragstart对应,是框架向我们供应的接口。我们可以在该事宜的回调函数内将我们须要通报的数据封装进去,为了大略,我们直接通报全体节点的data即可。如下:
handleDragStart(node, ev) { let dt = ev.dataTransfer; ev.dataTransfer.effectAllowed = 'copy'; dt.setData("text/plain", JSON.stringify(node.data));},
现在我们把树上被拖拽的那个节点的data压缩成json字符串写进了事宜工具的dataTransfer里。至此,树节点已经可以向外供应节点数据了。
下一步便是要处理我们的容器了。首先,我们要取消浏览器阻挡拖拽的默认行为,为了用户体验,我们在dragover和drop中同时阻挡该行为(drop的我们后面可以看到)。
handleTargetDragOver(e){ e.preventDefault();},
下面便是要处理drop事宜,我们须要在鼠标开释时修正容器的列表所对应的数据menus(从这里就可以看出MVVM的设计理念,我们的视线永久放在如何操作数据上,而不会想着如何操作DOM,由于框架会在数据变革时自动操作DOM)。实际上这相称大略:
handleTargetDrop(e){ let data = e.dataTransfer; let content = JSON.parse(data.getData("text/plain")); this.menus.push({id: content.id, name: content.name}); e.preventDefault(); //常日不须要阻挡冒泡,但是当涌现容器嵌套时最好这么做 //它可以防止节点被添加到数组中两次 e.stopPropagation();}
我们看到,只须要非常大略的代码,就可以将树上的节点拖拽到外部容器了,这再一次证明了原生拖拽的灵巧性和强大。
总结本文并不是一篇详细先容原生拖拽细节的文章。实际上拖拽事宜中有很多的参数都可以设置,比如你可以设置当前拖拽只能复制,或者修正鼠标移动时跟随鼠标移动的图片,你还可以设置当元素进入某个区域时,底部的元素产生一定的动态效果,这样会带来相称高等的用户体验。
此外,在事宜工具的dataTransfer中还包含一个有用的属性files,它存储了从浏览器外部拖拽进来的文件,如果我们在某个元素的drop事宜中读取这个文件列表,就可以获取用户拖拽进来的文件,HTML5的file API许可我们直接在浏览器显示该文件,或者选择上传到做事器等(如果你利用的是Chrome,并且征得了用户赞许,乃至可以修正这些文件,这依赖fileWriter接口,但由于安全问题,该接口的支持性不是很好)。除此之外,单凭拖拽乃至可以写出一些有趣的HTML5网页游戏,而这完备取决于你的创造能力。
本文最主要的目的不是展示该技能可以被利用得多么神奇,而是希望探究它的基本事理,为往后的利用打下良好的根本。希望对不理解原生拖拽的同学有所帮助。