拖拽DOM和组件中的Popup拖拽
发表于:2024-12-16 |

前言

最近来了个离谱的需求,那就是需要将组件库的 Popup 实现可拖拽,之前我搞过普通 dom 的拖拽,想着能不能复用一下,结果发现的确可以,本篇文章就来讲解一下如何实现。

普通 DOM 实现拖拽

这个我记得我好像之前写过文章了,但我好像没找到这篇文章,那就简单说一下如何实现普通 dom 的拖拽。

绘制一个普通的 dom

以下代码使用 vue3 实现,首先,先随便画个 dom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="drag-dom">我是可以拖拽的dom</div>
</template>
<script setup></script>
<style scoped>
.drag-dom {
width: 200px;
height: 200px;
background-color: #f00;
cursor: move;
color: white;
text-align: center;
line-height: 200px;
border: 1px solid #000;
}
</style>

此时的效果就如下:
效果图

绘制基本的架构

先搭建一个基本的架构,这里我定义了基本逻辑

  1. 鼠标按下开始拖拽
  2. 鼠标移动进行拖拽
  3. 鼠标松开停止拖拽
  4. 组件注销移除这些鼠标监听事件
  5. 定义组件为 fixed,或者 absolute,能让其受 top,left 属性控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<template>
<div class="wrapper">
<div
class="drag-dom"
ref="dragDomRef"
:style="{ top: position.top + 'px', left: position.left + 'px' }"
@mousedown="handleMousedown"
>
我是可以拖拽的dom
</div>
</div>
</template>
<script setup>
import { ref, reactive, onBeforeUnmount } from "vue";

// 当前的dom
const dragDomRef = ref(null);

// 位置
const position = reactive({
top: 200,
left: 800,
});

// 是否在拖拽
let dragging = false;
// 鼠标起始位置
let mouseStart = { x: 0, y: 0 };
// 元素起始位置
let elementStart = { x: 0, y: 0 };

// 拖拽事件
const doDrag = () => {};

// 停止拖拽事件
const stopDragging = () => {};

// 鼠标按下事件
const handleMousedown = () => {
dragDomRef.value.addEventListener("mousemove", doDrag, false);
dragDomRef.value.addEventListener("mouseup", stopDragging, false);
};

// 组件注销的时候清除鼠标监听事件
onBeforeUnmount(() => {
dragDomRef.value.removeEventListener("mousemove", doDrag, false);
dragDomRef.value.removeEventListener("mouseup", stopDragging, false);
});
</script>
<style>
* {
padding: 0;
margin: 0;
}
</style>
<style scoped>
.wrapper {
display: flex;
justify-content: center;
width: 100vw;
height: 100vh;
align-items: center;
}
.drag-dom {
width: 200px;
height: 200px;
background-color: #f00;
cursor: move;
color: white;
text-align: center;
line-height: 200px;
border: 1px solid #000;
position: fixed;
}
</style>

鼠标按下事件

在鼠标按下的时候,我们给起始位置进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
// 鼠标按下事件
const handleMousedown = (e) => {
e.preventDefault();
dragging = true;
// 获取鼠标起始位置
mouseStart.x = e.clientX;
mouseStart.y = e.clientY;
// 获取元素起始位置
elementStart.x = position.left;
elementStart.y = position.top;
dragDomRef.value.addEventListener("mousemove", doDrag, false);
dragDomRef.value.addEventListener("mouseup", stopDragging, false);
};

鼠标移动事件

我们根据鼠标移动的距离设置元素需要移动的距离,即可实现拖拽的效果

1
2
3
4
5
6
7
8
9
10
// 拖拽事件
const doDrag = (e) => {
if (!dragging) return;
// 获取鼠标移动的距离
const dx = e.clientX - mouseStart.x;
const dy = e.clientY - mouseStart.y;
// 根据鼠标移动的距离设置元素的位置
position.left = Math.max(0, elementStart.x + dx);
position.top = Math.max(0, elementStart.y + dy);
};

鼠标松开事件

1
2
3
4
5
6
// 停止拖拽事件
const stopDragging = () => {
dragging = false;
dragDomRef.value.removeEventListener("mousemove", doDrag, false);
dragDomRef.value.removeEventListener("mouseup", stopDragging, false);
};

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<template>
<div class="wrapper">
<div
class="drag-dom"
ref="dragDomRef"
:style="{ top: position.top + 'px', left: position.left + 'px' }"
@mousedown="handleMousedown"
>
我是可以拖拽的dom
</div>
</div>
</template>
<script setup>
import { ref, reactive, onBeforeUnmount } from "vue";

// 当前的dom
const dragDomRef = ref(null);

// 位置
const position = reactive({
top: 200,
left: 800,
});

// 是否在拖拽
let dragging = false;
// 鼠标起始位置
let mouseStart = { x: 0, y: 0 };
// 元素起始位置
let elementStart = { x: 0, y: 0 };

// 拖拽事件
const doDrag = (e) => {
if (!dragging) return;
// 获取鼠标移动的距离
const dx = e.clientX - mouseStart.x;
const dy = e.clientY - mouseStart.y;
// 根据鼠标移动的距离设置元素的位置
position.left = Math.max(0, elementStart.x + dx);
position.top = Math.max(0, elementStart.y + dy);
};

// 停止拖拽事件
const stopDragging = () => {
dragging = false;
dragDomRef.value.removeEventListener("mousemove", doDrag, false);
dragDomRef.value.removeEventListener("mouseup", stopDragging, false);
};

// 鼠标按下事件
const handleMousedown = (e) => {
e.preventDefault();
dragging = true;
// 获取鼠标起始位置
mouseStart.x = e.clientX;
mouseStart.y = e.clientY;
// 获取元素起始位置
elementStart.x = position.left;
elementStart.y = position.top;
dragDomRef.value.addEventListener("mousemove", doDrag, false);
dragDomRef.value.addEventListener("mouseup", stopDragging, false);
};

// 组件注销的时候清除鼠标监听事件
onBeforeUnmount(() => {
dragDomRef.value.removeEventListener("mousemove", doDrag, false);
dragDomRef.value.removeEventListener("mouseup", stopDragging, false);
});
</script>
<style>
* {
padding: 0;
margin: 0;
}
</style>
<style scoped>
.wrapper {
display: flex;
justify-content: center;
width: 100vw;
height: 100vh;
align-items: center;
}
.drag-dom {
width: 200px;
height: 200px;
background-color: #f00;
cursor: move;
color: white;
text-align: center;
line-height: 200px;
border: 1px solid #000;
position: fixed;
}
</style>

此时,我们的 dom 就可以进行拖拽了

需求最近我要实现 tdesign 的 Popup 组件拖拽

查看 Popup 的 dom

通过观察,我察觉到了这个 popup 组件内部其实时通过 transform 来实现的,那我就有了思路,我通过鼠标移动位置重新计算他的 transform 不就好了吗,说干就干。
观察dom

代码实现 Popup 拖拽

给浮层添加类

首先我们需要给这个浮层添加个类,用来获取这个 dom,通过文档可以观察到可以通过这个属性添加。
文档

1
2
3
4
5
6
<t-popup :visible="true" overlayClassName="popup-dom">
<template #content>
<div class="drag-dom" @mousedown="handleMousedown">我是popup的内容盒子</div>
</template>
<t-button>测试popup拖拽</t-button>
</t-popup>
1
2
3
4
// 浮层dom
const curPopupDom = computed(() => {
return document.querySelector(".popup-dom");
});
定义鼠标按下事件

这里之前我们用的 position 是我们定义好的位置,这里我们 dom 的初始位置就需要用这个浮层自带的 css 的 transform 属性了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 鼠标按下事件
const handleMousedown = (e) => {
e.preventDefault();
dragging = true;
// 获取鼠标起始位置
mouseStart.x = e.clientX;
mouseStart.y = e.clientY;
// 得到后的示例translate("815px","500px")
const transformEle = curPopupDom.value.style.transform;
// 得到括号里面的值
const reg = /\(([^)]*)\)/;
const result = reg.exec(transformEle);
if (result) {
const [x, y] = result[1].split(",");
elementStart.x = parseFloat(x);
elementStart.y = parseFloat(y);
console.log(elementStart, "elementStart");
curPopupDom.value.addEventListener("mousemove", doDrag, false);
curPopupDom.value.addEventListener("mouseup", stopDragging, false);
}
};

效果图

此时我们已经得到了元素起始位置,后面的逻辑就是和前面是一样的了,就是把 position 变成 transform 而已

1
2
3
4
5
6
7
8
9
10
11
// 拖拽事件
const doDrag = (e) => {
if (!dragging) return;
// 获取鼠标移动的距离
const dx = e.clientX - mouseStart.x;
const dy = e.clientY - mouseStart.y;
// 根据鼠标移动的距离设置元素的位置
const left = elementStart.x + dx;
const top = elementStart.y + dy;
curPopupDom.value.style.transform = `translate(${left}px, ${top}px)`;
};

此时我们的 popup 也可以拖拽了

结语

本篇文章就到这里了,基于此我们也可以理解可拖拽 dialog 是如何实现的了,当前这个案例还可以继续完善,比如处理一下边界问题,不让 popup 拖拽出屏幕啊,给后面的内容加个遮罩层啊等等,这些大家可以自己去实现一下,也是比较简单的,更多内容敬请期待,债见~

上一篇:
【回顾学习】CSS中的border
下一篇:
【回顾学习】::before与::after