如何在firefox和chrome/chromium中使用更改和删除事件上传和列出目录



mozilla和webkit浏览器现在都允许目录上传。当在<input type="file">元素中选择目录或在元素中删除目录时,如何按照firefox和chrome/chromium中实际目录中出现的顺序列出所有目录和文件,并在所有上传的目录迭代后对文件执行任务?

简短总结:可以在<input type="file">元素上设置webkitdirectory属性;附加changedrop事件;使用.createReader(), .readEntries()来获取所有选中/删除的文件和文件夹,并使用例如Array.prototype.reduce(), Promise和递归来迭代它们。

注意这里有两个不同的api:

  1. <input type="file">webkitdirectory特性及其change事件。
    • 这个API不支持空文件夹。他们被跳过了。
  2. 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

    兼容性问题/注意事项:

    • 旧文本(现已编辑):Firefox drop事件没有将选择列表为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中再次删除,请参阅下面的其他要点)。在这次提交中,它再次被删除(仅用于测试)。

    • 查看webkitdirectoryHTMLInputElement.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(),所以你应该谨慎地编写代码,寻找两者。

    最新更新