2017年8月25日

[筆記] 製作可拖曳的元素(HTML5 Drag and Drop API)

HTML5 中的 Drag & Drop API 可以讓我們在瀏覽器中做到拖曳元素、排序元素、或者是讓使用者透過拖拉的方式把要上傳的檔案拉到瀏覽器當中。
在學習 HTML5 Drag & Drop API 時,最重要的是去區分 Drag SourceDrop Target,因為它們會需要各自去監聽不同的事件。
假設我們要把下圖中藍色的圓從左邊的區域移到右邊的區域,那麼:
  • Drag Source 指的是被點擊要拖曳的物件,也就是藍色的圓,通常是一個 element
  • Drop Target 指的是拖曳的物件被放置的區域,也就是右邊的綠色區域,通常是一個 div container

前置動作

在開始實做前,有一些是需要知道和注意的前置動作。

事件(Event)

Drag & Drop 提供的間事件主要包含 dragstart, drag, dragend, dragenter, dragover, dragleave, drop,其中有些是針對 Drag Source 的,有些則是針對 Drop Target 的,整理如下表:
Drag Source Drop Target
dragstart
drag dragenter
dragover
dragleave
drop
dragend
  • drag:在 drag source 被拖曳時會持續被觸發。
  • dragover:當拖曳的 drag source 在 drop target 上方時會持續被觸發。
記得要針對被拖曳的物件取消預設行為(default)

CSS Style

針對要被拖曳的元素(Drag Source)可以透過 CSS 的屬性設定,避免讓使用者在拖曳該元素時選取到裡面的內容:
[draggable="true"] {
  /*
   To prevent user selecting inside the drag source
  */
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}

HTML Attribute

針對能夠被拖曳的元素,在其 HTML 標籤上添加屬性 draggable="true"
<div id="drag-source" draggable="true"></div>

Drag and Drop Basic

Demo & Source Code

See the Pen Drag and Drop Basic by PJCHEN (@PJCHENder) on CodePen.

示範如何將一個物件從一個 container (#source-container) 內拖曳到另一個 container (#target-container)內:

HTML

針對可以被拖曳的元素加上 draggable="true"
<div id="drag-drop-basic">
  <div id="source-container">
    <div id="drag-source" draggable="true"></div>
  </div>
  <div id="target-container"></div>
</div> 

JavaScript

針對要被拖曳的元素(dragSource) 監聽 dragstart 事件,並且把要傳遞給 dropTarget 的資料透過 setData 加以設定:
let dragSource = document.querySelector('#drag-source')
dragSource.addEventListener('dragstart', dragStart)

function dragStart (e) {
  console.log('dragStart')
  e.dataTransfer.setData('text/plain', e.target.id)
}
針對要被置放的容器 dropTarget 監聽 drop 事件,來處理當使用者放掉的時候要執行的行為,並透過 getData 來取得傳遞的資料;監聽 dragenterdragover 事件來避免預設行為:
let dropTarget = document.querySelector('#target-container')
dropTarget.addEventListener('drop', dropped)
dropTarget.addEventListener('dragenter', cancelDefault)
dropTarget.addEventListener('dragover', cancelDefault)

function dropped (e) {
  console.log('dropped')
  cancelDefault(e)
  let id = e.dataTransfer.getData('text/plain')
  e.target.appendChild(document.querySelector('#' + id))
}

function cancelDefault (e) {
  e.preventDefault()
  e.stopPropagation()
  return false
}

Drag and Drop with multiple sources in multiple containers

Demo & Source Code

在這個範例中,我們可以把 drag sources 來回拖放於左右兩邊的 drop target:

See the Pen Drag and Drop in Multiple Container by PJCHEN (@PJCHENder) on CodePen.

HTML

在能夠被拖放的容器上加上 data-role="drag-drop-container" 屬性:
<div id="drag-drop-basic">
  <div id="source-container" data-role="drag-drop-container">
    <div id="drag-source" draggable="true"></div>
  </div>
  <div id="target-container" data-role="drag-drop-container"></div>
</div>

JavaScript

允許多個可拖曳的物件:
// Allow multiple draggable items
let dragSources = document.querySelectorAll('[draggable="true"]')
dragSources.forEach(dragSource => {
  dragSource.addEventListener('dragstart', dragStart)
})

function dragStart (e) {
  e.dataTransfer.setData('text/plain', e.target.id)
}
允許多個可置放的容器:
// Allow multiple dropped targets
let dropTargets = document.querySelectorAll('[data-role="drag-drop-container"]')
dropTargets.forEach(dropTarget => {
  dropTarget.addEventListener('drop', dropped)
  dropTarget.addEventListener('dragenter', cancelDefault)
  dropTarget.addEventListener('dragover', cancelDefault)
})

function dropped (e) {
  cancelDefault(e)
  let id = e.dataTransfer.getData('text/plain')
  e.target.appendChild(document.querySelector('#' + id))
}

function cancelDefault (e) {
  e.preventDefault()
  e.stopPropagation()
  return false
}

修改拖曳時的 CSS 樣式

Demo & Source Code

See the Pen Drag and Drop with CSS Style by PJCHEN (@PJCHENder) on CodePen.

我們新增兩個 CSS 樣式分別套用在被拖曳的物件和被放置的容器上:
// For drag sources
.dragging {
  opacity: .25;
}

// For drop target
.hover {
  background-color: rgba(0,191,165,.04);
}
針對物件本身,我們在開始拖曳時添加樣式,結束拖曳時移除樣式:
function dragStart (e) {
  this.classList.add('dragging')
  e.dataTransfer.setData('text/plain', e.target.id)
}

function dragEnd (e) {
  this.classList.remove('dragging')
}
針對容器,我們在進入容器時添加樣式,在離開或放置後移除樣式:
function dropped (e) {
  let id = e.dataTransfer.getData('text/plain')
  e.target.appendChild(document.querySelector('#' + id))
  this.classList.remove('hover')
}

function dragOver (e) {
  this.classList.add('hover')
}

function dragLeave (e) {
  this.classList.remove('hover')
}

製作可拖拉排序的清單

Demo & Source Code

See the Pen Drag and Drop Sortable List by PJCHEN (@PJCHENder) on CodePen.

下面的範例中有用到 jQuery:

HTML

建立一個清單:
<ul id="items-list" class="moveable">
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
  <li>Four</li>
</ul>

JavaScript

先利用 document.querySelectorAll 將所有清單中的元素選取起來:
let items = document.querySelectorAll('#items-list > li')
將清單中的每一個元素都加上 draggable="true" 的屬性,並且監聽相關事件:
items.forEach(item => {
  $(item).prop('draggable', true)
  item.addEventListener('dragstart', dragStart)
  item.addEventListener('drop', dropped)
  item.addEventListener('dragenter', cancelDefault)
  item.addEventListener('dragover', cancelDefault)
})
取得被拖曳物件的 index 值:
function dragStart (e) {
  var index = $(e.target).index()
  e.dataTransfer.setData('text/plain', index)
}
放下(drop)的時候要把原本 index 的元素移除(remove)掉
function dropped (e) {
  cancelDefault(e)
  
  // get new and old index
  let oldIndex = e.dataTransfer.getData('text/plain')
  let target = $(e.target)
  let newIndex = target.index()
  
  // remove dropped items at old place
  let dropped = $(this).parent().children().eq(oldIndex).remove()

  // insert the dropped items at new place
  if (newIndex < oldIndex) {
    target.before(dropped)
  } else {
    target.after(dropped)
  }
}

function cancelDefault (e) {
  e.preventDefault()
  e.stopPropagation()
  return false
}

其他

將檔案拖曳至瀏覽器

DataTransfer.files  

Drag and Drop Effect

DataTransfer 屬性 對象 事件
effectAllowed drag sources dragstart
dropEffect drag target dragover
effectAllowed 是針對 drag sources 的 dragstart 事件;dropEffect 則是針對 drop target 的 dragover 事件。
如果 effectAllowd 和 dropEffect 中設定的類型不同,則無法將該 source 拖曳到 target。
/**
 * copy, move, link, none
 **/
DataTransfer.effectAllowed  // 設定在 dragstart event,允許拖曳的效果
DataTransfer.dropEffect     // 設定在 dragover event,當檔案拖曳進來時顯示的效果

客制化拖曳圖片

套用在 dragStart 事件中:
DataTransfer.setDragImage(img, x-offset, y-offset)
function dragStart (e) {
  let img = new Image() 
  img.src = 'example.gif'
  e.dataTransfer.setDragImage(img, 10, 10)
}

NOTICE

在 FireFox 中的 dragstart 事件可能需要將 setData 設為空:
function dragstart (e) {
  e.dataTransfer.setData('text/plain', '')
}

其他

一個寫得很好的清單拖拉範例,裡面有用到 document.elementFromPoint(x, y) 這個 Web API,謝謝 @hannahpun 分享:

See the Pen Vanilla JavaScript - Drag Sort by Fitri Ali (@fitri) on CodePen.

參考

HTML Fundamentals @ Pluralsight
  1. Introduction to Drag and Drop Events
  2. Drag and Drop Events in Detail
  3. Safari Support
  4. Draggable CSS Style
  5. Drag and Drop Basics
  6. Using Role Selectors
  7. Events in Action
  8. Styling Drag Sources and Drop Targets
  9. Implementing Drag and Drop Sortable List
  10. Drag and Drop Data Transfer Types
  11. Dropping Files from The Client into The Web Browser
  12. Setting and Enforcing Drag and Drop Effects
  13. Customizing The Drag Cursor Image
  14. Implementing A Drag and Drop Module Demo and Markup
  15. Implementing A Drag and Drop Module JavaScript

0 意見:

張貼留言