1. 程式人生 > >HTML 5 Drag and Drop 入門教程

HTML 5 Drag and Drop 入門教程

在 HTML 5 之前,想要實現 Drag and Drop(拖拽/拖放)一般需要求助於 JQuery,所幸 HTML 5 已經把 DnD 標準化,現在我們能“輕易”地為幾乎任意元素實現拖放功能。只是它的難度取決於你對 API 的理解程度,而官方文件並不好懂。這篇文章會一步步帶你瞭解它的 API。

最終效果如下:

#拖動事件

繼續之前,有必要先了解拖動時會觸發哪些事件。考慮拖動 Source Element,途中經過 Intermediate Element,最終進入 Target Element 並鬆開滑鼠,則路徑上會觸發的事件如下圖所示:

這些事件的具體內容下面會講到,你可以先跳過之後再回來檢視,簡單來說:

  1. dragstart:當我們“拖”起元素時會觸發。
  2. dragenter:當拖動元素 A 進入另一個元素 B 時,會觸發 B 的 dragenter 事件。
  3. dragleave:與 dragenter 相對應,當拖動元素 A 離開元素 B 時,觸發 B 的 dragleave 事件。
  4. dragover:當拖動元素 A 在另一個元素 B 中移動/停止時觸發 B 的 dragover 事件。文件說是每幾百毫秒觸發一次,Chrome 實測 1ms 左右觸發;Firefox 大概是 300ms
  5. drop:當在拖動元素 A 到元素 B 上,釋放滑鼠時觸發 B 的 drop 事件,相當於元素 B 接收了元素 A 。
  6. dragend:在 drop 事件之後,還會觸發元素 A 的 dragend 事件,這裡可以對元素 A 作一些清理工作。

除了上面的事件外,還有兩個一般用不到的事件:

  1. drag:和 dragover 類似,當元素 A 被拖動時,每隔一段時間就會觸發這個事件。與 dragover 不同,drag 事件是觸發在源元素 A 上,而 dragover 是觸發上潛在目標元素 B 上的。
  2. 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 都監聽了 dragenterdragleave 事件,當拖動元素進入它們時,邊框會變成虛線,離開時變回實線。這裡有幾個注意點:

  • dragenterdragover 裡我們呼叫了 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/plaintext/htmltext/uri-list等,但同時也可以是任意自定義的型別;不幸的是 data 只能是 stringfile
  • 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 並不容易,但換句話說這也就是它的幾乎全部內容了,而你現在已經掌握了!恭喜!

#其它用法

定製拖放的行為時,還會有一些其它的需求,如拖放時的圖示,到目標元素時滑鼠的指標樣式等,這裡簡單介紹一些。

當我們拖動元素時,瀏覽器預設生成了元素的縮圖,你可能需要自己設定,這時可以使用 DataTransfersetDragImage(image, xOffset, yOffset); 函式。參考 MDN 上的例子

event.dataTransfer.dropEffectevent.effectAllowed 共同決定了瀏覽器在執行拖動時的滑鼠指標的行為,還有一些其它的用途。只是我實際測試時發現並不起作用, StackOverflow 的這個問題 說了一些自己的理解。

HTML5 還支援從作業系統中拖拽檔案到瀏覽器中,或者從瀏覽器到作業系統中。如果從作業系統中獲取檔案,則可以訪問 event.dataTransfer.files 欄位,包含了作業系統中的檔案內容。反之,在 dragstart 時正確設定 event.dataTransfer.files 則允許從瀏覽器中拖拽檔案到作業系統中。

#一些坑

  • dataTransfer 的內容只在 drop 裡可讀,所以如果你想在 dragEnterdragOver 中通過 dataTransfer.getData() 返回的內容來決定一個目標元素是否允許放置是不可行的。其它的事件裡只能通過一個個檢查 dataTransfer.items 裡的 type 來獲取已經設定的 format 而無法獲取 data
  • dropdragend 事件是順序觸發的,但在 dragend 裡沒有辦法知道 drop 事件是否已經觸發。

如果你遇到過其它的坑,也請在評論區留言~

#參考