💥不说废话,带你使用原生 JS + HTML 实现超丝滑拖拽排序效果

目标效果

一、实现初始排列

给要排序的元素添加 draggable 属性,就可以拖拽元素并为元素添加拖拽监听事件了

二、无动画拖拽排序

最简思路:

  1. 监听元素 dragstart 事件,当 元素开始被拖拽 时,改变元素样式为 虚线框
  2. 监听元素 dragenter 事件,当 当前拖拽元素与其他元素重叠 时,将 当前元素通过 DOM 插入到重叠元素的前面或后面 (向下拖插入到后面,向上拖插入到前面)
  3. 监听元素 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(播放) 。它的核心思想是通过记录初始状态和最终状态,然后通过反转来实现平滑的动画效果。具体步骤如下:

  1. First(初始状态)

    在 DOM 顺序改变前,记录每个 DOM 节点的位置。这些位置将作为动画的起始点。

  2. Last(最终状态)

    在 DOM 顺序改变后,记录每个 DOM 节点的新位置。这些位置将作为动画的终点。

  3. Invert(反转)

    计算每个 DOM 节点从初始状态到最终状态的偏移量,并使用 transform 将节点回退到初始状态,这样节点看起来就像没有移动过。(利用浏览器在当前宏任务、微任务执行完毕后才渲染的特性,使得元素好像并没有进行位移)

  4. 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>

最终效果:

相关推荐
钡铼技术ARM工业边缘计算机3 分钟前
千元级PLC平台支持梯形图+Python双开发
javascript
努力敲代码呀~22 分钟前
前端高频面试题2:浏览器/计算机网络
前端·计算机网络·html
高山我梦口香糖44 分钟前
[electron]预脚本不显示内联script
前端·javascript·electron
拉不动的猪2 小时前
安卓和ios小程序开发中的兼容性问题举例
前端·javascript·面试
贩卖纯净水.2 小时前
浏览器兼容-polyfill-本地服务-优化
开发语言·前端·javascript
程序研3 小时前
一、ES6-let声明变量【解刨分析最详细】
前端·javascript·es6
疯狂的沙粒3 小时前
在uni-app中如何从Options API迁移到Composition API?
javascript·vue.js·uni-app
尽欢i3 小时前
HTML5 拖放 API
前端·html
xiaominlaopodaren4 小时前
Three.js 光影魔法:如何单独点亮你的3D模型
javascript
PasserbyX4 小时前
一句话解释JS链式调用
前端·javascript