最近有個需求,須要產品導航欄支持拖放。
雖然開源社區已有很多成熟的拖放庫,但考慮到代碼可控性和可定製性,仍是本身寫吧。css
關於選型,前端實現拖放功能,無外乎幾種:
一、經過樣式佈局+鼠標事件,採用此方案的插件如:@shopify/draggable
二、Canvas繪製,插件如:konva
三、Drag&Drop接口,插件如:dragulahtml
通過一番研究,最終選擇了原生Drag&Drop的方案,緣由以下:
一、原生拖放事件,順應JS語言發展趨勢;
二、兼容性符合項目要求;
三、在Can I use...中有以下描述:
前端
一個拖放行爲,天然牽涉到兩部分元素,即拖動元素和釋放區域元素。
與之相關的事件總共有8個,其中綁定在拖動元素的事件有三個:drag
、dragstart
、dragend
;
剩下5個事件綁定在釋放區域元素上:dragenter
、dragover
、dragleave
、dragexit
、drop
。
具體定義能夠參考mdnreact
瀏覽器中,有三種元素,默認是能夠被拖動的,它們是:
一、被選中後的文本;
二、圖片;
三、連接
其餘元素要轉成可拖動元素,必須添加draggable="true"
,如:git
<div draggable="true"></div>
複製代碼
注意:這裏不能略寫,如寫成:github
<div draggable></div>
複製代碼
是無效的。瀏覽器
要使一塊元素可被釋放,首先須要綁定dragenter
或dragover
事件,而後阻止事件,以下:app
<div ondragover="return false">
<div ondragover="event.preventDefault()"> 複製代碼
由於,這兩個事件的默認行爲就是「不觸發」drop
事件,因此要定義成可被釋放區域,就反其道而行之便可。框架
一個完整的拖放操做,除了拖動一個元素,在指定區域釋放以外,還有最重要的一步,就是將元素攜帶的信息在被釋放區域中展現。
好比,拖放一張圖片,本質上就是獲取到被拖動的圖片src
屬性值,並在釋放時,在釋放區域展現一張相同src
的圖片。
而這個信息,就存儲在DataTransfer對象中。
對於非默承認拖放元素來講,其包含的信息須要在dragstart
事件中設置,使用DataTransfer.setData()
,如:dom
dragItem.ondragstart = e => {
e.dataTransfer.setData('text/plain', 'drag info');
}
複製代碼
若是但願拖動時,展現自定義的圖片,還能夠調用dataTransfer.setDragImage
,如:
dragItem1.ondragstart = e => {
const img = new Image();
img.src = 'img_url.jpg';
e.dataTransfer.setDragImage(img, 0, 0);
}
複製代碼
在drop
事件中,能夠取得拖放元素的信息,並將指定信息經過dom操做,展現在特定區域,如:
dropArea.ondrop = e => {
e.preventDefault();
const data = event.dataTransfer.getData("text/plain");
const div = document.createElement("div");
div.textContent = data;
e.target.appendChild(div);
}
複製代碼
在DataTransfer對象還有一對屬性,用來確保釋放區域只能釋放特定類型的拖拽元素,即dropEffect
和effectAllowed
。
effectAllowed
只能在dragstart
事件中設置,在dragenter
或dragover
事件中,須要設置dropEffect
的值與effectAllowed
一致,才能觸發drop
事件。如:
dragItem.ondragstart = e => {
e.dataTransfer.effectAllowed = "move";
}
dropArea.ondragover = e => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}
複製代碼
其餘屬性及方法,詳細能夠查看mdn
跨終端能力是drag&drop最大的特色。
最多見的跨終端需求,就是從用戶的本地拖放文件到瀏覽器中指定區域實現上傳功能。
在指定區域的drop
事件中,經過DataTransfer對象的files屬性,便可得到文件列表信息,如:
dropArea.ondrop = e => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length) {
Array.prototype.forEach.call(files, f => {
console.log(f.name); //打印文件名
});
}
}
複製代碼
在React項目中使用drag&drop,依然遵循React數據驅動的原則,即事件->數據->DOM更新
。
因此,像以前提到的,經過DataTransfer對象傳遞數據的方式,在React項目中,能夠改成操做組件對象屬性,保證數據流的清晰。
但除此以外,在實際實踐中,仍是遇到了一些問題,須要特殊處理。具體以下:
dataTransfer.setData
起初,爲保證數據流清晰,在React組件中,綁定onDragStart
,僅負責監聽事件,數據的變更和傳遞所有修改組件屬性,可是會遇到Firefox瀏覽器沒法拖放的兼容問題。經查發現,在Firefox中,可拖放元素必須知足:
一、添加draggable="true"
;
二、綁定事件dragstart
;
三、在dragstart中,dataTransfer.setData設置數據
因此,即便e.dataTransfer.setData('text', '');
設置空字符串,也必須添加上這一條。
drop&drag跨終端能力有時也會成爲干擾。在項目中,會發現,若是沒有作判斷,同一個頁面同時打開兩個瀏覽器tab,其拖放元素能夠跨tab拖動,可能會形成意外BUG。爲此,須要增長判斷。
一種方式,在組件實例構建時,生成一個隨機字符,藉助dataTransfer.setData
,爲拖放元素打上標記。同時,在drop
事件中執行判斷。
固然,若是拖放元素和釋放區域分屬不一樣組件,則須要在他們的父組件中,生成隨機字符,以props
形式,傳遞到兩個子組件。
在上述提到的爲拖放元素打標籤中,起初採用的是這樣的寫法:
e.dataTransfer.setData('text', uniqDataTransferTag);
複製代碼
結果在Firefox中,每次drop
事件觸發時,瀏覽器會自動打開新tab並搜索uniqDataTransferTag(隨機字符)
。
根據官方解釋,須要在drop
事件中調用e.preventDefault()
,同時阻止冒泡e.stopPropagation()
,但通過嘗試,依然不生效。初步判斷,可能與React的SyntheticEvent機制有關。因而只好曲線救國,改成設置自定義的MIME type
,如:
e.dataTransfer.setData('ucloud_drag_tag', uniqDataTransferTag);
複製代碼
在項目中,須要在onDragOver
中,判斷被拖放元素當前位置,並執行DOM操做。
根據定義,dragover
事件會在被拖放元素拖到釋放區域上時,每幾百毫秒觸發一次,顯然不作任何處理會很是影響性能。這裏,天然想到採用節流throttle方式優化。
因爲節流是異步操做,而根據React的SyntheticEvent,event對象會在當前事件循環結束後移除,除非調用e.persist()
,才能在異步操做中訪問到。
根據設計師要求,項目中但願實現元素拖動開始後要被拖走,以下圖:
css class
,包含樣式:
transform: translateX(-9999px);
複製代碼
二、對被拖放元素添加樣式:
transition: transform 0.1s;
複製代碼
3。在拖動開始後,添加上述第一步的css class
。
根據交互設計,須要實現長按元素必定時長後才能夠觸發拖拽。
起初,採用的方案是,綁定鼠標事件mousedown
,觸發setTimeout
,達到固定時長後觸發state
更新,改變拖放元素的draggable
值。但實際測試中發現,這種方法存在必定的失敗率,即明明已經達到了長按的時長,依然不能拖放。並且,在Firefox
中這個問題更加明顯。
推測,多是draggable
的更新偶爾會晚於dragstart
事件,致使拖放失敗。
因而轉變思路,增設組件的屬性做爲判斷標誌,在mousedown
事件中更新判斷標誌,而draggable
始終設爲true
。以下:
// mousedown事件處理函數
handleLongPress = e => {
this.resetDragTimer(); // 清除定時器
return (this.triggerDragTimer = setTimeout(() => {
this.isMenuDraggable = true; // 判斷標誌
}, this.triggerDragInterval));
};
// dragstart事件處理函數
handleDragStart = e => {
if (!this.isMenuDraggable) {
e.preventDefault();
} else {
...
}
};
複製代碼
Drag&Drop做爲原生拖放API,能夠用最少代碼實現拖放,看似「簡單」,實際並不是如此。在實踐中,仍是須要對官方接口定義,以及各瀏覽器差別有足夠了解,才能避免各類未知錯誤。而在React這類數據驅動的框架中運用時,如何處理事件監聽,同時又不打亂組件的數據流,仍是須要好好設計一番。