最近在項目中使用了 react-dnd撵割,一個基于 HTML5 的拖拽庫米愿,“拖拽能力”豐富了前端的交互方式富蓄,基于拖拽能力,會擴展各種各樣的拖拽反饋效果尚镰,因此有必要學習了解,最好的學習方式就是實操初烘!
拖拽交互常見于各種前端編輯器里,而“編輯器”是一個集成前端技術能力的綜合性工程分俯,其中就會涉及到各種形式的拖拽交互肾筐,因為“拖拽”是提升用戶體驗的重要交互方式澳迫,所以需要對拖拽的交互效果做各種定制化橄登,作為開發(fā)者理應熟練掌握“拖拽”的應用!
最近在開發(fā)一款低代碼平臺谣妻,所以借此機會分享一下關于“拖拽”這一交互的基礎知識和實踐經(jīng)驗卒稳,希望可以給有需要的同學提供一點參考充坑。
一、HTML5 中的拖放
拖(Drag)和放(Drop)是 HTML5 標準的組成部分辈灼,了解掌握之后也榄,舉一反三,有助于提升我們在拖拽場景下技術方案的設計能力降宅。
1.1 draggable 屬性
現(xiàn)代瀏覽器中腰根,不難發(fā)現(xiàn),圖片標簽(<img />
)是可以被長按拖拽贸营,但如果需要自定義的 DOM 節(jié)點可以被拖拽需要配置以告訴瀏覽器提供對元素(Element
/ Tag
)支持拖拽的能力岩睁。
而元素是否允許被拖放且可響應 API 操作依賴于 draggable 全局標簽屬性
draggable 是一個布爾值類型的標簽屬性:
-
true
:元素可被拖拽 -
false
:元素不可拖拽
當元素設置了 draggable 屬性捕儒,此時長按就可以自由拖拽了:
1.2 Darg & Drop 事件
HTML 的 drag
& drop
使用了“DOM Event”和從“Mouse Event”繼承而來的“drag event” 刘莹。
一個典型的拖拽操作: 用戶選中一個可拖拽的(draggable
)元素,并將其拖拽(鼠標按住不放)至一個可放置的(droppable
)元素上扇调,然后松開鼠標抢肛。
在拖動元素期間捡絮,一些與拖放相關的事件會被觸發(fā),像 drag
和 dragover
類型的事件會被頻繁觸發(fā)涎拉。
除了定義拖拽事件類型的圆,每個事件類型還賦予了對應的事件處理器
事件類型 | 事件處理器 | 觸發(fā)時機 | 綁定元素 |
---|---|---|---|
dragstart |
ondragstart | 當開始拖動一個元素時 | 拖拽 |
drag |
ondrag | 當元素被拖動期間按一定頻率觸發(fā) | 拖拽 |
dragend |
ondragend | 當拖動的元素被釋放(???松開越妈、按鍵盤 ESC)時 | 拖拽 |
dragenter |
ondragenter | 當拖動元素到一個可釋放目標元素時 | 放置 |
dragexit |
ondragexit | 當元素變得不再是拖動操作的選中目標時 | 放置 |
dragleave |
ondragleave | 當拖動元素離開一個可釋放目標元素 | 放置 |
dragover |
ondragover | 當元素被拖到一個可釋放目標元素上時(100 ms/次) | 放置 |
drop |
ondrop | 當拖動元素在可釋放目標元素上釋放時 | 放置 |
各個事件的時機可以用下面這個圖簡單表示:
??注意: dragOver 事件的默認行為是:“Reset the current drag operation to "none"”。也就是說瓤檐,如果不阻止放置元素的 dragOver 事件,則放置元素不會響應“拖動元素”的“放置行為”
// 讓綁定該事件的元素支持放置
function handleDragOver(e) {
// 阻止默認的重置行為
// 即可成為拖拽元素的放置區(qū)
e.preventDefault();
}
從設計事件標準來看祭示,如果我們需要自行實現(xiàn)拖拽的效果质涛,就需要從這關鍵的幾個事件去思考設計掰担。
1.3 DataTransfer
在上述的事件類型中带饱,不難發(fā)現(xiàn),放置元素和拖動元素分別綁定了自己的事件教寂,可如何將拖拽元素和放置元素建立聯(lián)系以及傳遞數(shù)據(jù)执庐?
這就涉及到 DataTransfer
對象:
DataTransfer
對象用于保存拖動并放下(drag and drop)過程中的數(shù)據(jù)轨淌。它可以保存一項或多項數(shù)據(jù),這些數(shù)據(jù)項可以是一種或者多種數(shù)據(jù)類型婚被。 —— DataTransfer - MDN
DataTransfer
對象在不同瀏覽器上因為標準可能不一樣使得 API 有差異梳虽,但有幾個“標準(常用)”屬性和方法需要熟悉
在 Chrome 瀏覽器上的 DataTransfer 實例如下:
(1) 屬性
屬性 | 說明 |
---|---|
dropEffect |
獲取當前選定的拖放操作類型或者設置的為一個新的類型窜觉。值為:none禀挫、copy、link描孟、move |
effectAllowed |
提供所有可用的操作類型匿醒。值是:none、copy溉痢、copyLink憋他、copyMove竹挡、link、linkMove汽畴、move耸序、all坎怪、uninitialized |
files |
包含數(shù)據(jù)傳輸中可用的所有本地文件的列表。如果拖動操作不涉及拖動文件嘁酿,則此屬性為空列表 |
items (只讀) |
提供一個包含所有拖動數(shù)據(jù)列表的 DataTransferItemList 對象 |
types (只讀) |
提供一個 dragstart 事件中設置的格式的 strings 數(shù)組闹司。 |
(2) 方法
屬性 | 說明 | |
---|---|---|
setData(format, value) |
設置給定類型的數(shù)據(jù)沐飘。如果該類型的數(shù)據(jù)不存在,則將其添加到末尾保檐,以便類型列表中的最后一項將是新的格式影晓。如果該類型的數(shù)據(jù)已經(jīng)存在檩禾,則在相同位置替換現(xiàn)有數(shù)據(jù)锌订。 | |
getData(format) |
檢索給定類型的數(shù)據(jù)画株,如果該類型的數(shù)據(jù)不存在或 data transfer 不包含數(shù)據(jù)谓传,則返回空字符串 | |
clearData([format]) |
刪除與給定類型關聯(lián)的數(shù)據(jù)芹关。類型參數(shù)是可選的侥衬。如果類型為空或未指定,則刪除與所有類型關聯(lián)的數(shù)據(jù)直颅。如果指定類型的數(shù)據(jù)不存在功偿,或者 data transfer 中不包含任何數(shù)據(jù)往堡,則該方法不會產(chǎn)生任何效果虑灰。 | |
`setDragImage(img | element, xOffset, yOffset)` | 設置自定義的拖動圖像,注意圖像需要提前加載颤诀,否則會無效 |
在簡單的拖拽場景中庸娱,其實可以類比 window.localStorage 對象的 setItem()
和 getItem()
方法來理解記憶.
但 getData()
在測試中發(fā)現(xiàn)只能在 ondrop
事件中獲取到值:
1.4 一個案例掌握拖放 API
<div>
<div class="drag" draggable="true" id="dragger" ondragstart="handleDragStart(event)">拖動元素</div>
<div class="drop" ondrop="handleDrop(event)" ondragover="allowDrop(event)">放置區(qū)域</div>
</div>
<script>
function handleDragStart(e) {
e.dataTransfer.setData('DRAG_NODE_ID', e.target.id)
}
function handleDragOver(e) {
e.preventDefault();
}
function handleDrop(e) {
e.preventDefault();
var data = e.dataTransfer.getData('DRAG_NODE_ID');
e.target.appendChild(document.getElementById(data));
}
</script>
演示案例: https://codepen.io/DYBOY/pen/eYeyvWm
效果:
1.6 兼容性
是 HTML5 標準提出的能力,因此各大瀏覽器廠商對于標準的支持有差異剧包,其兼容性參考如下:
相較于傳統(tǒng)的通過鼠標事件:mousedown
、mousemove
一铅、mouseup
組合實現(xiàn)的拖拽要簡單很多潘飘,少了放入目標邊界的判斷掉缺,也少了對位置的實時獲取操作眶明。
另外目前的 API 不算多,例如我們想要定制化拖拽的圖片大小丑瞧、鼠標樣式等蜀肘,目前暫時沒發(fā)現(xiàn)比較方便的解決方式幌缝,但是從另一個角度來說,讓我們對于拖拽能力的設計和標準有了一個更深切的認識浴栽,對于設計實現(xiàn)拖拽交互有了一個“理論”基礎典鸡!
二坏晦、手搓一個
有了上面的基礎知識昆婿,那么實現(xiàn)一個列表拖拽排序并不是什么難事。
2.1 設計實現(xiàn)
結合上述的 Drag & Drop 的事件類型睁冬,那么拖拽排序主要是針對“拖動對象”之間相互作用關系的邏輯梳理看疙,此處我們暫且區(qū)分為:
- 源對象: 拖拽列表中被拖動的單個列表項
- 目標對象: 拖拽列表中和“源對象”產(chǎn)生“相互作用”的列表項
整體的交互事件的設計思路如下:
(1) ondragstart
此時開始拖拽“源對象”的時機,在此事件回調函數(shù)中改變“源對象”的樣式脚线,設置拖拽的一些傳遞參數(shù)等初始值弥搞。
// 源對象開始拖拽
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.effectAllowed = "move";
setDragId(e.currentTarget.dataset.index); // 從 dataset 獲取拖拽項的 id
};
(2) ondragover
正與拖拽中的“源對象”產(chǎn)生相互影響的目標對象拓巧,此時“源對象”處于“目標對象”的正上方肛度,目標對象 100ms/次的頻率調用“目標對象”的 ondragover
中聲明的回調事件投慈。
此時伪煤,我們會計算改變“源對象”和“目標對象”的位置。
// 源對象在目標對象上方時
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault(); // 允許放置职烧,阻止默認事件
const dropId = e.currentTarget.dataset.index;
move(dragId, dropId); // 改變原列表數(shù)據(jù)
};
(3) ondrag
該事件作用于“源對象”蚀之,此時正處于拖拽過程中捷泞,此時可以改變源對象的 opacity
锁右、display(none)
、visiblity
樣式屬性拂到,如果在 dragstart
事件改變兄旬,則會導致拖拽拷貝對象丟失浦夷。
// 源對象被拖拽過程中
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
e.currentTarget.style.opacity = "0";
};
(4) ondragend
在松手完成“源對象”的放置時,主動調用綁定在“源對象”身上的事件呐馆,此時恢復更改的樣式莲兢。
// 源對象被放置完成時
const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
e.currentTarget.style.opacity = "1";
};
2.2 實現(xiàn)效果
2.3 加點動畫
上面的實現(xiàn)中效果還算可以改艇,但是少了拖拽項的切換過程動畫谒兄,直接在 dragover 事件中通過 move(dragId, dropId)
方法直接修改了原列表數(shù)據(jù)的排序,導致切換突變邻耕。
借助 animation
新增 CSS 幀動畫:
@keyframes dropUp {
100% {
transform: translateY(5px);
}
}
@keyframes dropDown {
100% {
transform: translateY(-5px);
}
}
.drop-up{
animation: dropUp 0.3s ease-in-out forwards;
}
.drop-down{
animation: dropDown 0.3s ease-in-out forwards;
}
同樣的在 dragOver
事件中處理兄世,新增邏輯代碼:
// 源對象在目標對象上方時
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
...
// 設置動畫
const dropId = e.currentTarget.dataset.index;
const dragIndex = findIndex(listData, (i) => i.id === dragId);
const dropIndex = findIndex(listData, (i) => i.id === dropId);
// 通過增加對應的 CSS class御滩,實現(xiàn)視覺上的動畫過渡
e.currentTarget.classList.remove("drop-up", "drop-down");
if (dragIndex < dropIndex) {
e.currentTarget.classList.add("drop-down");
} else if (dragIndex > dropIndex) {
e.currentTarget.classList.add("drop-up");
}
...
};
增加了動畫的效果:
看起來似乎好一點了削解,當然大家可以去擴充動畫的效果麸锉,亦或者借助三方動畫庫花沉。
三碱屁、已有拖拽庫
目前主流的拖拽庫有:
- react-dnd: https://github.com/react-dnd/react-dnd/
- react-beautiful-dnd: https://github.com/atlassian/react-beautiful-dnd/
- sortablejs: https://sortablejs.github.io/Sortable/
- react-sortable-hoc: https://github.com/clauderic/react-sortable-hoc/
關于幾者的差異,可以參閱:《關于react中使用拖拽插件的評測》
四、總結
由于低代碼平臺其實會有豐富的拖拽場景,從可擴展和兼容性上考慮幻枉,最終選擇了 react-dnd
作為基礎拖拽庫熬甫,當然蔓罚,在復雜的拖拽場景下豺谈,是需要自行擴展該拖拽庫,上手難度相對會高一點厂榛,不過有了這些“拖拽知識”作為前置基礎噪沙,那么擴展功能也就不是什么難事了吐根。
朋友們可以關注筆者的微信公眾號:DYBOY拷橘,來一起玩耍呀~