拖拽需求,我们工作中常常使用一些库去实现,比如:vue-draggable 或者 react-beautiful-dnd 但是,某些情况下,需要我们自己去实现,于是就有了本文...
问题描述
- 关于拖拽的需求的解决方式有两种
- 一个是使用鼠标事件来控制,比如mousedown鼠标按下、mousemove鼠标移动、mouseup鼠标松开
- 再一个就是使用js提供的drag与drop事件去控制(本文主要讲述这个)
- 在此之前,我们先回顾一下鼠标事件的方式
- 效果示例1:http://ashuai.work:8890/28
- 效果示例2:http://ashuai.work:8890/29
- 完整代码github: https://github.com/shuirongshuifu/vue3-echarts5-example
鼠标事件的拖拽示例
假设我们在页面上有一个div要设置可拖拽(相当于视口进行拖拽),比如:
<div id="draggable"></div>
想要拖拽,首先必须让其脱离文档流,也就是position: absolute;
,所以样式控制如下:
<style>
#draggable {
width: 100px;
height: 100px;
background-color: red;
/* 脱离文档流 */
position: absolute;
top: 50px;
left: 50px;
cursor: move;
}
#draggable:hover {
background-color: blue;
}
</style>
- 然后,我们在鼠标按下mousedown的时候,计算这个div距离视口的左侧和上方的距离
- 在鼠标移动的时候,通过鼠标的距离和元素的距离计算出相对于视口的left和top的值
- 把left和top的值,赋值给元素,既可实现鼠标按下、拖拽移动元素
- 当然,鼠标松开mouseup的时候,别忘了移除原先绑定的事件
- 效果图如下:
核心代码:
<body>
<div id="draggable"></div>
<script>
const draggable = document.getElementById('draggable');
// 监听鼠标按下事件
draggable.addEventListener('mousedown', (e) => {
// 计算鼠标位置与元素位置的偏移
const offsetX = e.clientX - draggable.getBoundingClientRect().left;
const offsetY = e.clientY - draggable.getBoundingClientRect().top;
// 更改元素坐标位置
function onMouseMove(e) {
draggable.style.left = e.pageX - offsetX + 'px';
draggable.style.top = e.pageY - offsetY + 'px';
}
// 移动元素
document.addEventListener('mousemove', onMouseMove);
// 松开鼠标时移除事件监听
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
// 监听鼠标松开事件
document.addEventListener('mouseup', onMouseUp);
// 防止拖动时选中文本
e.preventDefault();
});
</script>
</body>
有了上方代码的基础,接下来,我们来说一下本文的重点,Drag and Drop
Drag and Drop
- Drag and Drop的核心就是Drag和Drop(这不废话嘛)
- 我们这样理解,把A元素Drag后,然后在B元素上Drop
- 在此操作期间,会触发多个事件,我们把这个事件分为两类
- 拖拽A元素,触发的相关事件
- 放置在B元素上,触发的相关事件
拖拽A元素,触发的相关事件
- 我们可以类比上方的mouse相关的事件学习Drag事件
- 开始拖拽事件ondragstart(触发一次),类似于mousedown
- 拖拽中事件ondrag(一直触发),类似于mousemove
- 拖拽结束事件ondragend(触发一次),类似于mouseup
- 如下效果图:
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.draggable {
width: 100px;
height: 100px;
background-color: red;
margin-bottom: 20px;
cursor: move;
}
</style>
</head>
<body>
<div
class="draggable"
draggable="true"
ondragstart="console.log('ondragstart triggered')"
ondrag="console.log('ondrag triggered')"
ondragend="console.log('ondragend triggered')"
>
Drag me
</div>
</body>
</html>
注意,元素默认不能拖拽,我们需要手动开启元素可拖拽,即为:<div draggable="true" />
将拖拽的A元素,放置在可拖放的区域B元素触发的相关事件
- 这里大致可以分为4个事件:拖拽进入、拖拽悬浮、拖拽放手、拖拽离开事件
- 比如 ondragenter 就是拖拽进入事件(进入可拖放区域B元素中)
- ondragover 就是拖拽悬浮事件(悬浮在可拖放区域B元素上方)
- ondrop 就是拖拽放手事件(把拖拽过来的A元素,在可拖放区域B元素中放手)
- ondragleave 就是拖拽离开事件(带着拖拽过来的A元素,离开了可拖放区域B元素)
注意,必须在ondragover事件内,阻止默认行为,才能够执行后续的拖拽放手操作
效果图:
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
.draggable {
width: 100px;
height: 100px;
background-color: red;
margin-bottom: 20px;
cursor: move;
}
.droppable {
width: 200px;
height: 200px;
background-color: lightblue;
margin-top: 20px;
padding: 10px;
}
</style>
</head>
<body>
<!-- 可拖动元素 -->
<div class="draggable" draggable="true" ondragstart="handleDragStart(event)">
Drag me
</div>
<!-- 拖放目标区域 -->
<div class="droppable" ondragenter="handleDragEnter(event)" ondragover="handleDragOver(event)"
ondrop="handleDrop(event)" ondragleave="handleDragLeave(event)">
Drop here
</div>
<script>
function handleDragStart(event) {
console.log('ondragstart 拖拽开始');
// 可以传参:设置拖拽数据(比如文本)
event.dataTransfer.setData("text/plain", "我是拖拽的元素数据");
}
function handleDragEnter(event) {
console.log('ondragenter 进入可拖放区域');
event.target.style.background = 'lightgreen';
}
function handleDragOver(event) {
// 必须阻止默认行为,才能触发 drop
event.preventDefault();
console.log('ondragover 悬浮在可拖放区域上 100毫秒触发一次');
}
function handleDrop(event) {
event.preventDefault(); // 阻止打开被拖拽的 URL
console.log('ondrop 放下拖过来的元素');
// 接收参数数据
const data = event.dataTransfer.getData("text");
event.target.innerText = `Dropped: ${data}`;
event.target.style.background = 'green';
}
// 这个事件在拖拽元素离开可放置区域时触发(用的少一些)
function handleDragLeave(event) {
console.log('ondragleave 离开了可拖放区域');
event.target.style.background = 'pink';
}
</script>
</body>
</html>
- 值得一提的是,我们可以在拖拽开始的时候,传递一些数据
event.dataTransfer.setData("text/plain", "someData");
- 在拖拽放手的时候,接收数据
const data = event.dataTransfer.getData("text");
- 不过这种数据传递,用的不太多,更多的还是我们自己维护数据的变化
- 拖拽前和拖拽后,通过自己的逻辑操作去控制数据
上述一共有七个事件:
事件 | 触发时机 |
---|---|
ondragstart | 拖拽开始 |
ondrag | 拖拽中 |
ondragend | 拖拽结束 |
ondragenter | 进入可拖放区域 |
ondragover | 悬浮在可拖放区域 |
ondrop | 在可拖放区域放手 |
ondragleave | 离开可拖放区域 |
不过,我们开发中,常用的一般是这三个:
ondragstart 拖拽开始
和ondragover 悬浮在可拖放区域
和ondrop 在可拖放区域放手
- 当我们拖拽的时候,需要修改一下拖拽的样式,也会用到
e.dataTransfer.effectAllowed
属性 - effectAllowed 的值有不少,详情见:https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer...
- 一般,我们在拖拽开始的时候,会将其设置为'move' 默认是 'copy',具体移动过去还是复制过去,看大家需求
- move和copy效果图如下:
move图示:
copy图示:
至此,Drag and Drop拖放知识点,就学习的差不多了,接下来,我们通过两个案例,来演示一下具体的应用
案例一 普通的拖拽(拖放)
效果图
思路分析
- 需求就是把上方的内容表情emoji,拖拽到下方盒子里面去
- 为了方便,我们的监听拖拽的事件,可以直接绑定在最外层(包含上方的emoji和下方的盒子)
- 然后,只给需要拖拽的emoji开启draggable="true"
- 而后在页面初始化的时候,绑定拖拽监听事件
- 当拖拽过去放手的时候,把拖拽的emoji给添加到下方
- 同时,把上方的原来的emoji给删除即可
代码
复制粘贴即用
<template>
<div class="tenBox" ref="dragBoxWrap">
<h3>拖拽元素示例</h3>
<div class="emojiList">
<div class="emojiItem" v-for="item in emojiList" :key="item.name">
<div class="emojiItem-content">
<div class="emojiItem-content-name">{{ item.name }}</div>
<!-- draggable="true"开启元素可以拖拽,必须要加 -->
<div class="emojiItem-content-emoji" draggable="true">{{ item.emoji }}</div>
</div>
</div>
</div>
<!-- 四个摆放盒子 -->
<div class="boxList">
<div class="boxItem" v-for="item in boxList" :key="item.name">
<!-- 名字绝对定位 -->
<div class="boxItem-content-name">{{ item.name }}</div>
<div class="boxItem-content" :data-emoji="item.name" :draggable="item.emoji ? true : false">{{
item.emoji }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
const emojiList = ref([
{
name: '微笑',
emoji: '😊'
},
{
name: '惊讶',
emoji: '😲'
},
{
name: '生气',
emoji: '🤬'
},
{
name: '尴尬',
emoji: '😢'
},
])
const boxList = ref([
{
name: '1',
emoji: ''
},
{
name: '2',
emoji: ''
},
{
name: '3',
emoji: ''
},
{
name: '4',
emoji: ''
}
])
const dragBoxWrap = ref(null)
const curDragEmoji = ref(null)
onMounted(() => {
initDrag()
})
const initDrag = () => {
dragBoxWrap.value.ondragstart = (e) => {
// 获取当前拖拽的元素
curDragEmoji.value = e.target
// 设置拖拽效果
e.dataTransfer.effectAllowed = "move";
}
dragBoxWrap.value.ondragover = (e) => {
// 阻止默认行为,允许drop
if (e.target.className === 'boxItem-content') {
e.preventDefault()
}
// console.log(e.target.dataset.emoji)
// let whichBox = Number(e.target?.dataset?.emoji)
// if (whichBox) {
// if (boxList.value[whichBox - 1].emoji) {
// e.preventDefault()
// e.target.style.backgroundColor = 'red'
// return
// }
// }
}
dragBoxWrap.value.ondrop = (e) => {
// 下方添加的时候,首先判断是否有值,有值不允许添加
let whichBox = Number(e.target.dataset.emoji)
if (boxList.value[whichBox - 1].emoji) {
alert('已被占用')
return
}
boxList.value[whichBox - 1].emoji = curDragEmoji.value.textContent
// 上方删除
// 方式一(不太建议)
// curDragEmoji.value.textContent = ''
// 方式二(不太建议)
// curDragEmoji.value.remove()
// 方式三(不太建议)
// curDragEmoji.value.style.display = 'none'
// 方式四(建议)
let curDragEmojiContent = curDragEmoji.value.textContent
let curDragEmojiIndex = emojiList.value.findIndex(item => item.emoji === curDragEmojiContent)
emojiList.value[curDragEmojiIndex].emoji = ''
}
}
</script>
<style>
.tenBox {
width: 100%;
height: 90vh;
position: relative;
}
.emojiList {
display: flex;
flex-wrap: wrap;
}
.emojiItem {
width: 100px;
height: 100px;
border: 1px solid #ccc;
border-radius: 10px;
margin: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.emojiItem-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.emojiItem-content-name {
font-size: 12px;
color: #333;
}
.emojiItem-content-emoji {
font-size: 36px;
color: #333;
cursor: move;
user-select: none;
}
.boxList {
display: flex;
flex-wrap: wrap;
margin-top: 54px;
}
.boxItem {
width: 100px;
height: 100px;
border: 1px solid #ccc;
border-radius: 10px;
margin: 10px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.boxItem-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
}
.boxItem-content-name {
position: absolute;
top: -60px;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
案例二 表格的拖拽(拖放)
效果图
思路分析
- 上述效果图想要实现的思路和案例一的差不多
- 不过需要注意的一点就是,一定要预先通过js把表格的单元格给设置为可拖拽、可放置
完整代码
<template>
<div class="boxWrap">
<div class="main">
<div class="down">
<div class="leftTable" ref="dragBoxWrap">
<div class="t1">
<el-table :data="tableData1" border style="width: 100%">
<el-table-column prop="type" label="类型" width="74" align="center" />
</el-table>
</div>
<div class="t2">
<el-table ref="table2" height="400" :data="tableData2" border style="width: 100%">
<el-table-column prop="way" label="对应策略管理办法" align="center" />
</el-table>
</div>
<div class="t3">
<el-table ref="table3" :data="tableData3" border style="width: 100%; margin-left: 24px;">
<el-table-column prop="way" label="可选策略" align="center" />
</el-table>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
const dragBoxWrap = ref(null); // 总拖拽dom
const tableData1 = ref([
{ type: 'A类' },
{ type: 'A类' },
{ type: 'A类' },
{ type: 'A类' },
{ type: 'B类' },
{ type: 'B类' },
{ type: 'B类' },
{ type: 'C类' },
{ type: 'C类' },
])
const tableData2 = ref([
{ way: '', },
{ way: '', },
{ way: '', },
{ way: '', },
{ way: '', },
{ way: '', },
{ way: '', },
{ way: '', },
{ way: '', },
])
const tableData3 = ref([
{ way: '投入更多时间' },
{ way: '投入更多金钱' },
{ way: '想办法提升效率' },
{ way: '想办法扩张规模' },
{ way: '招聘更加优秀的人才' },
{ way: '团队优化' },
{ way: '寻求咨询公司的指导建议' },
{ way: '求神问佛' },
{ way: '躺平摆烂' },
])
const table2 = ref(null);
const table3 = ref(null);
onMounted(() => {
setTimeout(() => {
initDarg();
}, 0);
})
const curTd = ref(null); // 当前拖拽的td
const initDarg = () => {
// 允许t3的单元格被拖拽
const t3Rows = table3.value.$el.querySelectorAll('tbody tr');
t3Rows.forEach((tr, index) => {
const td = tr.querySelector('td');
if (td) {
td.draggable = true;
td.dataset.index = index;
td.style.cursor = 'move';
}
});
// 允许t2的单元格被放置上去
const t2Rows = table2.value.$el.querySelectorAll('tbody tr');
t2Rows.forEach((tr, index) => {
const td = tr.querySelector('td');
if (td) {
td.classList.add('flag');
td.dataset.index = index;
}
});
// 拖拽开始事件
dragBoxWrap.value.ondragstart = (e) => {
if (!e.target.innerText) {
return;
}
// 改变样式
e.dataTransfer.effectAllowed = "move";
// 存一份
curTd.value = e.target;
}
// 拖拽悬浮事件
dragBoxWrap.value.ondragover = (e) => {
// 允许元素被拖拽放上去
if (
e.target.classList.contains("flag")
) {
e.preventDefault();
}
};
// 拖拽放下事件
dragBoxWrap.value.ondrop = (e) => {
if (!curTd.value.innerText) {
return;
}
// t2赋值
tableData2.value[e.target.dataset.index].way = curTd.value.innerText;
// t3清空值
tableData3.value[curTd.value.dataset.index].way = null;
}
}
</script>
<style scoped lang="less">
.boxWrap {
width: 100%;
height: 100%;
position: relative;
box-sizing: border-box;
.star {
position: absolute;
writing-mode: vertical-lr;
font-weight: 700;
color: #2c5896;
letter-spacing: 12px;
font-size: 20px;
left: 85px;
top: 0;
user-select: none;
}
.main {
width: 850px;
height: 558px;
background-color: #fff;
margin-left: 120px;
text-align: center;
padding: 0 15px;
padding-top: 12px;
.title {
height: 45px;
line-height: 45px;
text-align: center;
color: #fff;
font-size: 24px;
background-color: #589fd7;
margin-bottom: 6px;
}
.up {
text-align: left;
font-size: 14px;
}
.down {
display: flex;
align-items: center;
margin-top: 8px;
width: 100%;
.leftTable {
width: 100%;
display: flex;
.t1 {
width: 9%;
position: relative;
right: -6px;
z-index: 111;
}
.t2 {
width: 40%;
}
.t3 {
width: 40%;
margin-left: 36px;
}
}
:deep(.el-table) {
font-size: 13px;
font-weight: 600;
.el-table__header th {
background-color: #3874c8 !important;
color: #fff;
font-size: 13px;
}
.el-table__header th:last-child {
background-color: #3874c8 !important;
color: #fff;
font-size: 13px;
}
thead {
font-size: 16px;
}
tr {
height: 40px;
}
}
}
}
}
</style>
参考文章:https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_an...
欢迎大家多多star,您的支持,是我创作的动力呦 😊😊😊
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。