文件拖放

正如着陆页上所提到的,拖放 API 同时模拟了三种用例:在页面内拖动元素,将数据拖出页面,以及将数据拖入页面。本教程演示了第三种用例:将数据拖入页面。我们将实现一个基本的放置区域,允许用户从操作系统的文件浏览器中拖放图片文件,并在页面上显示它们。对于不能或不想使用拖放的用户,我们也提供了通过<input> 元素进行文件选择的替代功能。

页面基本布局

因为我们希望允许正常的<input> 文件选择,所以让放置区域由<input> 元素支持是有意义的,这样我们就可以同时拖放到它上面并点击它。我们利用了一个常见的技巧,就是让<input> 不可见,并使用与之关联的<label> 来与用户交互,因为<label> 元素更容易进行样式设置。我们还添加了用于预览拖放图片的元素。

html
<label id="drop-zone">
  Drop images here, or click to upload.
  <input type="file" id="file-input" multiple accept="image/*" />
</label>
<ul id="preview"></ul>
<button id="clear-btn">Clear</button>

我们为 label 元素设置样式,以直观地指示该元素是一个放置区域,并隐藏文件输入框。

css
body {
  font-family: "Arial", sans-serif;
}

#drop-zone {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 500px;
  max-width: 100%;
  height: 200px;
  padding: 1em;
  border: 1px solid #cccccc;
  border-radius: 4px;
  color: slategray;
  cursor: pointer;
}

#file-input {
  display: none;
}

#preview {
  width: 500px;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.5em;
  list-style: none;
  padding: 0;
}

#preview li {
  display: flex;
  align-items: center;
  gap: 0.5em;
  margin: 0;
  width: 100%;
  height: 100px;
}

#preview img {
  width: 100px;
  height: 100px;
  object-fit: cover;
}

由于我们使用了<label><input> 元素,因此无需额外的 JavaScript 来实现文件选择的用户体验。现在我们专注于文件放置和随后对放置文件的处理。

声明放置目标

我们的放置目标是<label> 元素。作为目标元素,它会监听drop 事件来处理放置的文件。

js
const dropZone = document.getElementById("drop-zone");

dropZone.addEventListener("drop", dropHandler);

对于文件放置,浏览器可能会默认处理它们(例如打开或下载文件),即使文件没有被拖放到有效的放置目标。为了阻止这种行为,我们还需要监听window 上的drop 事件并取消它。我们小心地只处理有文件被拖动的情况;如果拖动的是其他东西,例如链接,我们仍然会使用默认行为。如果拖动的项是非图片文件,我们仍然会处理该事件,但会向用户提供反馈,表明不允许该文件。

js
window.addEventListener("drop", (e) => {
  if ([...e.dataTransfer.items].some((item) => item.kind === "file")) {
    e.preventDefault();
  }
});

为了使drop 事件触发,该元素还必须取消dragover 事件。因为我们监听的是window 上的drop 事件,所以我们也需要取消整个windowdragover 事件。如果文件不是图片或没有被拖动到正确的位置,我们还将DataTransfer.dropEffect 设置为none

js
dropZone.addEventListener("dragover", (e) => {
  const fileItems = [...e.dataTransfer.items].filter(
    (item) => item.kind === "file",
  );
  if (fileItems.length > 0) {
    e.preventDefault();
    if (fileItems.some((item) => item.type.startsWith("image/"))) {
      e.dataTransfer.dropEffect = "copy";
    } else {
      e.dataTransfer.dropEffect = "none";
    }
  }
});

window.addEventListener("dragover", (e) => {
  const fileItems = [...e.dataTransfer.items].filter(
    (item) => item.kind === "file",
  );
  if (fileItems.length > 0) {
    e.preventDefault();
    if (!dropZone.contains(e.target)) {
      e.dataTransfer.dropEffect = "none";
    }
  }
});

注意: 当从操作系统将文件拖入浏览器时,不会触发dragstartdragend 事件。要检测从操作系统拖动文件到浏览器,请使用dragenterdragleave。这意味着无法使用setDragImage() 应用自定义拖动图像/光标叠加层来拖动来自操作系统的文件 — 因为拖动数据存储只能在dragstart 事件中修改。这也适用于setData()

处理放置

现在我们通过使用getAsFile() 方法来访问每个文件来实现dropHandler。然后,您的应用程序可以使用File API 决定如何处理该文件。这里我们只是在页面上显示它们;在实际应用中,您可能还希望最终将它们上传到服务器。

js
const preview = document.getElementById("preview");

function displayImages(files) {
  for (const file of files) {
    if (file.type.startsWith("image/")) {
      const li = document.createElement("li");
      const img = document.createElement("img");
      img.src = URL.createObjectURL(file);
      img.alt = file.name;
      li.appendChild(img);
      li.appendChild(document.createTextNode(file.name));
      preview.appendChild(li);
    }
  }
}

function dropHandler(ev) {
  ev.preventDefault();
  const files = [...ev.dataTransfer.items]
    .map((item) => item.getAsFile())
    .filter((file) => file);
  displayImages(files);
}

将相同行为添加到输入框

以上是拖放的整个数据流;现在我们需要将displayImages() 函数也连接到文件输入框。

js
const fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", (e) => {
  displayImages(e.target.files);
});

清除按钮

最后,我们添加了一种清除预览区域的方法。我们使用URL.revokeObjectURL() 来释放图像对象使用的内存。

js
const clearBtn = document.getElementById("clear-btn");
clearBtn.addEventListener("click", () => {
  for (const img of preview.querySelectorAll("img")) {
    URL.revokeObjectURL(img.src);
  }
  preview.textContent = "";
});

结果

另见