HTML 5 Drag and Drop 入門教程
在 HTML 5 之前,想要實現 Drag and Drop(拖拽/拖放)一般需要求助於 JQuery,所幸 HTML 5 已經把 DnD 標準化,現在我們能“輕易”地為幾乎任意元素實現拖放功能。只是它的難度取決於你對 API 的理解程度,而官方文件並不好懂。這篇文章會一步步帶你瞭解它的 API。
最終效果如下:
#拖動事件
繼續之前,有必要先了解拖動時會觸發哪些事件。考慮拖動 Source Element,途中經過 Intermediate Element,最終進入 Target Element 並鬆開滑鼠,則路徑上會觸發的事件如下圖所示:
這些事件的具體內容下面會講到,你可以先跳過之後再回來檢視,簡單來說:
dragstart
:當我們“拖”起元素時會觸發。dragenter
:當拖動元素 A 進入另一個元素 B 時,會觸發 B 的dragenter
事件。dragleave
:與dragenter
相對應,當拖動元素 A 離開元素 B 時,觸發 B 的dragleave
事件。dragover
:當拖動元素 A 在另一個元素 B 中移動/停止時觸發 B 的dragover
事件。文件說是每幾百毫秒觸發一次,Chrome 實測 1ms 左右觸發;Firefox 大概是 300msdrop
:當在拖動元素 A 到元素 B 上,釋放滑鼠時觸發 B 的drop
事件,相當於元素 B 接收了元素 A 。dragend
:在drop
事件之後,還會觸發元素 A 的dragend
事件,這裡可以對元素 A 作一些清理工作。
除了上面的事件外,還有兩個一般用不到的事件:
drag
:和dragover
類似,當元素 A 被拖動時,每隔一段時間就會觸發這個事件。與dragover
不同,drag
事件是觸發在源元素 A 上,而dragover
是觸發上潛在目標元素 B 上的。dragexit
:這個事件只有 Firefox 支援,和dragleave
作用幾乎相同,發生在dragleave
之前。
如果想實際驗證一下這些事件是何時觸發的,可以看看這個 jsfiddle,console 裡會輸出拖放的元素及對應的事件。下面我們開始一起實現咱們的拖放示例吧。
#讓元素可拖放
一般在 HTML 裡,元素預設是不可以作為源元素的(除了 <a>
,<img>
),例如一個div
,我們是“拖不動”它的。這時只需要為它加上 draggable="true"
屬性它就能“拖”了。下面是我們的 DOM 結構:
<div id="drag-container"> <div class="dropzone"> <div id="draggable" draggable="true"> Drag Me </div> </div> <div class="dropzone"></div> <div class="dropzone"></div></div> |
draggable
元素上加了 draggable="true"
,這樣我們就能拖動它了,起碼在 Chrome
裡可以,在 Firefox 裡我們還需要在 dragstart
裡為 dataTransfer
設定一些資料,因此需要加上下面的程式碼。具體的作用我們之後會說。
let draggable = document.getElementById('draggable');draggable.addEventListener('dragstart', (ev) => { ev.dataTransfer.setData('text/plain', null);}); |
於是效果如下(CSS 沒有貼出):
這樣紅色的 Drag Me
元素就可以拖動了。下面我們增加一些拖動時的反饋,讓互動更真實。
#新增拖動特效
首先,我們想在拖起元素讓原始的元素變成半透明,這樣當我們拖動時就會知道它是“真的可以拖動的”,而不是瀏覽器的什麼奇怪行為。為此,我們可以監聽 dragstart
事件:
draggable.addEventListener("dragstart", (ev) => { ev.target.style.opacity = ".5";}); |
這樣一來我們開始拖動元素,它就變得透明瞭,然而我們鬆開滑鼠,它依舊保持透明!這可不是我們想要的結果,因此我們需要監聽 dragend
在拖動結束後還原透明度:
draggable.addEventListener("dragend", (ev) => { ev.target.style.opacity = "";}); |
下面,我們希望拖著元素 A 進入目標 B 時讓 B 的邊框變成虛線,以示意我們可以放入元素。
let dropzones = document.querySelectorAll('.dropzone');dropzones.forEach((dropzone) => { dropzone.addEventListener('dragenter', (ev) => { ev.preventDefault(); dropzone.style.borderStyle = 'dashed'; return false; }); dropzone.addEventListener('dragover', (ev) => { ev.preventDefault(); return false; }); dropzone.addEventListener('dragleave', (ev) => { dropzone.style.borderStyle = 'solid'; });}); |
我們為所有的 dropzone
都監聽了 dragenter
及 dragleave
事件,當拖動元素進入它們時,邊框會變成虛線,離開時變回實線。這裡有幾個注意點:
- 在
dragenter
與dragover
裡我們呼叫了ev.preventDefault()
,事實上幾乎所有元素預設都是不允許 drop 發生的,這裡呼叫ev.preventDefault()
可以阻止預設行為。 - 在
dragenter
中我們通過dropzone
變數來修改樣式而不是ev.target
,你可能覺得ev.target
指向的是目標 B 元素,然而它指向的是源元素 A。 - 我們在
dragenter
而不是dragover
中修改樣式,是因為dragover
會觸發太頻繁了。
我們完成了“拖”的操作,最後需要完成“放”的操作了。
#資料傳輸 DataTransfer
拖動是最終目的是為了對源和目標元素做一些操作。為了完成操作,需要在源和目標傳輸資料,我們可以通過設定/讀取全域性變數來完成,這並不是一個好習慣。在 HTML 5 中,我們通過 DataTransfer 完成。
我們在 dragstart
時設定需要傳輸的資料,在 drop 中獲取需要的資料。
event.dataTransfer
提供了兩個主要函式:
setData(format, data)
:用於新增資料,一般 format 對應於 MIME 型別字串,常見的有text/plain
、text/html
及text/uri-list
等,但同時也可以是任意自定義的型別;不幸的是 data 只能是string
或file
。getData(format)
:用於獲取資料。
我們要實現將 Drag Me
放到其它藍色元素中,需要傳輸它的 ID ,通過下面的程式碼實現:
draggable.addEventListener('dragstart', (ev) => { ev.target.style.opacity = ".5"; // 設定 ID ev.dataTransfer.setData('text/plain', ev.target.id);});dropzones.forEach((dropzone) => { dropzone.addEventListener('drop', (ev) => { ev.preventDefault() ev.target.style.borderStyle = 'solid'; // 獲取 ID const sourceId = ev.dataTransfer.getData('text/plain') ev.target.appendChild(document.getElementById(sourceId)) })}); |
- 在
dragstart
時通過setData
將 ID 放入DataTransfer
中 - 在
drop
事件中,通過getData
獲取元素 ID 並通過appendChild
加入到藍色元素中。
至此我們的簡單示例就結束了,為了實現這麼一個簡單的示例,我們用到了全部的 6 個事件。因此從入門的角度來說 DnD API 並不容易,但換句話說這也就是它的幾乎全部內容了,而你現在已經掌握了!恭喜!
#其它用法
定製拖放的行為時,還會有一些其它的需求,如拖放時的圖示,到目標元素時滑鼠的指標樣式等,這裡簡單介紹一些。
當我們拖動元素時,瀏覽器預設生成了元素的縮圖,你可能需要自己設定,這時可以使用 DataTransfer
的 setDragImage(image, xOffset, yOffset);
函式。參考
MDN 上的例子。
event.dataTransfer.dropEffect
和 event.effectAllowed
共同決定了瀏覽器在執行拖動時的滑鼠指標的行為,還有一些其它的用途。只是我實際測試時發現並不起作用,
StackOverflow 的這個問題
說了一些自己的理解。
HTML5 還支援從作業系統中拖拽檔案到瀏覽器中,或者從瀏覽器到作業系統中。如果從作業系統中獲取檔案,則可以訪問 event.dataTransfer.files
欄位,包含了作業系統中的檔案內容。反之,在 dragstart
時正確設定 event.dataTransfer.files
則允許從瀏覽器中拖拽檔案到作業系統中。
#一些坑
dataTransfer
的內容只在drop
裡可讀,所以如果你想在dragEnter
或dragOver
中通過dataTransfer.getData()
返回的內容來決定一個目標元素是否允許放置是不可行的。其它的事件裡只能通過一個個檢查dataTransfer.items
裡的 type 來獲取已經設定的format
而無法獲取data
。drop
與dragend
事件是順序觸發的,但在dragend
裡沒有辦法知道drop
事件是否已經觸發。
如果你遇到過其它的坑,也請在評論區留言~