拖拽排序是一種在網(wǎng)頁設(shè)計(jì)和應(yīng)用程序中常見的交互方式,允許用戶通過鼠標(biāo)或觸摸操作來重新排列頁面或界面上的元素。這種交互方式對于提升用戶體驗(yàn)和操作效率具有重要意義。
在拖拽排序中,用戶可以用鼠標(biāo)或手指按住某個元素,然后將其拖動到新
的位置,從而實(shí)現(xiàn)對元素的重新排列。這種操作直觀且靈活,使得用戶可以根據(jù)自己的需求隨時(shí)調(diào)整頁面或界面的布局,提升了個性化體驗(yàn)。同時(shí),拖拽排序也增加了用戶的參與度和粘性,用戶可以通過自由選擇和排序感興趣的內(nèi)容,提升留存率和活躍度。
從技術(shù)實(shí)現(xiàn)的角度來看,拖拽排序主要依賴于前端技術(shù)的支持。例如,基于JavaScript的實(shí)現(xiàn)方法主要是通過監(jiān)聽鼠標(biāo)或觸摸事件來實(shí)現(xiàn)。在拖拽開始時(shí),需要記錄拖拽元素的位置,然后在拖拽過程中更新元素的位置,最后在拖拽結(jié)束時(shí)判斷元素與其他元素的位置關(guān)系并進(jìn)行排序。
在拖拽排序的應(yīng)用場景中,列表排序和圖片排序是兩個典型的例子。在列表排序中,用戶可以通過拖動列表項(xiàng)來改變它們的順序,這在任務(wù)管理應(yīng)用、待辦事項(xiàng)列表等場景中非常常見。在圖片排序中,用戶可以通過拖動圖片來改變它們的順序,這在圖片庫或相冊應(yīng)用中較為常見。
在HTML中,我們給需要拖動的元素加上draggable="true"就可以實(shí)現(xiàn)拖拽效果了。在CSS中,我們設(shè)置了列表和拖拽項(xiàng)的樣式。
<div class="list">
<div draggable="true" class="list-item">1</div>
<div draggable="true" class="list-item">2</div>
<div draggable="true" class="list-item">3</div>
<div draggable="true" class="list-item">4</div>
<div draggable="true" class="list-item">5</div>
<div draggable="true" class="list-item">6</div>
</div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
}
.list {
width: 600px;
margin-top: 100px;
}
.list-item {
margin: 6px 0;
padding: 0 20px;
line-height: 40px;
height: 40px;
background: #409eff;
color: #fff;
text-align: center;
cursor: move;
user-select: none;
border-radius: 5px;
}
效果如下:
元素是可以拖拽了,但是拖拽時(shí)元素本身的樣式要改變,我們只需要給元素加上一個類樣式就可以了。那么,什么時(shí)候添加這個類呢?當(dāng)然是開始拖動的時(shí)候,我們使用了HTML5的拖放API ondragstart,它是在用戶開始拖動元素時(shí)觸發(fā)。
2.1 拖拽開始
我們找到拖拽項(xiàng)的父元素,用事件委托的方式找到父元素,也就是.list并給它注冊一個ondragstart事件,當(dāng)拖拽開始時(shí),可以使用event.target來獲取被拖拽的元素,給它的類型樣式添加一個moving。
.list-item.moving {
background: transparent;
color: transparent;
border: 1px dashed #ccc;
}
const list = document.querySelector('.list');
list.ondragstart = (e) => {
setTimeout(() => {
e.target.classList.add('moving')
}, 0)
}
為什么要加setTimeout呢?因?yàn)?span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; font-size: var(--articleFontsize); letter-spacing: 0.034em;">跟隨鼠標(biāo)的樣式取決于拖拽開始時(shí)元素本身的樣式,拖轉(zhuǎn)開始時(shí)把元素的樣式改變了,那就意味著跟隨鼠標(biāo)的樣式也改變了,我們可以加一個setTimeout變成異步,在拖拽開始時(shí)還是保持原來的樣式,然后過一點(diǎn)點(diǎn)時(shí)間在變成添加moving的樣式。
2.2 拖拽過程
(1)當(dāng)被拖拽的元素移動到另一個列表項(xiàng)上方時(shí),會不斷觸發(fā)dragover事件。
(2)默認(rèn)情況下,瀏覽器不允許放置(drop)操作,因此需要阻止這個事件的默認(rèn)行為。這可以通過調(diào)用event.preventDefault()方法來實(shí)現(xiàn)。
ondragover: 當(dāng)某被拖動的對象在另一對象容器范圍內(nèi)拖動時(shí)觸發(fā)此事件。
list.ondragover = (e) => {
e.preventDefault();
}
(3)當(dāng)用戶釋放鼠標(biāo)按鈕,且被拖拽的元素位于一個有效的放置目標(biāo)上方時(shí),drop事件被觸發(fā)。
(4)在drop事件處理程序中,首先需要獲取拖拽源元素,接著獲取放置目標(biāo)元素,這通常是觸發(fā)drop事件的元素。
(5)然后,需要更新DOM來反映新的排序。這通常涉及改變元素的位置,可以通過直接操作DOM(如insertBefore或appendChild)來實(shí)現(xiàn)。
ondragenter:當(dāng)被鼠標(biāo)拖動的對象進(jìn)入其容器范圍內(nèi)時(shí)觸發(fā)此事件。
const list = document.querySelector('.list');
// 記錄被拖拽的元素
let sourceNode;
list.ondragstart = (e) => {
setTimeout(() => {
e.target.classList.add('moving')
}, 0)
// 記錄被拖拽的元素
sourceNode = e.target;
}
list.ondragover = (e) => {
e.preventDefault();
}
list.ondragenter = e => {
e.preventDefault();
// 判斷拖拽元素進(jìn)入的元素等于父元素list或等于拖拽元素本身,
// 不做受任何處理,直接結(jié)束
if(e.target === list || e.target === sourceNode) {
return;
}
// 判斷元素拖拽進(jìn)入的位置是在目標(biāo)的上面還是下面,
// 比如拖動3進(jìn)入到4時(shí),4要移動到上面,
// 當(dāng)拖動3進(jìn)入到2時(shí),2要移動到下面,
// 通過元素所處的下表既可判斷。
// 首先,拿到元素list所有的子元素
const children = [...list.children];
// 接著,拿到要拖拽元素在整個子元素里面的下標(biāo)
const sourceIndex = children.indexOf(sourceNode);
// 然后,拿到要進(jìn)入目標(biāo)元素在整個子元素里面的下標(biāo)
const targetIndex = children.indexOf(e.target);
if(sourceIndex < targetIndex) {
// 進(jìn)入目標(biāo)元素大于拖拽元素的下標(biāo),
// 此時(shí)要插入目標(biāo)元素的下方位置,
// 也就是目標(biāo)元素下一個元素的前面
list.insertBefore(sourceNode, e.target.nextElementSibling);
} else {
// 進(jìn)入目標(biāo)元素小于拖拽元素的下標(biāo),
// 此時(shí)要插入目標(biāo)元素的上方位置,
// 也就是目標(biāo)元素前面的位置
list.insertBefore(sourceNode, e.target);
}
}
2.3 拖拽結(jié)束
ondragend:用戶完成元素拖動后觸發(fā)。
list.ondragend = () => {
sourceNode.classList.remove('moving');
}
拖拽結(jié)束時(shí),只需要把moving的樣式移除即可。
為了使元素位置改變時(shí)不那么生硬,可能需要提供一些額外的反饋,可以通過動畫來平滑地展示元素位置的改變。那么我們來了解一種動畫——Flip動畫。什么是Flip動畫呢?
Flip技術(shù)可以讓我們的動畫更加流暢,同時(shí)也能降低復(fù)雜動畫的開發(fā)難度。其實(shí),Flip是幾個英文單詞的縮寫。
F:Fist —— 一個元素的起始位置。
L:Last —— 另一個元素的終止位置,注意另一個這個詞,后面會有具體代碼的體現(xiàn)。
I:Invert —— 計(jì)算"F"與"L"的差異,包括位置,大小等,并將差異用transform屬性,添加到終止元素上,讓它回到起始位置,也是此項(xiàng)技術(shù)的核心。
P:Play —— 添加transtion 過渡效果,清除Invert階段添加進(jìn)來transform,播放動畫。
直接上帶代碼:
// Flip.js
const Flip = (function () {
class FlipDom {
constructor(dom, duration = 0.5) {
this.dom = dom;
this.transition =
typeof duration === 'number' ? `${duration}s` : duration;
this.firstPosition = {
x: null,
y: null,
};
this.isPlaying = false;
this.transitionEndHandler = () => {
this.isPlaying = false;
this.recordFirst();
}
}
getDomPosition() {
const rect = this.dom.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
}
}
recordFirst(firstPosition) {
if (!firstPosition) {
firstPosition = this.getDomPosition()
}
this.firstPosition.x = firstPosition.x;
this.firstPosition.y = firstPosition.y;
}
* play() {
if (!this.isPlaying) {
this.dom.style.transition = 'none';
const lastPosition = this.getDomPosition();
const dis = {
x: lastPosition.x - this.firstPosition.x,
y: lastPosition.y - this.firstPosition.y,
}
if (!dis.x && !dis.y) {
return;
}
this.dom.style.transform = `translate(${-dis.x}px, ${-dis.y}px)`;
yield 'moveToFirst';
this.isPlaying = true;
}
this.dom.style.transition = this.transition;
this.dom.style.transform = 'none';
this.dom.removeEventListener('transitionend', this.transitionEndHandler);
this.dom.addEventListener('transitionend', this.transitionEndHandler);
}
}
class Flip {
constructor(doms, duration = 0.5) {
this.flipDoms = [...doms].map((it) => new FlipDom(it, duration));
this.flipDoms = new Set(this.flipDoms);
this.duration = duration;
this.flipDoms.forEach((it) => it.recordFirst());
}
addDom(dom, firstPosition) {
const flipDom = new FlipDom(dom, this.duration);
this.flipDoms.add(flipDom)
flipDom.recordFirst(firstPosition)
}
play() {
let gs = [...this.flipDoms].map((it) => {
const generator = it.play();
return {
generator,
iteratorResult: generator.next()
}
})
.filter((g) => !g.iteratorResult.done);
while (gs.length > 0) {
document.body.clientWidth;
gs = gs.map((g) => {
g.iteratorResult = g.generator.next();
return g;
})
.filter((g) => !g.iteratorResult.done);
}
}
}
return Flip;
})();
完整代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
}
.list {
width: 600px;
margin-top: 100px;
}
.list-item {
margin: 6px 0;
padding: 0 20px;
line-height: 40px;
height: 40px;
background: #409eff;
color: #fff;
text-align: center;
cursor: move;
user-select: none;
border-radius: 5px;
}
.list-item.moving {
background: transparent;
color: transparent;
border: 1px dashed #ccc;
}
</style>
</head>
<body>
<div class="list">
<div draggable="true" class="list-item">1</div>
<div draggable="true" class="list-item">2</div>
<div draggable="true" class="list-item">3</div>
<div draggable="true" class="list-item">4</div>
<div draggable="true" class="list-item">5</div>
<div draggable="true" class="list-item">6</div>
</div>
</body>
<script src="./flip.js"></script>
<script>
const list = document.querySelector('.list');
// 記錄被拖拽的元素
let sourceNode;
let flip;
list.ondragstart = (e) => {
setTimeout(() => {
e.target.classList.add('moving')
}, 0)
sourceNode = e.target;
flip = new Flip(list.children, 0.5);
}
list.ondragover = (e) => {
e.preventDefault();
}
list.ondragenter = e => {
e.preventDefault();
// 判斷拖拽元素進(jìn)入的元素等于父元素list或等于拖拽元素本身,
// 不做受任何處理,直接結(jié)束
if(e.target === list || e.target === sourceNode) {
return;
}
// 判斷元素拖拽進(jìn)入的位置是在目標(biāo)的上面還是下面,
// 比如拖動3進(jìn)入到4時(shí),4要移動到上面,
// 當(dāng)拖動3進(jìn)入到2時(shí),2要移動到下面,
// 通過元素所處的下表既可判斷。
// 首先,拿到元素list所有的子元素
const children = [...list.children];
// 接著,拿到要拖拽元素在整個子元素里面的下標(biāo)
const sourceIndex = children.indexOf(sourceNode);
// 然后,拿到要進(jìn)入目標(biāo)元素在整個子元素里面的下標(biāo)
const targetIndex = children.indexOf(e.target);
if(sourceIndex < targetIndex) {
// 進(jìn)入目標(biāo)元素大于拖拽元素的下標(biāo),
// 此時(shí)要插入目標(biāo)元素的下方位置,
// 也就是目標(biāo)元素下一個元素的前面
list.insertBefore(sourceNode, e.target.nextElementSibling);
} else {
// 進(jìn)入目標(biāo)元素小于拖拽元素的下標(biāo),
// 此時(shí)要插入目標(biāo)元素的上方位置,
// 也就是目標(biāo)元素前面的位置
list.insertBefore(sourceNode, e.target);
}
// 調(diào)用flip動畫play方法
flip.play();
}
list.ondragend = () => {
sourceNode.classList.remove('moving');
}
</script>
</html>
該文章在 2024/4/12 23:19:02 編輯過