mozilla和webkit浏览器现在都允许目录上传。当在<input type="file">
元素中选择目录或在元素中删除目录时,如何按照firefox和chrome/chromium中实际目录中出现的顺序列出所有目录和文件,并在所有上传的目录迭代后对文件执行任务?
简短总结:可以在<input type="file">
元素上设置webkitdirectory
属性;附加change
、drop
事件;使用.createReader()
, .readEntries()
来获取所有选中/删除的文件和文件夹,并使用例如Array.prototype.reduce()
, Promise
和递归来迭代它们。
注意这里有两个不同的api:
-
<input type="file">
的webkitdirectory
特性及其change
事件。- 这个API不支持空文件夹。他们被跳过了。
-
DataTransferItem.webkitGetAsEntry()
及其drop
事件,这是拖放API的一部分。 这个API支持空文件夹。
他们都可以在Firefox中工作,即使他们有"webkit";
它们都处理文件夹/目录层次结构。
如前所述,如果你需要支持空文件夹,你必须强迫你的用户使用拖放而不是操作系统文件夹选择器,当点击<input type="file">
时显示。
完整代码示例
<input type="file">
也接受拖放到更大的区域。
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
input[type="file"] {
width: 98%;
height: 180px;
}
label[for="file"] {
width: 98%;
height: 180px;
}
.area {
display: block;
border: 5px dotted #ccc;
text-align: center;
}
.area:after {
display: block;
border: none;
white-space: pre;
content: "Drop your files or folders here!aOr click to select files folders";
pointer-events: none; /* see note [drag-target] */
position: relative;
left: 0%;
top: -75px;
text-align: center;
}
.drag {
border: 5px dotted green;
background-color: yellow;
}
#result ul {
list-style: none;
margin-top: 20px;
}
#result ul li {
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
#result li span {
font-weight: bold;
color: navy;
}
</style>
</head>
<body>
<!-- Docs of `webkitdirectory:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
-->
<!-- Note [drag-target]:
When you drag something onto a <label> of an <input type="file">,
it counts as dragging it on the <input>, so the resulting
`event` will still have the <input> as `.target` and thus
that one will have `.webkitdirectory`.
But not if the <label> has further other nodes in it (e.g. <span>
or plain text nodes), then the drag event `.target` will be that node.
This is why we need `pointer-events: none` on the
"Drop your files or folder here ..." text added in CSS above:
So that that text cannot become a drag target, and our <label> stays
the drag target.
-->
<label id="dropArea" class="area">
<input id="file" type="file" directory webkitdirectory />
</label>
<output id="result">
<ul></ul>
</output>
<script>
var dropArea = document.getElementById("dropArea");
var output = document.getElementById("result");
var ul = output.querySelector("ul");
function dragHandler(event) {
event.stopPropagation();
event.preventDefault();
dropArea.className = "area drag";
}
function filesDroped(event) {
var processedFiles = [];
console.log(event);
event.stopPropagation();
event.preventDefault();
dropArea.className = "area";
function handleEntry(entry) {
// See https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
let file =
"getAsEntry" in entry ? entry.getAsEntry() :
"webkitGetAsEntry" in entry ? entry.webkitGetAsEntry()
: entry;
return Promise.resolve(file);
}
function handleFile(entry) {
return new Promise(function(resolve) {
if (entry.isFile) {
entry.file(function(file) {
listFile(file, entry.fullPath).then(resolve)
})
} else if (entry.isDirectory) {
var reader = entry.createReader();
reader.readEntries(webkitReadDirectories.bind(null, entry, handleFile, resolve))
} else {
var entries = [entry];
return entries.reduce(function(promise, file) {
return promise.then(function() {
return listDirectory(file)
})
}, Promise.resolve())
.then(function() {
return Promise.all(entries.map(function(file) {
return listFile(file)
})).then(resolve)
})
}
})
function webkitReadDirectories(entry, callback, resolve, entries) {
console.log(entries);
return listDirectory(entry).then(function(currentDirectory) {
console.log(`iterating ${currentDirectory.name} directory`, entry);
return entries.reduce(function(promise, directory) {
return promise.then(function() {
return callback(directory)
});
}, Promise.resolve())
}).then(resolve);
}
}
function listDirectory(entry) {
console.log(entry);
var path = (entry.fullPath || entry.webkitRelativePath.slice(0, entry.webkitRelativePath.lastIndexOf("/")));
var cname = path.split("/").filter(Boolean).join("-");
console.log("cname", cname)
if (!document.getElementsByClassName(cname).length) {
var directoryInfo = `<li><ul class=${cname}>
<li>
<span>
Directory Name: ${entry.name}<br>
Path: ${path}
<hr>
</span>
</li></ul></li>`;
var curr = document.getElementsByTagName("ul");
var _ul = curr[curr.length - 1];
var _li = _ul.querySelectorAll("li");
if (!document.querySelector("[class*=" + cname + "]")) {
if (_li.length) {
_li[_li.length - 1].innerHTML += `${directoryInfo}`;
} else {
_ul.innerHTML += `${directoryInfo}`
}
} else {
ul.innerHTML += `${directoryInfo}`
}
}
return Promise.resolve(entry);
}
function listFile(file, path) {
path = path || file.webkitRelativePath || "/" + file.name;
var filesInfo = `<li>
Name: ${file.name}</br>
Size: ${file.size} bytes</br>
Type: ${file.type}</br>
Modified Date: ${file.lastModifiedDate}<br>
Full Path: ${path}
</li>`;
var currentPath = path.split("/").filter(Boolean);
currentPath.pop();
var appended = false;
var curr = document.getElementsByClassName(`${currentPath.join("-")}`);
if (curr.length) {
for (li of curr[curr.length - 1].querySelectorAll("li")) {
if (li.innerHTML.indexOf(path.slice(0, path.lastIndexOf("/"))) > -1) {
li.querySelector("span").insertAdjacentHTML("afterend", `${filesInfo}`);
appended = true;
break;
}
}
if (!appended) {
curr[curr.length - 1].innerHTML += `${filesInfo}`;
}
}
console.log(`reading ${file.name}, size: ${file.size}, path:${path}`);
processedFiles.push(file);
return Promise.resolve(processedFiles)
};
function processFiles(files) {
Promise.all([].map.call(files, function(file, index) {
return handleEntry(file, index).then(handleFile)
}))
.then(function() {
console.log("complete", processedFiles)
})
.catch(function(err) {
alert(err.message);
})
}
var files;
if (event.type === "drop" && event.target.webkitdirectory) {
files = event.dataTransfer.items || event.dataTransfer.files;
} else if (event.type === "change") {
files = event.target.files;
}
if (files) {
processFiles(files)
}
}
dropArea.addEventListener("dragover", dragHandler);
dropArea.addEventListener("change", filesDroped);
dropArea.addEventListener("drop", filesDroped);
</script>
</body>
</html>
实时演示:https://plnkr.co/edit/hUa7zekNeqAuwhXi
兼容性问题/注意事项:
旧文本(现已编辑):
Firefoxdrop
事件没有将选择列表为Directory
,但File
对象具有size
0
,因此在Firefox中删除目录不提供删除文件夹的表示,即使使用event.dataTransfer.getFilesAndDirectories()
。这是在Firefox 50中修复的,它添加了
webkitGetAsEntry
支持(changelog, issue)。Firefox曾经在
<input type="file">
(HTMLInputElement
)上有.getFilesAndDirectories()
功能(在本次提交中添加)。它只有在设置about:config
首选项dom.input.dirpicker
时才可用(它只在Firefox Nightly中打开,在Firefox 101中再次删除,请参阅下面的其他要点)。在这次提交中,它再次被删除(仅用于测试)。查看
webkitdirectory
和HTMLInputElement.getFilesAndDirectories()
的历史。旧文本:
当设置allowdirs
属性时,Firefox提供两个输入元素;第一个元素允许单个文件上传,第二个元素允许目录上传。chrome/chromium提供单个<input type="file">
元素,其中只能选择单个或多个目录,不能选择单个文件。allowdirs
特性在Firefox 101中被删除(代码,issue)。在此之前,它可以通过关闭默认的about:config
设置dom.input.dirpicker
来使用。它在Firefox 50 (code, issue)中是默认关闭的。在此之前,它仅在Firefox Nightly中是默认开启的。这意味着现在,Firefox会忽略
allowdirs
属性,当点击Choose file
按钮时,它会显示一个仅目录选择器(与Chrome相同的行为)。<input type="file">
的webkitdirectory
功能目前在中除:- Android WebView
- non-Edge IE
DataTransferItem.webkitGetAsEntry()
目前在除:- Android上的Firefox
- non-Edge IE
DataTransferItem.webkitGetAsEntry()
docs说:此函数目前在包括Firefox在内的非webkit浏览器中作为
webkitGetAsEntry()
实现;它可能在将来被重命名为getAsEntry()
,所以你应该谨慎地编写代码,寻找两者。