现在Mozilla和Webkit浏览器都支持目录上传。当在<input type="file">
元素选择目录或多个目录或将它们拖到一个元素上时,如何按照实际目录中的顺序列出所有目录和文件,并在迭代完所有上传的目录后对文件执行任务,在Firefox和Chrome/Chromium上实现?
现在Mozilla和Webkit浏览器都支持目录上传。当在<input type="file">
元素选择目录或多个目录或将它们拖到一个元素上时,如何按照实际目录中的顺序列出所有目录和文件,并在迭代完所有上传的目录后对文件执行任务,在Firefox和Chrome/Chromium上实现?
<input type="file">
的webkitdirectory
特性和它的change
事件。
DataTransferItem.webkitGetAsEntry()
与其drop
事件,它是拖放API的一部分。
尽管它们的名称中带有"webkit",但它们都可以在Firefox中使用。
它们都可以处理文件夹/目录层次结构。
如上所述,如果您需要支持空文件夹,则必须强制您的用户使用拖放而不是单击<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>
drop
事件不将选择列为Directory
,而是将其作为File
对象列出,该对象具有size
0
,因此在firefox中放置目录不提供已删除文件夹的表示,即使使用了event.dataTransfer.getFilesAndDirectories()
。
这个问题在Firefox 50中得到了解决,它添加了webkitGetAsEntry
支持(changelog,issue)。
<input type="file">
(HTMLInputElement
)上拥有.getFilesAndDirectories()
函数(添加于this commit,issue)。只有在设置了dom.input.dirpicker
首选项(仅在Firefox Nightly中启用,并在Firefox 101中再次删除,见下面的其他点)时才可用。它在this commit中被移除(仅用于测试)。webkitdirectory
和HTMLInputElement.getFilesAndDirectories()
的历史。allowdirs
属性时提供了两个输入元素;第一个元素允许单个文件上传,第二个元素允许目录上传。Chrome/Chromium提供单个<input type="file">
元素,只能选择单个或多个目录,而不能选择单个文件。allowdirs
功能(code,issue)。在此之前,它通过一个关闭的about:config
设置dom.input.dirpicker
可用。在Firefox 50中变为默认关闭:(code,issue)。在此之前,它仅在Firefox Nightly中默认启用。allowdirs
属性,在点击Choose file
按钮时,它显示一个仅限目录的选择器(与Chrome的行为相同)。
<input type="file">
的webkitdirectory
功能当前在除以下地方外的所有地方都可以使用except:DataTransferItem.webkitGetAsEntry()
目前在除以下地方外的所有地方都可以使用except:DataTransferItem.webkitGetAsEntry()
文档说:
这个函数在非WebKit浏览器中包括Firefox在内是实现为
webkitGetAsEntry()
的;它可能在将来被重命名为getAsEntry()
,因此您应该进行防御性编码,同时查找两者。
allowdirs
功能已在 Firefox 101 中被移除(代码,问题)。在此之前,它可以通过默认关闭的 about:config
设置 dom.input.dirpicker
进行使用。
它在 Firefox 50 中被默认关闭:(代码,问题)。
在此之前,它仅在 Firefox Nightly 中默认开启。 - nh2webkitGetAsEntry
支持(changelog),比您原始答案晚了2个月。 - nh2
listDirectory()
和listFile()
函数可能仍然需要改进,以便在每个浏览器中获取和列出准确的结果。当我最后一次处理这些函数时,如果我记得正确的话,很难获得包含目录的文件对象的正确父目录,其中包含文件的目录也包含目录。在答案中有一个TODO
,主要是尝试在Chromium中使用listDirectories()
和listFiles()
。 - guest271314