前言:
你还在为移动端拖拽而烦恼?其实很简单,看看移动端拖拽终极解决方案!
实现思路:
使用touch事件进行用户交互,配合document.elementFromPoint()判断是否移入目标元素。
- 在
touchstart事件获取被拖拽元素,并克隆该元素生成一个子元素,添加至拖拽面板或者根元素,使用绝对定位或者fixed定位又或者transform来实现拖拽跟随手指移动的交互交互效果。 - 在
touchmove事件使用document.elementFromPoint()获取当前手指下方的dom元素,根据实际的布局情况以及业务需求,自行向上递归寻找是否有符合拖拽进入的目标元素,并做相应的处理。注意:document.elementFromPoint()获取的元素始终为屏幕最顶层元素,所以在使用前应当先隐藏当前触点下移动的元素。 - 在
touchend事件对最后的触摸元素进行判断操作,完成相应业务。
主要实现原理:
- 移动端touch事件 ----> 用户交互
- document.elementFromPoint() ----> 判断是否移入目标元素
实现效果:
效果演示:
源代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>移动端拖拽实现</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
}
.panel {
width: 100vw;
height: 100vh;
background-color: lightblue;
overflow: hidden;
position: relative;
}
/* 容器盒子 */
.container {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
padding: 20px;
}
/* 目标容器盒子 */
.target-box {
height: 120px;
width: 120px;
background-color: rgb(180, 164, 196);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
flex-shrink: 0;
}
/* 被拖拽元素盒子 */
.drag-box {
height: 200px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 20px;
background-color: rgb(184, 210, 210);
}
/* 被拖拽元素 */
.drag-item {
width: 50px;
height: 50px;
background-color: antiquewhite;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
font-weight: bold;
cursor: grab;
user-select: none;
flex-shrink: 0;
}
/* 拖拽至目标容器上方移动时容器样式 */
.dragging-move {
border: 2px solid black;
}
</style>
</head>
<body>
<!-- 拖拽面板 -->
<div class="panel" id="panel">
<!-- 拖拽容器 -->
<div class="container">
<!-- 目标容器盒子 -->
<div class="target-box"></div>
<div class="target-box"></div>
<div class="target-box"></div>
<div class="target-box"></div>
</div>
<!-- 被拖拽容器盒子 -->
<div class="drag-box">
<!-- 被拖拽元素 -->
<div class="drag-item" draggable="true">1</div>
<div class="drag-item" draggable="true">2</div>
<div class="drag-item" draggable="true">3</div>
<div class="drag-item" draggable="true">4</div>
<div class="drag-item" draggable="true">5</div>
<div class="drag-item" draggable="true">6</div>
<div class="drag-item" draggable="true">7</div>
<div class="drag-item" draggable="true">8</div>
<div class="drag-item" draggable="true">9</div>
<div class="drag-item" draggable="true">10</div>
</div>
</div>
<script>
//拖拽进入的目标盒子的类名,也可用其他自定义属性
let targetClass = "target-box";
//获取被拖拽的元素
let dragItems = document.querySelectorAll(".drag-item");
//获取拖拽进入的目标容器盒子
let targetBoxes = document.querySelectorAll(".target-box");
//获取拖拽面板
let panel = document.getElementById("panel");
//正在移动的元素
let movingNode = null;
//移动元素的宽高
let height = 0;
let width = 0;
//拖拽进入的目标盒子元素
let target = null;
//初始化绑定事件
dragItems.forEach((item) => {
//pc端 ---> drag事件
item.addEventListener("dragstart", (e) => {
movingNode = e.target;
});
item.addEventListener("dragend", () => {
target.classList.remove("dragging-move");
});
//兼容移动端 ---> touch事件 + elementFromPoint
item.addEventListener("touchstart", (e) => {
//克隆原始被拖拽节点作为拖拽对象
movingNode = e.target.cloneNode(true);
//获取宽高用于设置移动时触点位于元素中心
height = e.target.getBoundingClientRect().height;
width = e.target.getBoundingClientRect().width;
//设置绝对定位,实现跟随手指移动,也可使用 transform
movingNode.style.position = "absolute";
movingNode.style.top = "0px";
movingNode.style.left = "0px";
let { clientX, clientY } = e.changedTouches[0];
movingNode.style.transform = `translateX(${
clientX - width / 2
}px) translateY(${clientY - height / 2}px)`;
//添加克隆节点,跟随手指移动
panel.appendChild(movingNode);
});
item.addEventListener("touchmove", (e) => {
let { clientX, clientY } = e.changedTouches[0];
movingNode.style.transform = `translateX(${
clientX - width / 2
}px) translateY(${clientY - height / 2}px)`;
//清除目标容器盒子高亮样式
targetBoxes.forEach((node) => {
node.classList.remove("dragging-move");
});
//记录元素开始的display样式
let display = movingNode.style.display;
//暂时隐藏移动元素,方便获取当前手指下方的元素(tips:document.elementFromPoint()始终获取屏幕最顶层元素)
movingNode.style.display = "none";
//获取手指下方的元素,并判断是否为移入的目标容器盒子
let targetEL = document.elementFromPoint(clientX, clientY);
//这里只向上判断了当前手指下方的元素以及他的父元素,可以根据需要使用递归函数,递归有限次
let parent = targetEL.parentNode;
if (targetEL.classList.value.indexOf(targetClass) > -1) {
//添加移入后盒子的高亮样式
targetEL.classList.add("dragging-move");
} else if (parent.classList.value.indexOf(targetClass) > -1) {
//添加移入后盒子的高亮样式
parent.classList.add("dragging-move");
}
//还原移动节点的display样式
movingNode.style.display = display;
});
item.addEventListener("touchend", (e) => {
let { clientX, clientY } = e.changedTouches[0];
let display = movingNode.style.display;
movingNode.style.display = "none";
let targetEL = document.elementFromPoint(clientX, clientY);
let parent = targetEL.parentNode;
//判断是否满足加入条件,满足进行对应的操作
if (targetEL.classList.value.indexOf(targetClass) > -1) {
targetEL.classList.remove("dragging-move");
let children = targetEL.childNodes;
if (children.length >= 4) {
target = null;
return;
}
targetEL.appendChild(item);
} else if (parent.classList.value.indexOf(targetClass) > -1) {
parent.classList.remove("dragging-move");
let children = parent.childNodes;
if (children.length >= 4) {
target = null;
return;
}
parent.appendChild(item);
}
panel.removeChild(movingNode);
});
});
//处理pc端的drag事件
targetBoxes.forEach((item) => {
item.addEventListener("dragenter", () => {
target = item;
});
item.addEventListener("dragover", (e) => {
e.preventDefault();
targetBoxes.forEach((node) => {
node.classList.remove("dragging-move");
});
item.classList.add("dragging-move");
});
item.addEventListener("drop", (e) => {
if (!target) {
return;
}
let children = target.childNodes;
if (children.length >= 4) {
target = null;
return;
}
target.appendChild(movingNode);
});
});
</script>
</body>
</html>