檔案拖放

正如著陸頁上所提到的,拖放 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 = "";
});

結果

另見