HTML5 Drag and Drop API

本文將說明拖曳和放置功能的基本概念。

建立可拖曳的內容

在大多數瀏覽器中,系統預設可拖曳文字選取項目、圖片和連結。舉例來說,如果您在網頁上拖曳連結,就會看到一個小方塊,其中包含標題和網址,您可以將其放到網址列或電腦桌面上,建立捷徑或前往連結。如要讓其他類型的內容可拖曳,您必須使用 HTML5 拖曳和放置 API。

如要讓物件可拖曳,請在該元素上設定 draggable=true。幾乎所有項目都能拖曳,包括圖片、檔案、連結、檔案或頁面上的任何標記。

以下範例會建立介面,用於重新排列已使用 CSS 格線排版的資料欄。欄的基本標記如下所示,每個欄的 draggable 屬性都設為 true

<div class="container">
  <div draggable="true" class="box">A</div>
  <div draggable="true" class="box">B</div>
  <div draggable="true" class="box">C</div>
</div>

以下是容器和方塊元素的 CSS。與拖曳功能相關的 CSS 只有 cursor: move 屬性。程式碼的其餘部分會控制容器和方塊元素的版面配置和樣式。

.container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
}

.box {
  border: 3px solid #666;
  background-color: #ddd;
  border-radius: .5em;
  padding: 10px;
  cursor: move;
}

此時您可以拖曳項目,但不會發生其他動作。如要新增行為,您必須使用 JavaScript API。

監聽拖曳事件

如要監控拖曳程序,您可以監聽下列任一事件:

如要處理拖曳流程,您需要某種來源元素 (拖曳開始的位置)、資料酬載 (拖曳的項目) 和目標 (接收拖曳的區域)。來源元素幾乎可以是任何類型的元素。目標是接受使用者要放置的資料的放置區或放置區組合。並非所有元素都能做為目標。舉例來說,目標不得是圖片。

開始及結束拖曳序列

在內容上定義 draggable="true" 屬性後,請附加 dragstart 事件處理常式,以便啟動每個資料欄的拖曳序列。

這段程式碼會在使用者開始拖曳時將資料欄的透明度設為 40%,然後在拖曳事件結束時將其設為 100%。

function handleDragStart(e) {
  this.style.opacity = '0.4';
}

function handleDragEnd(e) {
  this.style.opacity = '1';
}

let items = document.querySelectorAll('.container .box');
items.forEach(function (item) {
  item.addEventListener('dragstart', handleDragStart);
  item.addEventListener('dragend', handleDragEnd);
});

您可以在下列 Glitch 示範中看到結果。拖曳項目時,其不透明度會變化。由於來源元素具有 dragstart 事件,因此將 this.style.opacity 設為 40% 可向使用者提供視覺回饋,讓他們知道該元素是目前正在移動的選取項目。即使您尚未定義放置行為,當您放置項目時,來源元素仍會恢復 100% 不透明度。

新增其他視覺提示

如要協助使用者瞭解如何與介面互動,請使用 dragenterdragoverdragleave 事件處理常式。在這個範例中,除了可拖曳之外,資料欄也是放置目標。當使用者將拖曳的項目懸停在資料欄上時,讓邊框顯示虛線,有助於使用者瞭解這項功能。舉例來說,您可以在 CSS 中為元素建立 over 類別,以便做為放置目標:

.box.over {
  border: 3px dotted #666;
}

接著,在 JavaScript 中設定事件處理常式,在拖曳資料欄時新增 over 類別,並在拖曳元素離開時移除該類別。在 dragend 處理常式中,我們也會確保在拖曳結束時移除類別。

document.addEventListener('DOMContentLoaded', (event) => {

  function handleDragStart(e) {
    this.style.opacity = '0.4';
  }

  function handleDragEnd(e) {
    this.style.opacity = '1';

    items.forEach(function (item) {
      item.classList.remove('over');
    });
  }

  function handleDragOver(e) {
    e.preventDefault();
    return false;
  }

  function handleDragEnter(e) {
    this.classList.add('over');
  }

  function handleDragLeave(e) {
    this.classList.remove('over');
  }

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });
});

這個程式碼中有一些值得注意的重點:

  • dragover 事件的預設動作是將 dataTransfer.dropEffect 屬性設為 "none"。我們會在本頁稍後的部分說明 dropEffect 屬性。目前,您只需知道這會防止 drop 事件觸發即可。如要覆寫這項行為,請呼叫 e.preventDefault()。另一個最佳做法是在相同的處理常式中傳回 false

  • dragenter 事件處理常式用於切換 over 類別,而非 dragover。如果您使用 dragover,當使用者將拖曳的項目放在資料欄上時,事件會重複觸發,導致 CSS 類別重複切換。這會導致瀏覽器執行許多不必要的轉譯作業,進而影響使用者體驗。我們強烈建議您盡量減少重繪作業,如果您需要使用 dragover,請考慮調節或取消事件監聽器的偵測動作

完成投放

如要處理放置動作,請為 drop 事件新增事件監聽器。在 drop 處理常式中,您必須避免瀏覽器的預設掉落行為,這通常是某種惱人的重新導向。如要執行此操作,請呼叫 e.stopPropagation()

function handleDrop(e) {
  e.stopPropagation(); // stops the browser from redirecting.
  return false;
}

請務必將新處理常式與其他處理常式一起註冊:

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });

如果您在此時執行程式碼,項目就不會放到新位置。如要實現這項功能,請使用 DataTransfer 物件。

dataTransfer 屬性會保留在拖曳動作中傳送的資料。dataTransfer 會在 dragstart 事件中設定,並在 drop 事件中讀取或處理。呼叫 e.dataTransfer.setData(mimeType, dataPayload) 可讓您設定物件的 MIME 類型和資料酬載。

在這個範例中,我們會讓使用者重新排列資料欄的順序。為此,您必須先在拖曳開始時儲存來源元素的 HTML:

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

drop 事件中,您可以將來源資料欄的 HTML 設為資料放置目標資料欄的 HTML,藉此處理資料欄放置作業。這包括檢查使用者是否將項目放回拖曳來源的同一欄。

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

您可以在下列示範中看到結果。如要使用這項功能,您必須使用電腦版瀏覽器。行動裝置不支援拖曳 API。將 A 欄拖曳到 B 欄上方並放開,並注意兩者如何互換位置:

更多拖曳屬性

dataTransfer 物件會公開屬性,在拖曳程序期間向使用者提供視覺回饋,並控制每個放置目標如何回應特定資料類型。

  • dataTransfer.effectAllowed 會限制使用者可在元素上執行的「拖曳類型」。在拖曳處理模式中使用,可在 dragenterdragover 事件期間初始化 dropEffect。這個屬性可使用以下值:nonecopycopyLinkcopyMovelinklinkMovemovealluninitialized
  • dataTransfer.dropEffect 會控制使用者在 dragenterdragover 事件期間收到的意見回饋。當使用者將游標懸停在目標元素上時,瀏覽器的游標會指出要執行的作業類型,例如複製或移動。效果可採用下列任一值:nonecopylinkmove
  • e.dataTransfer.setDragImage(imgElement, x, y) 表示您可以設定拖曳圖示,而非使用瀏覽器的預設「模糊圖片」意見回饋。

上傳檔案

這個簡單的範例會同時使用資料欄做為拖曳來源和拖曳目標。這種情況可能會發生在要求使用者重新排列項目的 UI 中。在某些情況下,拖曳目標和來源可能為不同的元素類型,例如在使用者需要拖曳所選圖片至目標,以便選取一張圖片做為產品主要圖片的介面中。

拖曳功能經常用於讓使用者將電腦桌面上的項目拖曳至應用程式。主要差異在於 drop 處理常式。這些檔案的資料並未使用 dataTransfer.getData() 存取,而是包含在 dataTransfer.files 屬性中:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; (f = files[i]); i++) {
    // Read the File objects in this FileList.
  }
}

如需進一步瞭解這項功能,請參閱「自訂拖曳」一文。

其他資源