動(dòng)態(tài)列表組件 - 拖拽排序功能設(shè)計(jì)與實(shí)現(xiàn)
大廠技術(shù)??堅(jiān)持周更??精選好文
最終效果

原生實(shí)現(xiàn)原理
關(guān)于拖拽

標(biāo)簽的圖片默認(rèn)是可以拖動(dòng)的(效果如上圖)
然而其他的標(biāo)簽(div等)是不能被拖動(dòng)的,鼠標(biāo)點(diǎn)擊選擇后移動(dòng)沒(méi)有拖拽效果,需要添加屬性draggable="true" 使得元素可以被拖動(dòng)。
拖放過(guò)程觸發(fā)事件
在拖動(dòng)目標(biāo)上觸發(fā)事件(源元素) :
ondragstart[1]- 用戶開始拖動(dòng)元素時(shí)觸發(fā)
ondrag[2]- 元素正在拖動(dòng)時(shí)觸發(fā)
ondragend[3]- 用戶完成元素拖動(dòng)后觸發(fā)
釋放目標(biāo)時(shí)觸發(fā)的事件:
ondragenter - 當(dāng)被鼠標(biāo)拖動(dòng)的對(duì)象進(jìn)入其容器范圍內(nèi)時(shí)觸發(fā)此事件
ondragover[4]- 當(dāng)某被拖動(dòng)的對(duì)象在另一對(duì)象容器范圍內(nèi)拖動(dòng)時(shí)觸發(fā)此事件
ondragleave[5]- 當(dāng)被鼠標(biāo)拖動(dòng)的對(duì)象離開其容器范圍內(nèi)時(shí)觸發(fā)此事件
ondrop[6]- 在一個(gè)拖動(dòng)過(guò)程中,釋放鼠標(biāo)鍵時(shí)觸發(fā)此事件

ps.需要注意是 ondragend 和 ondragover 的默認(rèn)事件 Reset the current drag operation to "none".也就是說(shuō)它的默認(rèn)行是當(dāng)ondragover觸發(fā)時(shí),移動(dòng)會(huì)失效(產(chǎn)生殘影),所以想讓一個(gè)元素可放置,需要阻止默認(rèn)行為。
element.ondragover?=?event?=>?{??????
????event.preventDefault();?????//?...?
}
一個(gè)簡(jiǎn)單js原生拖拽demo

思考思路

ondragstart
獲得源元素
ondrag
進(jìn)行拖拽過(guò)程中的源元素樣式改變
ondragend
移除多余樣式,拿到ondragover中最后獲得的目標(biāo)元素index,改變列表數(shù)組
ondragover
以較短時(shí)間內(nèi)生成快照,記錄源元素的拖動(dòng)軌跡經(jīng)過(guò)了哪些元素,用來(lái)生成最終的目標(biāo)元素,給途經(jīng)元素增加位移動(dòng)畫樣式
問(wèn)題解決
如何進(jìn)行數(shù)據(jù)傳遞
思路1:為每個(gè)item設(shè)置className=index${index},通過(guò)Number(event.target.classList.slice("5"))來(lái)取值,
可以達(dá)到目標(biāo)效果,但實(shí)現(xiàn)不太優(yōu)雅,所以放棄這個(gè)思路
思路2:為每個(gè)item設(shè)置屬性data-index={index},通過(guò)event.target.dataset.index來(lái)取值,符合代碼的語(yǔ)義性,故采用這個(gè)方法
嵌套DOM造成ondragover重復(fù)觸發(fā)
ondragover函數(shù)會(huì)在源元素移動(dòng)到目標(biāo)元素范圍內(nèi)且不停止拖拽時(shí)反復(fù)觸發(fā),如果源元素非單層DOM結(jié)構(gòu)時(shí),每一層DOM都會(huì)觸發(fā)ondragover,造成數(shù)據(jù)混亂
下圖控制臺(tái)第一個(gè)紅框的數(shù)據(jù)是拖拽item1時(shí)打印,第二個(gè)紅框是拖拽item2時(shí)打印,可以發(fā)現(xiàn)item1的兩層DOM結(jié)構(gòu)觸發(fā)了兩次打印:

html>
<html?lang="zh">
??<head>
????<meta?charset="UTF-8"?/>
????<meta?name="viewport"?content="width=device-width,?initial-scale=1.0"?/>
????<meta?http-equiv="X-UA-Compatible"?content="ie=edge"?/>
????<title>Documenttitle>
????<style>
??????.wrapper?{
????????display:?flex;
????????border:?1px?solid?orangered;
????????padding:?10px;
??????}
??????.col?{
????????border:?1px?solid?#808080;
????????height:?500px;
????????width:?200px;
????????margin:?0?10px;
????????padding:?10px;
??????}
??????.item?{
????????border:?1px?solid?#808080;
????????margin:?5px?0;
??????}
????style>
??head>
??<body>
????<div?class="wrapper">
??????<div?class="col1?col">
????????<div?class="item"?id="item1"?draggable="true">
??????????<div>item1div>
????????div>
????????<div?class="item"?id="item2"?draggable="true">item2div>
??????div>
????div>
????<script>
??????let?cols?=?document.getElementsByClassName("col");
??????for?(let?col?of?cols)?{
????????col.ondragover?=?(e)?=>?{
??????????e.preventDefault();
??????????console.log(e.target);
????????};
??????}
????script>
??body>
html>
解決這個(gè)問(wèn)題的方法就是放棄使用e.target,改用e.currentTarget
target和currentTarget的概念:
1、target發(fā)生在事件流的目標(biāo)階段,而currentTarget發(fā)生在事件流的整個(gè)階段(捕獲、目標(biāo)和冒泡階段)
2、只有當(dāng)目標(biāo)流處于目標(biāo)階段的時(shí)候才相同。
3、而當(dāng)事件流處于捕獲和冒泡階段時(shí),target指向被點(diǎn)擊的對(duì)象,而currentTarget指向當(dāng)前事件活動(dòng)的對(duì)象,通常是事件的祖元素。
拖拽結(jié)束后出現(xiàn)重影
所謂重影就是我們?cè)谕蟿?dòng)到指定位置后,頁(yè)面我們拖動(dòng)的元素先慢慢漂移到了初始位置,隨后才更新到了我們指定的位置

解決方法是在上層父元素上添加方法
onDragOver``={(e:) => {
e.``preventDefault``();
}}>
阻止默認(rèn)的禁止拖拽行為(慢慢漂移到了初始位置就是禁止默認(rèn)拖拽行為的體現(xiàn))
合適的拖拽過(guò)渡動(dòng)畫
讓畫面出現(xiàn)一個(gè)上移或下移"讓位"的效果
.drag-up?{
??animation:?dragup?ease?0.1s?1;
??animation-fill-mode:?forwards;
}
.drag-down?{
??animation:?dragdown?ease?0.1s?1;
??animation-fill-mode:?forwards;
}
@keyframes?dragup?{
??from?{
????margin-top:?10px;
??}
??to?{
????margin-top:?50px;
??}
}
@keyframes?dragdown?{
??from?{
????margin-bottom:?10px;
????margin-top:?50px;
??}
??to?{
????margin-bottom:?50px;
????margin-top:?10px;
??}
}
其他第三方庫(kù)
| 名稱 | react-dnd | react-beautiful-dnd | react-sortable-hoc |
|---|---|---|---|
| 包體積 | 較大 | 大(>100k) | 較小 |
| TS支持 | 有 | 無(wú) | 無(wú) |
| 維護(hù)情況 | 良好 | 良好 | 一年以上未曾更新 |
| 傳參方式 | event.target.dataset | event.target.dataset | ref |
| 使用難度 | 學(xué)習(xí)成本較高 | 學(xué)習(xí)成本較低 | 學(xué)習(xí)成本較低 |
| 核心實(shí)現(xiàn) | html5 drag API | html5 drag API | html5 mouse API |
| 倉(cāng)庫(kù)地址 | https://github.com/react-dnd/react-dnd | https://github.com/atlassian/react-beautiful-dnd | https://github.com/clauderic/react-sortable-hoc/issues |
其實(shí)除了最后一個(gè)庫(kù),其他兩個(gè)比較著名的庫(kù)體積都不是很小,所以簡(jiǎn)單需求原生實(shí)現(xiàn)還是有必要的
react-dnd

https://codesandbox.io/s/github/react-dnd/react-dnd/tree/gh-pages/examples_hooks_js/01-dustbin/single-target?from-embed=&file=/src/Box.jsx:437-440(體驗(yàn)demo)
React DnD建立在 HTML5 drag and drop API[7]之上。因?yàn)樗梢詫?duì)已拖動(dòng)的DOM節(jié)點(diǎn)進(jìn)行屏幕快照,并將其用作“拖動(dòng)預(yù)覽”,這樣你就不必在光標(biāo)移動(dòng)時(shí)進(jìn)行任何繪制相關(guān)的操作,同時(shí)這個(gè)API也是處理文件刪除事件的唯一方法。
但是,HTML5拖放API也有一些缺點(diǎn)。它在觸摸屏上不起作用,并且與其他瀏覽器相比,它在IE上提供的定制化更少。
這就是為什么在React DnD中以可插入方式實(shí)現(xiàn)HTML5 drag and drop API的原因。你不必非得用它,你完全可以根據(jù)原生的觸摸事件,鼠標(biāo)事件等來(lái)封裝自己的實(shí)現(xiàn)。這種可插拔的實(shí)現(xiàn)在React DnD中稱為Backends。該庫(kù)現(xiàn)在只在HTML backend中使用,未來(lái)可能會(huì)有更多地方會(huì)用到。
backends的作用類似于React的綜合事件系統(tǒng):它們都是把瀏覽器差異抽象出來(lái)并處理本地的DOM事件。不同的是,Backends并不依賴React或者React的綜合事件系統(tǒng)。底層實(shí)現(xiàn)中,backends做的就是原生的Dom事件轉(zhuǎn)換成React DnD能處理的內(nèi)部 Redux 的actions。
react-beautiful-dnd

https://react-beautiful-dnd.netlify.app/?path=/story/single-vertical-list--basic(體驗(yàn)demo)

react-sortable-hoc

http://clauderic.github.io/react-sortable-hoc/#/react-virtualized/elements-of-varying-heights?_k=dbj6xa(體驗(yàn)demo)
核心代碼
const?[dragged,?setDragged]?=?useState<any>();
const?[over,?setOver]?=?useState<any>();
const?[draggable,?setDraggable]?=?useState(false);
const?dragStart?=?(e:?any)?=>?{
??e.currentTarget.style.backgroundColor?=?"#fafafa";
??setDragged(e.currentTarget);
};
const?dragEnd?=?(e:?any)?=>?{
??e.preventDefault();
??e.target.style.display?=?"flex";
??e.target.classList.remove("drag-up");
??over.classList.remove("drag-up");
??e.target.classList.remove("drag-down");
??over.classList.remove("drag-down");
??const?from?=?cloneDeep(value[dragged.dataset.index]);
??const?to?=?cloneDeep(value[over.dataset.index]);
??splice(dragged.dataset.index,?1,?to);
??splice(over.dataset.index,?1,?from);
??e.target.style.opacity?=?"1";
??e.target.style.backgroundColor?=?"";
};
const?dragOver?=?(e:?any)?=>?{
??e.preventDefault();
??const?dgIndex?=?dragged.dataset.index;
??const?taIndex?=?e.currentTarget.dataset.index;
??const?animateName?=?dgIndex?>?taIndex???"drag-up"?:?"drag-down";
??if?(over?&&?e.currentTarget.dataset.index?!==?over.dataset.index)?{
????over.classList.remove("drag-up",?"drag-down");
??}
??if?(!e.currentTarget.classList.contains(animateName))?{
????e.currentTarget.classList.add(animateName);
????setOver(e.currentTarget);
??}
};
{items.map((item,?index)?=>?{
??return?(- ???draggable={draggable}
???data-index={index}
???onDragStart={e?=>?{
?????dragStart(e);
???}}
???onDrag={(e:?any)?=>?{
?????e.preventDefault();
?????e.target.style.opacity?=?'0';
???}}
???onDragOver={dragOver}
???onDragEnd={e?=>?{
?????dragEnd(e);
???}}>
//你的列表數(shù)組數(shù)據(jù)???
)
}
參考資料
ondragstart: https://www.runoob.com/jsref/event-ondragstart.html
[2]ondrag: https://www.runoob.com/jsref/event-ondrag.html
[3]ondragend: https://www.runoob.com/jsref/event-ondragend.html
[4]ondragover: https://www.runoob.com/jsref/event-ondragover.html
[5]ondragleave: https://www.runoob.com/jsref/event-ondragleave.html
[6]ondrop: https://www.runoob.com/jsref/event-ondrop.html
[7]HTML5 drag and drop API: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
???謝謝支持
以上便是本次分享的全部內(nèi)容,希望對(duì)你有所幫助^_^
喜歡的話別忘了?分享、點(diǎn)贊、收藏?三連哦~。
