如何使用拖放事件在Firefox和Chrome/Chromium上上传并列出目录

19

现在Mozilla和Webkit浏览器都支持目录上传。当在<input type="file">元素选择目录或多个目录或将它们拖到一个元素上时,如何按照实际目录中的顺序列出所有目录和文件,并在迭代完所有上传的目录后对文件执行任务,在Firefox和Chrome/Chromium上实现?


你还缺少哪些信息? - K Scandrett
@KScandrett 我不知道如何从Firefox或Chromium的目录中创建文件数组。答案中的listDirectory()listFile()函数可能仍然需要改进,以便在每个浏览器中获取和列出准确的结果。当我最后一次处理这些函数时,如果我记得正确的话,很难获得包含目录的文件对象的正确父目录,其中包含文件的目录也包含目录。在答案中有一个TODO,主要是尝试在Chromium中使用listDirectories()listFiles() - guest271314
如果您已经开发出不同于当前答案的方法以满足要求,请发布您自己的答案。 - guest271314
@KScandrett 决定不选择“必须有规范答案”的选项,因为已经自己回答了如何将文件作为单个数组部分获取的问题;这样可以潜在地查看其他 SO 观众和用户如何处理将文件作为单个数组获取并创建呈现目录和文件树的要求,在 HTML 中准确反映上传文件夹的目录树,并在 Chromium/Chrome 和 Firefox 浏览器中保持一致。 - guest271314
1个回答

11
简要概述:您可以在元素上设置webkitdirectory属性;将change、drop事件附加到它上面;使用.createReader()、.readEntries()获取所有选定/拖放的文件和文件夹,并使用例如Array.prototype.reduce()、Promise和递归对它们进行迭代。
请注意,这里实际上有两个不同的API:
  1. <input type="file">webkitdirectory特性和它的change事件。
    • 该API不支持空文件夹,它们会被跳过。
  2. DataTransferItem.webkitGetAsEntry()与其drop事件,它是拖放API的一部分。
    • 该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>

实时演示:https://plnkr.co/edit/hUa7zekNeqAuwhXi

兼容性问题/注意事项:

旧文本(现已删除):Firefox的drop事件不将选择列为Directory,而是将其作为File对象列出,该对象具有size0,因此在firefox中放置目录不提供已删除文件夹的表示,即使使用了event.dataTransfer.getFilesAndDirectories()

这个问题在Firefox 50中得到了解决,它添加了webkitGetAsEntry支持(changelogissue)。

Firefox曾经在<input type="file">HTMLInputElement)上拥有.getFilesAndDirectories()函数(添加于this commitissue)。只有在设置了dom.input.dirpicker首选项(仅在Firefox Nightly中启用,并在Firefox 101中再次删除,见下面的其他点)时才可用。它在this commit中被移除(仅用于测试)。
请查看this post,了解webkitdirectoryHTMLInputElement.getFilesAndDirectories()的历史。
旧文本(现已删除):Firefox在设置了allowdirs属性时提供了两个输入元素;第一个元素允许单个文件上传,第二个元素允许目录上传。Chrome/Chromium提供单个<input type="file">元素,只能选择单个或多个目录,而不能选择单个文件。
在Firefox 101中删除了allowdirs功能(codeissue)。在此之前,它通过一个关闭的about:config设置dom.input.dirpicker可用。在Firefox 50中变为默认关闭:(codeissue)。在此之前,它仅在Firefox Nightly中默认启用。
这意味着现在,Firefox忽略allowdirs属性,在点击Choose file按钮时,它显示一个仅限目录的选择器(与Chrome的行为相同)。 <input type="file">webkitdirectory功能当前在除以下地方外的所有地方都可以使用except
- Android WebView - 非Edge IE DataTransferItem.webkitGetAsEntry()目前在除以下地方外的所有地方都可以使用except
- Firefox on Android - 非Edge IE DataTransferItem.webkitGetAsEntry()文档说:

这个函数在非WebKit浏览器中包括Firefox在内是实现为webkitGetAsEntry()的;它可能在将来被重命名为getAsEntry(),因此您应该进行防御性编码,同时查找两者。


请参阅文件和目录条目API - guest271314
allowdirs 功能已在 Firefox 101 中被移除代码问题)。在此之前,它可以通过默认关闭的 about:config 设置 dom.input.dirpicker 进行使用。 它在 Firefox 50 中被默认关闭:(代码问题)。 在此之前,它仅在 Firefox Nightly 中默认开启。 - nh2
链接相关问题和答案:https://dev59.com/RlgQ5IYBdhLWcg3whEVn - nh2
请注意,在Firefox下的拖放事件中不会将选定内容列为目录,而是作为大小为0的文件对象。因此,在Firefox下进行文件夹的拖放操作时无法提供拖放文件夹的表示形式。我认为这已经过时: Firefox 50增加了webkitGetAsEntry支持(changelog),比您原始答案晚了2个月。 - nh2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接