在用户拖拽文件到浏览器的某个元素上时,js能够监听到与拖拽相关的事件,并对拖拽结果进行处理,本文讨论下和拖拽文件相关的一些问题,不过没有处理太多关于兼容性的问题。
js
可以监听到拖拽的事件有drag
、dragend
、dragenter
、dragexit(没有浏览器实现)
、dragleave
、dragover
、dragstart
、drop
,详细的内容能够看MDN。javascript
其中,与拖拽文件相关的事件有dragenter(文件拖拽进)
、dragover(文件拖拽在悬浮)
、dragleave(文件拖拽离开)
、drop(文件拖拽放下)
。
拖拽事件能够绑定到指定的DOM元素上,能够绑定到整个页面中。java
var dropEle = document.querySelector('#dropZone'); dropEle.addEventListener('drop', function (e) { // }, false); document.addEventListener('drop', function (e) { // }, false);
通常来讲,咱们只须要把处理拖拽文件的业务逻辑写到drop
事件中就能够了,为何还要绑定dragenter
、dragover
、dragleave
这三个事件呢?web
由于当你拖拽一个文件到没有对拖拽事件进行处理的浏览器中的时候,浏览器会打开这个文件,好比拖拽一张图片浏览器会打开这个图片,在没有PDF阅读器的时候也能够拖拽一个PDF到浏览器中,浏览器就会打开这个PDF文件。浏览器
若是浏览器打开了拖拽的文件,页面就跳走了,咱们但愿获得拖拽的文件,而不是让页面跳走。上面说到浏览器会打开拖拽的文件是浏览器的默认行为,咱们须要阻止这个默认行为,就须要再上述的事件中进行阻止。异步
dropZone.addEventListener("dragenter", function (e) { e.preventDefault(); e.stopPropagation(); }, false); dropZone.addEventListener("dragover", function (e) { e.preventDefault(); e.stopPropagation(); }, false); dropZone.addEventListener("dragleave", function (e) { e.preventDefault(); e.stopPropagation(); }, false); dropZone.addEventListener("drop", function (e) { e.preventDefault(); e.stopPropagation(); // 处理拖拽文件的逻辑 }
实际上dragenter
不阻止默认行为也不会触发浏览器打开文件,为了防止某些浏览器可能有的兼容性问题,把拖拽周期中的全部的事件都阻止默认行为而且阻止了事件冒泡。函数
咱们会在drop
这个事件的回调中的事件对象可以获得文件对象。测试
在事件对象中,一个e.dataTransfer
这样的属性,它是一个DataTransfer
类型的数据,有以下的属性调试
属性 | 类型 | 说明 |
---|---|---|
dropEffect | String | 用来hack某些兼容性问题 |
effectAllowed | String | 暂时不用 |
files | FileList | 拖拽的文件列表 |
items | DataTransferItemList | 拖拽的数据(有多是字符串) |
types | Array | 拖拽的数据类型 该属性在Safari下比较混乱 |
在Chrome
中咱们用items
对象得到文件,其余浏览器用files
得到文件,主要是为了处理拖拽文件夹的问题,最好不容许用户拖拽文件夹,由于文件夹内可能还有文件夹,递归上传文件会好久,若是不递归查找,只上传目录第一层级的文件,用户可能觉得上传功能了,可是没有上传子目录文件,因此仍是禁止上传文件夹比较好,后面我会说要怎么处理。code
dropZone.addEventListener("drop", function (e) { e.preventDefault(); e.stopPropagation(); var df = e.dataTransfer; var dropFiles = []; // 存放拖拽的文件对象 if(df.items !== undefined) { // Chrome有items属性,对Chrome的单独处理 for(var i = 0; i < df.items.length; i++) { var item = df.items[i]; // 用webkitGetAsEntry禁止上传目录 if(item.kind === "file" && item.webkitGetAsEntry().isFile) { var file = item.getAsFile(); dropFiles.push(file); } } } }
这里只测试了Safari,其余浏览器并无测试,不过看完本文必定也有思路处理其余浏览器的兼容状况。对象
dropZone.addEventListener("drop", function (e) { e.preventDefault(); e.stopPropagation(); var df = e.dataTransfer; var dropFiles = []; // 存放拖拽的文件对象 if(df.items !== undefined) { // Chrome拖拽文件逻辑 } else { for(var i = 0; i < df.files.length; i++) { dropFiles.push(df.files[i]); } } }
因为Safari
没有item
,天然也没有webkitGetAsEntry
,因此在Safari没法肯定拖拽的是不是文件仍是文件夹。
浏览器获取到的每一个file对象有四个属性:lastModified
、name
、size
、type
,其中type
是文件的MIME Type
,文件夹的type
是空的,可是有些文件没有MIME Type
,若是按照type
是否为空判断是否是拖拽的文件夹的话,会误伤一部分文件,因此这个方法行。
那么还有什么方法能够判断呢,思路大概是这样子的,用户拖拽的文件和文件夹应该是不同的东西,用File API
操做的时候应该会有区别,好比进行某些操做的时候,文件就可以正常操做,可是文件夹就会报错,经过错误的捕获就可以判断是文件仍是文件夹了,好咱们根据这个思路来写一下。
dropZone.addEventListener("drop", function (e) { e.preventDefault(); e.stopPropagation(); var df = e.dataTransfer; var dropFiles = []; if(df.items !== undefined){ // Chrome拖拽文件逻辑 } else { for(var i = 0; i < df.files.length; i++){ var dropFile = df.files[i]; if ( dropFile.type ) { // 若是type不是空串,必定是文件 dropFiles.push(dropFile); } else { try { var fileReader = new FileReader(); fileReader.readAsDataURL(dropFile.slice(0, 3)); fileReader.addEventListener('load', function (e) { console.log(e, 'load'); dropFiles.push(dropFile); }, false); fileReader.addEventListener('error', function (e) { console.log(e, 'error,不能够上传文件夹'); }, false); } catch (e) { console.log(e, 'catch error,不能够上传文件夹'); } } } } }, false);
上面代码建立了一个FileReader
实例,经过这个实例对文件进行读取,我测试读取一个1G多的文件要3S多,时间有点长,就用slice
截取了前3个字符,为何是前3个不是前2个或者前4个呢,由于代码是我写的,我开心这么写呗~
若是load
事件触发了,就说明拖拽过来的东西是文件,若是error
事件触发了,就说明是文件夹,为了防止其余可能的潜在错误,用try
包起来这段代码。
通过测试发现经过Mac
的Finder
拖拽文件没有问题,可是有时候文件并不必定在Finder
中,也可能在某些应用中,有一个应用叫作圈点
,这个应用的用户反馈文件拖拽失效,去看了其余开源文件上传的源码,发现了这样一行代码:
dropZone.addEventListener("dragover", function (e) { e.dataTransfer.dropEffect = 'copy'; // 兼容某些三方应用,如圈点 e.preventDefault(); e.stopPropagation(); }, false);
须要把dropEffect
置为copy
,上网搜了下这个问题,源码文档中也没有说为何要加这个,有兴趣的同窗能够找一下为何。
因为用了FileReader
去读取文件,这是一个异步IO操做,为了记录当前处理了多少个文件,以及何时触发拖拽结束的回调,写了一个checkDropFinish
的方法一直去比较处理的文件数量和文件总数,肯定全部文件处理完了后就去调用完成的回调。
另外,我在最后调试异步处理的时候,用的断点调试,发现断点调试在Safari
中会致使异步回调不触发,须要本身调试定制功能的同窗注意下。
// 得到拖拽文件的回调函数 function getDropFileCallBack (dropFiles) { console.log(dropFiles, dropFiles.length); } var dropZone = document.querySelector("#dropZone"); dropZone.addEventListener("dragenter", function (e) { e.preventDefault(); e.stopPropagation(); }, false); dropZone.addEventListener("dragover", function (e) { e.dataTransfer.dropEffect = 'copy'; // 兼容某些三方应用,如圈点 e.preventDefault(); e.stopPropagation(); }, false); dropZone.addEventListener("dragleave", function (e) { e.preventDefault(); e.stopPropagation(); }, false); dropZone.addEventListener("drop", function (e) { e.preventDefault(); e.stopPropagation(); var df = e.dataTransfer; var dropFiles = []; // 拖拽的文件,会放到这里 var dealFileCnt = 0; // 读取文件是个异步的过程,须要记录处理了多少个文件了 var allFileLen = df.files.length; // 全部的文件的数量,给非Chrome浏览器使用的变量 // 检测是否已经把全部的文件都遍历过了 function checkDropFinish () { if ( dealFileCnt === allFileLen-1 ) { getDropFileCallBack(dropFiles); } dealFileCnt++; } if(df.items !== undefined){ // Chrome拖拽文件逻辑 for(var i = 0; i < df.items.length; i++) { var item = df.items[i]; if(item.kind === "file" && item.webkitGetAsEntry().isFile) { var file = item.getAsFile(); dropFiles.push(file); console.log(file); } } } else { // 非Chrome拖拽文件逻辑 for(var i = 0; i < allFileLen; i++) { var dropFile = df.files[i]; if ( dropFile.type ) { dropFiles.push(dropFile); checkDropFinish(); } else { try { var fileReader = new FileReader(); fileReader.readAsDataURL(dropFile.slice(0, 3)); fileReader.addEventListener('load', function (e) { console.log(e, 'load'); dropFiles.push(dropFile); checkDropFinish(); }, false); fileReader.addEventListener('error', function (e) { console.log(e, 'error,不能够上传文件夹'); checkDropFinish(); }, false); } catch (e) { console.log(e, 'catch error,不能够上传文件夹'); checkDropFinish(); } } } } }, false);