HTML5 Drag and Drop API

在使用 Drag and Drop API 以前,要在網頁上實作拖曳一般會使用 mousedownmousemovemouseup 三種事件來完成,拖曳(drag)這個動作可以想成是在 mousedown 的情況下同時進行 mousemove ,而 mouseup 會拿來結束拖曳。

由於這三個事件並不是特別為了處理拖曳而生,這種手段雖然行得通,但在撰寫邏輯時必須很小心,否則 Bug 解起來會很痛苦。

因此研究了 HTML 5 特別為了處理拖曳而生 Drag and Drop API ,希望未來處理拖曳時可以好受點,以下開始重點整理。

拖曳事件與對象

HTML5 提供多種用來觸發的拖曳事件,在使用拖曳事件前得先了解拖曳事件所監聽的對象。

拖曳的概念分成「拖曳對象」(Drag Source)和「放置目標」(Drop Target)。
拖曳對象就是被拖曳的元素,放置目標就是拖曳對象最終會著陸的目標元素。

有些事件是以拖曳對象為目標、有些事件是以放置目標為目標。最常用的大致如下:

拖曳對象的事件(Drag Source Event)

dragstart :拖曳對象最一開始被拖曳時觸發(點擊時還沒,按住不放也還沒,按住不放開始拖的瞬間觸發)
drag:在拖曳對象被拖動的期間持續觸發

放置目標的事件(Dop Target Event)

dragenter:當拖曳對象首次進入放置目標的範圍時觸發
dragover :只要拖曳對象是在放置目標的範圍內,會持續觸發
drop :當拖曳對象在放置目標的範圍內被釋放時觸發

實作簡單的拖曳

基本樣板如下, target 會被當作放置目標, source 會被當作拖曳對象。

1
2
3
4
<div class="target">
<div id="source" class="source"></div>
</div>
<div class="target"></div>

設置拖曳對象

做為拖曳對象的元素,必須將 draggable 屬性設為 true

1
<div class="source" draggable="true"></div>

設置監聽事件

對拖曳對象監聽 dragstart 事件,對放置目標監聽 dragenterdragoverdrop 三個事件。

1
2
3
4
5
6
7
8
9
10
11
// 連結 DOM
let dragSource = document.querySelector('.source');
let dropTargets = document.querySelectorAll('.target');

// 設置監聽事件
dragSource.addEventListener('dragstart', dragStart);
dropTargets.forEach(target => {
target.addEventListener('dragenter', cancelDefault)
target.addEventListener('dragover', cancelDefault)
target.addEventListener('drop', drop);
})

設置回呼函式

當拖曳對象剛被拖動時,就把拖曳對象的 id 放進要傳送的資料內。

1
2
3
function dragStart(e) {
e.dataTransfer.setData('text/plain', e.target.id)
}

當拖曳對象放置目標上被釋放時,就把 id 從資料中取出來,然後利用 id 把 DOM 抓過來。

1
2
3
4
function drop(e) {
let id = e.dataTransfer.getData('text/plain');
e.target.append(document.getElementById(`${id}`));
}

元素預設行為是不能被放置拖曳物的,因此在拖曳對象出現在放置目標上時,取消預設行為,讓放置目標可以被放置。

1
2
3
4
5
function cancelDefault(e) {
e.preventDefault();
e.stopPropagation();
return false;
}

完成上述,就可以在兩個目標間來回拖曳東西。

See the Pen Simple Drag and Drop by Arel (@godlike0108) on CodePen.

細節補充

dataTransfer

上述實作時用到的 dataTransfer 物件,主要用來執行「拖曳對象」和「放置目標」間的資料傳遞。

dataTransfer 提供了 setDatagetData 兩種方法傳遞資料, setData 負責決定要從拖曳對象發送的資料,因此多寫在 dragstart 的回呼函式內。

setData 第一個參數為資料類型,支援常用的 MIME ,第二個參數為資料內容,同樣類型的資料只能指定一份內容,指定第二份會蓋過第一份。

getData 負責接收資料,因此多與對應放置目標的事件 drop 一起使用。 getData 的唯一參數式資料類型,必須跟 setData 類型相同才收得到對應的資料。

使用 setDatagetData 的良好習慣是,越特殊的資料類型優先寫,像 text/plain 這種支援度最高的類型則寫在最後面,如此才能確保資料可以被傳送。

拖曳效果(Drag Effects)

拖曳效果定義了常見的拖曳操作,有 copymovelink 三種,有了被定義的拖曳效果,瀏覽器也會根據不同的拖曳操作顯示不同的提示。

dataTransfereffectAlloweddropEffect 屬性可以用來操作拖曳效果。只有當該放置目標的 dropEffect 與 拖曳對象的 effectAllowed 匹配時,才允許 drop

通常我們會在拖曳對象的 dragstart 時使用 dataTransfer.effectAllowed 限制該拖曳對象只有在哪種效果下才會拖曳成功。

然後在放置目標的 dragover 使用 dataTransfer.dropEffect 指定該放置目標的放置效果。

註: dropEffect 在 Chrome 瀏覽器有個 bug ,就是它只會顯示 none ,但並不影響實作,只是用 console 看可能會誤判。

Drag and Drop 清單

附上用 Drag and Drop API 實作的拖曳清單。

See the Pen Drag and Drop List by Arel (@godlike0108) on CodePen.

結論

比起使用舊的 mouse events 來說簡便很多,但由於是直接操作 DOM ,在和資料驅動的框架一起使用時(例如 Vue),邏輯上比較不直覺。

Reference

MDN Drag and Drop 教學