【温故而知新-Javascript】使用拖放
HTML5 添加了对拖放(drag and drop)的支持。我们之前只能依靠jQuery 这样的JavaScript库才能处理这种操作。把拖放内置到浏览器的好处是它可以正确的集成到操作系统中,而且正如将要看到的,它能跨浏览器工作。
1. 创建来源项目
我们通过 draggable属性告诉浏览器文档里的哪些元素可以被拖动。这个值有三个允许的值:
它的默认值是auto,即把决定权交给浏览器,通常来说这就意味着所有元素默认都是可拖动的,我们必须显示设置draggable 属性为false 来禁止拖动。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>定义可拖放项目</title> <style> #src > * {float: left;} #target,#src > img {border: thin solid black;padding: 2px;margin: 4px;} #target {height: 81px;width: 81px;text-align: center;display: table;} #target > p {display: table-cell;vertical-align: middle;} #target > img {margin: 1px;} </style> </head> <body> <div id="src"> <img draggable="true" id="apple" src="../imgs/apple.png" alt="apple" /> <img draggable="true" id="banana" src="../imgs/banana-small.png" alt="banana" /> <img draggable="true" id="lemon" src="../imgs/lemon100.png" alt="lemon" /> <div id="target"> <p>Drop Here</p> </div> </div> <script> var src = document.getElementById("src"); var target = document.getElementById("target"); </script> </body> </html>
此例里有三个img元素,每一个的draggable 的属性都被设为true。这里还创建了一个id为target的div元素,稍后将设置它用来接收我们拖动的img元素。从下图可以看到这个文档再来浏览器里的样子。
我们不需要再做任何设置就能拖动水果图像,但浏览器会提示我们不能把它们释放到任何地方。通常的做法是展示一个禁止进入的标准作为光标,如下图所示:
处理拖动事件
我们通过一系列事件来利用拖放功能
我们可以用这些事件在视觉上强调拖动操作,如下所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用针对被拖动元素的事件</title> <style> #src > * {float: left;} #target,#src > img {border: thin solid black;padding: 2px;margin: 4px;} #target {height: 81px;width: 81px;text-align: center;display: table;} #target > p {display: table-cell;vertical-align: middle;} #target > img {margin: 1px;} img.dragged {background-color: lightgrey;} </style> </head> <body> <div id="src"> <img draggable="true" id="apple" src="../imgs/apple.png" alt="apple" /> <img draggable="true" id="banana" src="../imgs/banana-small.png" alt="banana" /> <img draggable="true" id="lemon" src="../imgs/lemon100.png" alt="lemon" /> <div id="target"> <p id="msg">Drop Here</p> </div> </div> <script> var src = document.getElementById("src"); var target = document.getElementById("target"); var msg = document.getElementById("msg"); src.ondragstart = function(e){ e.target.classList.add("dragged"); } src.ondragend = function(e){ e.target.classList.remove("dragged"); msg.innerHTML = "Drop Here"; } src.ondrag = function(e){ msg.innerHTML = e.target.id; } </script> </body> </html>
此例定义了一个新的CSS样式,它会被应用到属于dragged类的元素上。在dragstart事件触发时把拖动的元素添加到这个类中,在dragend事件触发时把它从类中移除。作为对drag事件的响应,这里把释放区里显示的文本设为被拖动元素的id值。在拖动操作过程中,drag事件每隔几毫秒就会触发以此,所以这不是最有效率的技巧,但它确实能演示这个事件。此例的显示效果如下:
2. 创建释放区
要让某个元素成为释放区,我们需要处理 dragenter和 dragover事件。它们是针对释放区的其中两个事件。
dragenter和 dragover 事件的默认行为是拒绝接受任何被拖放的项目,因此我们必须要做的最重要的事就是防止这种默认行为被执行。
PS:拖放功能的规范告诉我们还必须想要称为释放区的元素应用dropzone属性,而且此属性的值应当包含我们愿意接受的操作与数据类型细节。浏览器实际上不是这么实现拖放功能的。
修改前面例子的JavaScript代码如下:
<script> var src = document.getElementById("src"); var target = document.getElementById("target"); var msg = document.getElementById("msg"); target.ondragenter = handleDrag; target.ondragover = handleDrag; function handleDrag(e){ e.preventDefault(); } src.ondragstart = function(e){ e.target.classList.add("dragged"); } src.ondragend = function(e){ e.target.classList.remove("dragged"); msg.innerHTML = "Drop Here"; } src.ondrag = function(e){ msg.innerHTML = e.target.id; } </script>
添加这些代码后,我们就有了一个活动的释放区。当我们拖动一个项目到释放区元素上时,浏览器会提示如果我们放下时它就会被接受,如下图所示:
接受释放
我们通过处理drop事件来接收释放的元素,它会在某个项目被放到释放区元素上时触发。下面的例子展示了如何处理响应drop事件,具体的做法是使用一个全局变量作为被拖动元素和释放区之间的桥梁。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>处理drop事件</title> <style> #src > * {float: left;} #target,#src > img {border: thin solid black;padding: 2px;margin: 4px;} #target {height: 81px;width: 81px;text-align: center;display: table;} #target > p {display: table-cell;vertical-align: middle;} #target > img {margin: 1px;} img.dragged {background-color: lightgrey;} </style> </head> <body> <div id="src"> <img draggable="true" id="apple" src="../imgs/apple.png" alt="apple" /> <img draggable="true" id="banana" src="../imgs/banana-small.png" alt="banana" /> <img draggable="true" id="lemon" src="../imgs/lemon100.png" alt="lemon" /> <div id="target"> <p id="msg">Drop Here</p> </div> </div> <script> var src = document.getElementById("src"); var target = document.getElementById("target"); var msg = document.getElementById("msg"); var draggedID; target.ondragenter = handleDrag; target.ondragover = handleDrag; function handleDrag(e){ e.preventDefault(); } target.ondrop = function(e){ var newElem = document.getElementById(draggedID).cloneNode(false); target.innerHTML = ""; target.appendChild(newElem); e.preventDefault(); } src.ondragstart = function(e){ draggedID = e.target.id; e.target.classList.add("dragged"); } src.ondragend = function(e){ var elems = document.querySelectorAll(".dragged"); for (var i=0;i<elems.length;i++){ elems[i].classList.remove("dragged"); } } </script> </body> </html>
此例在dragstart事件触发时设置了变量draggedID 的值。这能够记录被拖动元素的id属性值。当drop事件触发时,用这个值克隆了被拖动的img元素,把它添加为释放区元素的一个子元素。其显示效果如下:
3. 使用DataTransfer对象
与拖放操作所触发的事件同时派发的对象是DragEvent,它派生于MouseEvent。DragEvent对象定义了Event与MouseEvent对象的所有功能,并额外增加了 dataTransfer 属性,用来返回用于传输数据到释放区的DataTransfer对象。
我们可以用DataTransfer对象从被拖动元素传输任意数据到释放区元素上。DataTransfer对象定义的属性和方法如下表所示:
在上个例子中,克隆了元素本身。但DataTransfer对象允许我们使用一种更为复杂的方式。我们能做的第一件事是用DataTransfer对象从被拖动元素传输数据到释放区,如下所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用DataTransfer对象传输数据</title> <style> #src > * {float: left;} #target,#src > img {border: thin solid black;padding: 2px;margin: 4px;} #target {height: 81px;width: 81px;text-align: center;display: table;} #target > p {display: table-cell;vertical-align: middle;} #target > img {margin: 1px;} img.dragged {background-color: lightgrey;} </style> </head> <body> <div id="src"> <img draggable="true" id="apple" src="../imgs/apple.png" alt="apple" /> <img draggable="true" id="banana" src="../imgs/banana-small.png" alt="banana" /> <img draggable="true" id="lemon" src="../imgs/lemon100.png" alt="lemon" /> <div id="target"> <p id="msg">Drop Here</p> </div> </div> <script> var src = document.getElementById("src"); var target = document.getElementById("target"); target.ondragenter = handleDrag; target.ondragover = handleDrag; function handleDrag(e){ e.preventDefault(); } target.ondrop = function(e){ var droppedID = e.dataTransfer.getData("Text"); var newElem = document.getElementById(droppedID).cloneNode(false); target.innerHTML = ""; target.appendChild(newElem); e.preventDefault(); } src.ondragstart = function(e){ e.dataTransfer.setData("Text", e.target.id); e.target.classList.add("dragged"); } src.ondragend = function(e){ var elems = document.querySelectorAll(".dragged"); for (var i=0;i<elems.length;i++){ elems[i].classList.remove("dragged"); } } </script> </body> </html>
此例在响应dragstart事件时用setData方法设置了想要传输的数据。第一个蚕食指定了数据的类型,它只支持两个值: Text和 Url。第二个参数是我们想要传输的数据(此例中是被拖动元素的id属性)。为了获取它的值,使用了getData方法,并把数据类型作为参数。
你可能会觉得奇怪:为什么这种方式比使用全局变量更好?答案是它能跨浏览器工作。这么说的意思不是指跨同一个浏览器了IDE窗口或标签页,而是横跨不同类型的浏览器。这意味着我们可以从 Chrome浏览器的文档拖动一个元素,然后再Firefox浏览器的文档里释放它,因为拖放功能的支持是集成在操作系统里的,有着相同的特性。如果你打开一个文本编辑器,输入单词 apple,选中它然后拖动到浏览器的释放区,你就会看到苹果的图像被显示出来,效果和我们拖动同一个文档里的某个img元素一样。效果如下:
3.1 根据数据过滤被拖动的项目
可以用DataTransfer对象里存放的数据来选择我们愿意在释放区接受哪些种类的元素。修改上一个示例的JavaScript代码如下:
<script> var src = document.getElementById("src"); var target = document.getElementById("target"); target.ondragenter = handleDrag; target.ondragover = handleDrag; function handleDrag(e){ if(e.dataTransfer.getData("Text") == "banana"){ e.preventDefault(); } } target.ondrop = function(e){ var droppedID = e.dataTransfer.getData("Text"); var newElem = document.getElementById(droppedID).cloneNode(false); target.innerHTML = ""; target.appendChild(newElem); e.preventDefault(); } src.ondragstart = function(e){ e.dataTransfer.setData("Text", e.target.id); e.target.classList.add("dragged"); } src.ondragend = function(e){ var elems = document.querySelectorAll(".dragged"); for (var i=0;i<elems.length;i++){ elems[i].classList.remove("dragged"); } } </script>
此例从DataTransfer对象获得数据值,然后检查它是什么。此例表明只有当数据值是banana时才愿意接受被拖动的元素。这么做的效果是过滤掉了苹果和柠檬的图像。当用户拖动这些图像当释放区时,浏览器会提示它们不能被释放。
PS:这种过滤方式测试暂时不可用,估计和浏览器的支持度有关。
3.2 拖放文件
另一种隐藏在浏览器里的HTML5新功能被称为文件API,它允许我们使用本机文件,不过是以严格受控的方式。部分控制来自于我们通常不直接与文件API进行交互,而是使它通过其他功能显露出来,包括拖放功能。下面的示例展示了如何使用文件API,在用户从操作系统里拖动文件并放入释放区时做出响应。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>处理文件</title> <style> body > * {float: left;} #target {border:medium double black;margin: 4px;height: 75px;width: 200px;text-align: center;display: table;} #target > p {display: table-cell;vertical-align: middle;} table {margin: 4px;border-collapse: collapse;} th,td {padding: 4px;} </style> </head> <body> <div id="target"> <p id="msg">Drop Files Here</p> </div> <table id="data" border="1"></table> <script> var target = document.getElementById("target"); target.ondragenter = handleDrag; target.ondragover = handleDrag; function handleDrag(e){ e.preventDefault(); } target.ondrop = function(e){ var files = e.dataTransfer.files; var tableElem = document.getElementById("data"); tableElem.innerHTML = "<tr><th>Name</th><th>Type</th><th>Size</th></tr>"; for(var i=0;i<files.length;i++){ var row = "<tr><td>" + files[i].name+"</td><td>" + files[i].type+"</td><td>" + files[i].size+"</td></tr>"; tableElem.innerHTML += row; } e.preventDefault(); } </script> </body> </html>
当用户把文件放入释放区时,DataTransfer对象的文件属性会返回一个FileList对象。我们可以将它视为一个由File对象构成的数组,每个对象都代表用户释放的一个文件(用户可以选择多个文件然后一次性释放它们)。下表展示了File对象的属性:
此例里,script 枚举了放入释放区的文件,并在表格里显示了File属性的值。此例的运行效果如下:
在表单里上传被释放文件
我们可以结合拖放功能、文件API和用Ajax请求上传数据,让用户能从操作系统拖动想要在表单里提交的文件。下面的是示例对此进行演示,新建HTML文档如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>结合拖放、文件API和FormData对象</title> <style> body > * {float: left;} #target {border:medium double black;margin: 4px;height: 75px;width: 200px;text-align: center;display: table;} #target > p {display: table-cell;vertical-align: middle;} table {margin: 4px;border-collapse: collapse;} th,td {padding: 4px;} </style> </head> <body> <form id="form1" method="post" action="http://localhost:53396/ajax/html5/fileupload.aspx"> <div id="target"> <p id="msg">Drop Files Here</p> </div> <table id="data" border="1"></table> <button id="submit" type="submit">Submit Form</button> </form> <script> var target = document.getElementById("target"); var httpRequest; var fileList; document.getElementById("submit").onclick = handleButtonPress; target.ondragenter = handleDrag; target.ondragover = handleDrag; function handleDrag(e){ e.preventDefault(); } target.ondrop = function(e){ fileList = e.dataTransfer.files; var tableElem = document.getElementById("data"); tableElem.innerHTML = "<tr><th>Name</th><th>Type</th><th>Size</th></tr>"; for(var i=0;i<fileList.length;i++){ var row = "<tr><td>" + fileList[i].name+"</td><td>" + fileList[i].type+"</td><td>" + fileList[i].size+"</td></tr>"; tableElem.innerHTML += row; } e.preventDefault(); } function handleButtonPress(e){ e.preventDefault(); var form = document.getElementById("form1"); var formData = new FormData(form); if(fileList || true){ for(var i=0;i<fileList.length;i++){ formData.append("file"+i,fileList[i]); } } httpRequest = new XMLHttpRequest(); httpRequest.onreadystatechange = handleResponse; httpRequest.open("POST",form.action); httpRequest.send(formData); } function handleResponse(){ if(httpRequest.readyState == 4 && httpRequest.status == 200){ alert(httpRequest.responseText); } } </script> </body> </html>
这个例子里用FormData.append方法添加那些放入释放区的文件,并传递一个File对象作为此方法的第二个参数。当表单被提交时,文件内容会作为表单请求的一部分自动上传到服务器。
这里的服务器用.NET实现,新建Web项目,并新建 fileupload.aspx 文件,其cs代码如下:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace Web4Luka.Web.ajax.html5 { public partial class fileupload : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { string tip = "上传失败!"; string path; if (Request.HttpMethod == "POST") { if (Request.ContentType.IndexOf("multipart/form-data") > -1) { if (Request.Files.Count > 0) { for (int i = 0; i < Request.Files.Count; i++) { HttpPostedFile file = Request.Files[i]; path = Server.MapPath("/upload/files/" + file.FileName); if (File.Exists(path)) { File.Delete(path); } file.SaveAs(path); } tip = "上传成功!"; } } Response.AddHeader("Access-Control-Allow-Origin", "http://localhost:63342"); Response.Write(tip); } } } }
注意在服务器端创建对应上传文件保存的文件夹,此例的运行效果如下图所示: