目标效果

一、实现初始排列
给要排序的元素添加 draggable 属性,就可以拖拽元素并为元素添加拖拽监听事件了

二、无动画拖拽排序
最简思路:
- 监听元素
dragstart
事件,当 元素开始被拖拽 时,改变元素样式为 虚线框 - 监听元素
dragenter
事件,当 当前拖拽元素与其他元素重叠 时,将 当前元素通过 DOM 插入到重叠元素的前面或后面 (向下拖插入到后面,向上拖插入到前面) - 监听元素
dragend
事件,当 元素结束拖拽 后,移除虚线框样式
代码实现:
js
<script>
//事件委托通过冒泡机制触发事件
const box = document.querySelector(".box");
let sourceNode; //记录当前被拖拽的元素,用于判断现在是向上拖还是向下拖
box.ondragstart = (e) => {
setTimeout(() => {
e.target.classList.add("dragging"); // 虚线框样式
}, 0);
sourceNode = e.target;
};
box.ondragend = (e) => {
e.target.classList.remove("dragging");
};
box.ondragover = (e) => {
e.preventDefault(); // 阻止默认行为,允许放置
};
box.ondragenter = (e) => {
e.preventDefault(); // 阻止默认行为,允许放置
//对于容器元素跟自己,不做处理
if (e.target === box || e.target === sourceNode) return;
//通过索引判断是向上拖还是向下拖
const children = Array.from(box.children);
const sourceIndex = children.indexOf(sourceNode);
const targetIndex = children.indexOf(e.target);
//插入操作
if (sourceIndex < targetIndex) {
box.insertBefore(sourceNode, e.target.nextSibling);
} else {
box.insertBefore(sourceNode, e.target);
}
};
</script>
效果如下:

三、加入动画
CSS 动画通常是基于具体的属性变化进行的,但在这个场景中,我们改变的不是任何 CSS 属性,而是 DOM 节点的位置。因此,要添加过渡动画,就需要使用一个新的动画思路:FLIP。
FLIP 是一种动画技术,全称为 First(初始状态)、Last(最终状态)、Invert(反转)、Play(播放) 。它的核心思想是通过记录初始状态和最终状态,然后通过反转来实现平滑的动画效果。具体步骤如下:
-
First(初始状态) :
在 DOM 顺序改变前,记录每个 DOM 节点的位置。这些位置将作为动画的起始点。
-
Last(最终状态) :
在 DOM 顺序改变后,记录每个 DOM 节点的新位置。这些位置将作为动画的终点。
-
Invert(反转) :
计算每个 DOM 节点从初始状态到最终状态的偏移量,并使用
transform
将节点回退到初始状态,这样节点看起来就像没有移动过。(利用浏览器在当前宏任务、微任务执行完毕后才渲染的特性,使得元素好像并没有进行位移) -
Play(播放) :
去除上一步添加的
transform
属性,并添加transition
进行过渡,完成动画效果。
觉得抽象的可以看看这个视频 FLIP动画讲解
Flip 类实现:
js
class Flip {
constructor(elements, duration = 0.3) {
this.elements = elements; // 传入要监听的元素列表
this.duration = duration; // 过渡时间
this.firstMap = new Map(); // 记录元素 DOM 移动前位置
this.lastMap = new Map(); // 记录元素 DOM 移动后位置
}
//循环获取元素的初始位置
getFirstPosition() {
this.elements.forEach((ele) => {
const rect = ele.getBoundingClientRect();
this.firstMap.set(ele, {
left: rect.left,
top: rect.top,
});
});
}
//
play() {
this.elements.forEach((ele) => {
ele.style.removeProperty("transition");// 清除过渡效果,使元素瞬间回到初始位置
// 记录元素 DOM 移动后位置
const rect = ele.getBoundingClientRect();
this.lastMap.set(ele, {
left: rect.left,
top: rect.top,
});
const first = this.firstMap.get(ele);
const last = this.lastMap.get(ele);
// 计算正确的偏移量(初始位置 - 最终位置)
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
// invert操作,使元素在渲染下一帧前瞬间移动到正确的位置
ele.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
requestAnimationFrame(() => {
// 应用动画
ele.style.transition = `transform ${this.duration}s ease`;
ele.style.removeProperty("transform");
this.lastMap.clear();
this.firstMap.clear();
});
});
}
}
完整 HTML 代码:
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FLIP</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.box {
padding: 10px;
height: fit-content;
width: 600px;
background-color: transparent;
border: 2px solid black;
gap: 8px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.draggable {
width: 100%;
height: 48px;
background-color: #22c55e;
border-radius: 8px;
color: white;
line-height: 48px;
padding-left: 16px;
font-size: 1.25rem;
cursor: pointer;
}
.dragging {
background: transparent;
color: transparent;
border: 1px dashed #ccc;
}
</style>
</head>
<body class="container">
<div class="box">
<div draggable="true" class="draggable">1</div>
<div draggable="true" class="draggable">2</div>
<div draggable="true" class="draggable">3</div>
<div draggable="true" class="draggable">4</div>
<div draggable="true" class="draggable">5</div>
</div>
</body>
<script src="./flip.js"></script>
<script>
const box = document.querySelector(".box");
let sourceNode;
// 创建flip动画实例,监听列表元素
const flip = new Flip(Array.from(box.children), 0.3);
box.ondragstart = (e) => {
setTimeout(() => {
e.target.classList.add("dragging");
}, 0);
sourceNode = e.target;
};
box.ondragend = (e) => {
e.target.classList.remove("dragging");
};
box.ondragover = (e) => {
e.preventDefault();
};
box.ondragenter = (e) => {
e.preventDefault();
if (e.target === box || e.target === sourceNode) return;
const children = Array.from(box.children);
const sourceIndex = children.indexOf(sourceNode);
const targetIndex = children.indexOf(e.target);
// DOM 顺序改变前记录元素的初始位置
flip.getFirstPosition();
if (sourceIndex < targetIndex) {
box.insertBefore(sourceNode, e.target.nextSibling);
} else {
box.insertBefore(sourceNode, e.target);
}
// 合并 last、invert、play操作
flip.play();
};
</script>
</html>
最终效果:
