Demo Linkhtml
注 1: onDragExit (dragexit) 事件只有 Firefox 支持,忽略之html5
注 2: onDragLeave (dragleave) 和 onDrop (drop) 在同一個 drop target component 上只會發生一個,要麼 onDragLeave,要麼 onDropreact
Drag and drop 的整個生命週期如上所示,在整個週期中,有 drag source 和 drop target 兩類 component,須要將 drag source 的 draggable 屬性置爲 true,才能被拖動 (對 drop target 沒有要求),二者之間經過 event.dataTransfer (後面會詳細講一下 dataTransfer 的使用) 或者全局變量傳遞數據。git
咱們通常會在生命週期中進行如下操做:github
在 onDragStart 中保存拖動的數據源,修改 drag source 的樣式瀏覽器
var dragSrcEl = null
function handleDragStart(e) {
console.log('drag start:', e.target.id)
// this / e.target is current target element
this.style.opacity = '0.4'
dragSrcEl = this
e.dataTransfer.setData('text/html', this.innerHTML)
}
複製代碼
onDrag 事件通常狀況下咱們不關心,除非你的頁面要根據 drag 的距離,時間來發生變化,則須要處理this
在 onDragEnter 中修改 drop target 的樣式google
function handleDragEnter(e) {
console.log('drag enter:', e.target.id)
this.classList.add('over')
}
複製代碼
在 onDragOver 中,通常只要進行的操做就是執行 event.preventDefault()
來阻止拖拽的默認操做。尚不清楚這個默認操做是什麼,但若是你不在 onDragOver 事件中執行這個操做,onDrop 事件就不會發生,Chrome 和 Firefox 皆如此。由於這個事件發生很頻繁,因此不適合在這個事件中處理過於繁重的事情,好比修改樣式,這就是爲何咱們要把修改樣式的工做放到 onDragEnter 中url
function handleDragOver(e) {
console.log('drag over:', e.target.id)
e.preventDefault() // Necessary. Allows us to drop.
}
複製代碼
在 onDragLeave 中,恢復 drop target 的樣式 (若是發生了 onDrop,則 onDragLeave 不會發生,因此恢復 drop target 樣式的工做在 onDrop 或 onDragEnd 中也要處理)spa
function handleDragLeave(e) {
console.log('drag leave:', e.target.id)
this.classList.remove('over')
}
複製代碼
在 onDrop 中,處理拖拽的真正邏輯,通常是實現數據交換
function handleDrop(e) {
console.log('drop:', e.target.id)
e.preventDefault() // stop the browser from redirection
// swap
if (dragSrcEl != this) {
dragSrcEl.innerHTML = this.innerHTML
this.innerHTML = e.dataTransfer.getData('text/html')
}
}
複製代碼
另外,在 onDrop,event.preventDefault()
也是幾乎必須執行的操做,用來阻止拖拽的默認操做。但實際這個操做只是爲了兼容 Firefox,在 Chrome 上不執行這個操做是沒有問題的。在 Firefox 上,若是 event.dataTransfer 中的 data 是連接,那麼拖拽的默認操做是跳轉到此連接。
假如在 onDragStart 中 dataTransfer 中設置的數據是這樣的:
function handleDragStart(e) {
e.dataTransfer.setData('text/plain', 'https://www.google.com')
// or
// e.dataTransfer.setData('text', 'anything')
}
複製代碼
那麼在 onDrop 中,若是沒有 event.preventDefault()
,在 Firefox 上的效果這樣的:
e.dataTransfer.setData('text', 'anything')
的默認操做是跳轉到 https://www.anything.com
但若是設置是的 e.dataTransfer.setData('aaa', 'anything')
則不會發生跳轉,anyway,就乾脆加上 event.preventDefault()
就對了。也許你會問,爲何會往 dataTransfer 中設置 'aaa' 這樣奇怪的數據呢,不往 dataTransfer 中設置數據行不行,答案是不行,在 Firefox 上若是 dataTransfer 中沒有 data,Firefox 會認爲你不是真的想拖拽,而後拖拽就會失敗,因此咱們就要往裏面隨便塞點數據。
在 Chrome 上不會發生跳轉,因此不也強制在 onDragStart 中往 dataTransfer 存數據。
在 onDragEnd 中進行一些清理工做,好比恢復 drag source 和 drop target 的樣式,由於 onDragLeave 和 onDrop 並不能保證都發生,但 onDragEnd 確定是會發生的
function handleDragEnd(e) {
console.log('drag end:', e.target.id)
// reset
dragSrcEl = null
this.style.opacity = ''
[].forEach.call(cols, function (col) {
col.classList.remove('over')
});
}
複製代碼
將 column-1 element 拖動,經歷 column-2 和 column-3,最終在 column-3 放下,整個過程是這樣的:
column-1 (drag source) | column-2 (drop target) | column-3 (drop target) |
---|---|---|
onDragStart | ||
onDrag... | ||
... | onDragEnter | |
... | onDragOver... | |
... | onDragOver | |
... | onDragLeave | |
... | onDragEnter | |
... | onDragOver... | |
onDrag | onDragOver | |
onDrop | ||
onDragEnd |
在 Chrome 上運行時,整個過程和預期一致,但在 Firefox 上運行,不知道是否是 Firefox 的 bug,常常會出現後一個 component 的 enter 事件出如今前一個 component 的 leave 事件以前。
在 React 中處理 Drag and Drop 和處理原生事件差很少。
renderItem = (item_id, index) => {
const item = this.state.data.items[item_id]
return (
<div className={this.getItemClassName(item)}
key={item.id}
draggable={true}
onDragStart={(e)=>this.handleDragStart(e, item, index)}
onDrag={(e)=>this.handleDrag(e, item)}
onDragEnd={(e)=>this.handleDragEnd(e, item)}
onDragEnter={(e)=>this.handleDragEnter(e, item)}
onDragOver={(e)=>this.handleDragOver(e, item)}
onDragExit={(e)=>this.handleDragExit(e, item)}
onDragLeave={(e)=>this.handleDragLeave(e, item)}
onDrop={(e)=>this.handleDrop(e, item, index)}>
<h1>{item.title}</h1>
</div>
)
}
複製代碼
但在 Firefox 上,和原生的 Drag and Drop 事件同樣,也存在相同的問題,後一個 component 的 enter 事件發生在前一個 component 的 leave 以前。
更糟糕的是,在 Chrome 和 Firefox 還存在一個問題,會連續發生兩次 onDragEnter,再連續兩次 onDragLeave,以下圖所示 (React 16.5.2,Chrome: 69.0.3497.100,Firefox: 62.0):
這致使的問題是,onDragLeave 變得不可靠,咱們通常會在 onDragLeave 中恢復 drop target 的樣式,但實際在連續兩次 onDragEnter 後的第一次 onDragLeave 發生後,拖拽並無離開這個 drop target,drop target 還會響應 onDragOver 事件。
不確實這是否是 React 的 bug,但我如今不會在 onDragLeave 中處理任何邏輯,而是把它放到 onDrop 或 onDragEnd 中處理。
前面咱們說道 dataTransfer 是用來在拖拽源和放置目標之間傳遞數據用的,你可能或疑問,爲何須要這個東東,我直接把源數據放在全局變量裏不行嗎?
若是隻是在同一個頁面內操做,其實是能夠的,像用 React 實現時,咱們就必須放在 state 中。
可是 HTML5 的 Drag and Drop API 是支持從網頁中拖拽到桌面,或是從桌面拖拽到網頁中的,這個時候,全局變量的方法就不可行的,我想這是爲何須要 dataTransfer 的緣由。而 Firefox 也強制你必須在 onDragStart 中往 dataTransfer 中設置 data,即便是在同一個頁面內拖拽,不然認爲你不是真的想要拖拽 (沒數據你拖拽啥),從而致使拖拽不可用,因此通常這時候咱們就會隨便往 dataTransfer 中賦一些值,Chrome 沒有這個要求。
由於支持從桌面拖拽到網頁,因此在上面第一個例子的 onDrop 事件中,可能存在 dragSrcEl 爲 null 的狀況,咱們要加上判斷以加強程序的健壯性。
function handleDrop(e) {
console.log('drop:', e.target.id)
e.preventDefault() // stop the browser from redirection in firefox
// swap
// drop event maybe caused by draging from desktop, so dragSrcEl maybe null
if (dragSrcEl && dragSrcEl != this) {
dragSrcEl.innerHTML = this.innerHTML
this.innerHTML = e.dataTransfer.getData('text/html')
}
}
複製代碼
咱們在 onDrop 中打印一下 event.dataTransfer,而後拖拽一個文件進來看看這個值是什麼。
function handleDrop(e) {
console.log(e.dataTransfer)
...
}
複製代碼
在 Firefox 上的輸出:
在 Chrome 上的輸出:
這種狀況下,沒有 onDragStart/onDrag/onDragEnd 事件,只有 onDragEnter/onDragOver/onDragLeave 或 onDrop 事件。
但 Firefox 和 Chrome 上 dataTransfer 的值不同,須要作兼容性處理。
dataTransfer 的常見方法和屬性:
setData(key, value)
和 getData(key)
是配套使用的。在 HTML5 中,key 能夠是任意值,但若是 key 是標準中指定的一些值,好比 'text' 或 'text/plain', 'url' 等,則在 onDrop 中若是沒有 preventDefault(),則會觸發默認操做,好比跳轉到 value 中指定的網址去。(在之前或一些瀏覽器上,key 只支持某些固定值,好比 'text' 或 'url')
setDragImage()
是用來改變拖拽時隨光標一塊兒移動的圖片,默認是拖拽源的鏡像但透明度更低。(用到時再補充)
effectAllowed 和 dropEffect 也是配套使用的,兩個值要匹配,才能拖拽成功,不然失敗。
effectAllowed 是在 onDragStart 中給拖拽源所賦的值,dropEffect 是在 onDragOver 中給放置目標所賦的值。effectAllowed 代表拖拽源所指望放置目標的類型,好比 effectAllowed 值爲 copy,則遇到 dropEffect 也爲 copy 的放置目標時才能放置,不然不行。若是 effectAllowed 爲 copyLink,則代表可接受的 dropEffect 必須爲 copy 或 link。
咱們在 onDragStart 中設置 effectAllowed 爲 copyLink
function handleDragStart(e) {
...
e.dataTransfer.effectAllowed = 'copyLink'
}
複製代碼
在 onDragOver 中,檢測若是 id 爲 column-4,則 dropEffect 爲 move,其他爲 copy 或 link。
function handleDragOver(e) {
...
if (e.target.id === 'column-4') {
e.dataTransfer.dropEffect = 'move';
} else if (e.target.id === 'column-3') {
e.dataTransfer.dropEffect = 'link';
} else {
e.dataTransfer.dropEffect = 'copy';
}
}
複製代碼
那麼,column-1 ~ column-3 能夠相互拖拽,但不能拖拽到 column-4 上,效果以下所示。
另外,能夠發現,不一樣的 dropEffect,拖拽時鼠標樣式也有所不一樣,copy 的鼠標樣式是一個綠色的十字,而 link 是一個連接樣式。
(但不知道除了改變鼠標樣式外,effectAllowed 和 dropEffect 還有什麼實際用途,咱們應該也不會用這兩個屬性做爲約束可否拖拽的邏輯吧。)