文件拖放
正如着陆页上所提到的,拖放 API 同时模拟了三种用例:在页面内拖动元素,将数据拖出页面,以及将数据拖入页面。本教程演示了第三种用例:将数据拖入页面。我们将实现一个基本的放置区域,允许用户从操作系统的文件浏览器中拖放图片文件,并在页面上显示它们。对于不能或不想使用拖放的用户,我们也提供了通过<input> 元素进行文件选择的替代功能。
页面基本布局
因为我们希望允许正常的<input> 文件选择,所以让放置区域由<input> 元素支持是有意义的,这样我们就可以同时拖放到它上面并点击它。我们利用了一个常见的技巧,就是让<input> 不可见,并使用与之关联的<label> 来与用户交互,因为<label> 元素更容易进行样式设置。我们还添加了用于预览拖放图片的元素。
<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 元素设置样式,以直观地指示该元素是一个放置区域,并隐藏文件输入框。
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 事件来处理放置的文件。
const dropZone = document.getElementById("drop-zone");
dropZone.addEventListener("drop", dropHandler);
对于文件放置,浏览器可能会默认处理它们(例如打开或下载文件),即使文件没有被拖放到有效的放置目标。为了阻止这种行为,我们还需要监听window 上的drop 事件并取消它。我们小心地只处理有文件被拖动的情况;如果拖动的是其他东西,例如链接,我们仍然会使用默认行为。如果拖动的项是非图片文件,我们仍然会处理该事件,但会向用户提供反馈,表明不允许该文件。
window.addEventListener("drop", (e) => {
if ([...e.dataTransfer.items].some((item) => item.kind === "file")) {
e.preventDefault();
}
});
为了使drop 事件触发,该元素还必须取消dragover 事件。因为我们监听的是window 上的drop 事件,所以我们也需要取消整个window 的dragover 事件。如果文件不是图片或没有被拖动到正确的位置,我们还将DataTransfer.dropEffect 设置为none。
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";
}
}
});
处理放置
现在我们通过使用getAsFile() 方法来访问每个文件来实现dropHandler。然后,您的应用程序可以使用File API 决定如何处理该文件。这里我们只是在页面上显示它们;在实际应用中,您可能还希望最终将它们上传到服务器。
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() 函数也连接到文件输入框。
const fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", (e) => {
displayImages(e.target.files);
});
清除按钮
最后,我们添加了一种清除预览区域的方法。我们使用URL.revokeObjectURL() 来释放图像对象使用的内存。
const clearBtn = document.getElementById("clear-btn");
clearBtn.addEventListener("click", () => {
for (const img of preview.querySelectorAll("img")) {
URL.revokeObjectURL(img.src);
}
preview.textContent = "";
});