携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情
摘要
在思维导图中,我们提及了低代码有两大组成部分:编辑器和渲染器,要使得我们的编辑器可以操作,那么最重要的就是要能对组件进行拖拽、拉伸、旋转等等。这部分作者将从零开始实现组件拖拽功能,一点点的去学习和思考其中的实现方案,当然 github
在这方面已经有很多优秀的案例了,有兴趣的朋友可以自行检索。
PS:最近很忙所以断更了很久,实现这部分利用了下班时间花费了大概1个多星期去实现,问题还是有的,大概思路应该不会有太大问题。
思考
- 如何实现拖拽?
之前跟小伙伴聊天的时候,他们的第一反应是,H5不是有一个拖放功能吗?确实,拖放(Drag 和 drop)是 HTML5 标准的组成部分。我们去菜鸟教程上可以发现这部分有很详细的说明:
拖放是一种常见的特性,即抓取对象以后拖到另一个位置。
在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放。
我们可以将元素的 draggable
属性设置为 true
,通过事件 ondragstart
和 setData
告知放置元素事件 ondragover
如何进行放置 ondrop
- Drag\Drop标准是否是我们需要的效果?
在得到这一结论之后,作者急哄哄的去打开 vscode
行云流水般的开始敲击键盘。啪的一下很快,屏幕上出现了一行又一行的代码,然后高潮来了,执行完 yarn dev
发现:
无论我怎么拖拽,都只能实现从 A1
到 A2
的效果,并且这个过程中 A1
并不会跟着你的鼠标改变位置,除非你放下鼠标,随后我就进入了贤者时间。
- 如何实现连贯的可拖拽操作?
这个时候我找到小伙伴,狠狠的玩弄了他一番。他流着泪喘息着跟我说:我错了,我错了。我们分析了一下,发现Drag\Drop
该标准更注重结果,并不关心过程,这意味着我们很难在拖拽的过程中对该元素进行监控。要实现连贯的拖拽操作,必定会对当前元素的dom进行操作,并不断的修正它的坐标值。
但是这次我学乖了,我找到了小伙伴,画了一张示意图,并表明这是我要的效果:
实现
技术结构
vite3
、vue3
、sass
数据结构
由于我们是想把该拖拽思路做成一个组件的形式、我们将直接放入一个 Array
进行传入
[
{
x: 0,
y: 0,
z: 1,
w: 100,
h: 100,
draggable: true,
resizable: true,
rotatable: true
}
]
- 参数说明:
参数 | 说明 | 数据类型 | 默认值 |
---|---|---|---|
x | 坐标X,指绝对定位中 left 位置,单位 px | Number | String | 0 |
y | 坐标Y,指绝对定位中 top 位置,单位 px | Number | String | 0 |
w | 元素宽度,单位 px | Number | String | 0 |
h | 元素高度,单位 px | Number | String | 0 |
draggable | 元素是否能拖拽 | Boolean | true |
resizable | 元素是否能拉伸 | Boolean | true |
rotatable | 元素是否能旋转 | Boolean | true |
视图方案
我们将平面设计为三个维度:view(布局)、item(拖拽视图)、slot(拖拽内容)
- view:负责控制组件的初始坐标、父级元素、空白点击效果等
- item:负责组件的拖拽、拉伸、旋转、裁剪等功能、可以通过该元素设置一些基础属性等
- slot:插槽,主要是展示组件内容,通过
#default="{ item }"
传递数据
具体编码
通过 item
标定触发和选中组件、监听 window
中的 onmousemove
和 onmouseup
控制元素的移动的取消,通过 view
取消选中的组件。
<template>
<div class="bk-drag-view" @mousedown="handleMouseDown('clear')">
<div
class="bk-drag-item"
v-for="(item, index) in modelValue"
:key="index"
:style="itemStyle(item)"
@mousedown.stop="handleMouseDown('move', $event, item, index)"
>
<div class="bk-drag-item-inner">
<slot :item="item" />
</div>
</div>
</div>
</template>
由于我们后期还得实现拉伸和旋转,所以我们以 hook
的形式去实现不同的操作,主要应对的是鼠标按下(MoveStart)、鼠标移动(Move)、鼠标放开(MoveEnd) 。并在 vue
中声明一个当前操作的变量 triggerState
。
// 鼠标过滤函数: 按下
const triggerState = ref(null)
const handleMouseDown = (action, ...) => {
...
triggerState.value = action
}
// 鼠标过滤函数: 移动
window.onmousemove = event => {}
// 鼠标过滤函数: 放开
window.onmouseup = event => {}
// 清空事件
const handleClear = () => {}
// 使用移动钩子
const { ... } = useMove(modelValue)
在鼠标按下时,需要记录三个值:moveLocal(初始点)、moveState(当前选中的组件)、moveIndex(被鼠标按下时的组件序号)。鼠标按下时采集初始点的 pageX
和 pageY
,并判断当该组件以及被判断选中时,不记录 moveIndex
;当该组件没有被选中时,先清空 moveState
,并且将元数据中的所有组件的选中状态 selected
记录为 false
。
pageX/pageY属性是鼠标指针的位置,相对于文档的左边缘和上边缘。
// 初始点坐标
moveLocal.value[0].x = event.pageX
moveLocal.value[0].y = event.pageY
// 筛出选中
moveState.value = modelValue.value.filter(item => item.selected)
// 如果点击其他元素
if (moveState.value.find(item => item === value)) {
moveIndex.value = -1
} else {
moveState.value = []
moveIndex.value = index
handleSelect()
}
在鼠标放开时如果鼠标位置没有移动,安装 moveIndex
序号选中对应该序号的组件。
const flag1 = moveLocal.value[0].x === event.pageX
const flag2 = moveLocal.value[0].y === event.pageY
const value = modelValue.value[index]
在移动过程中,我们将移动过程中每次产生数据与初始点数据进行对比,在移动过程中,不断得出移动的 X
距离 movementX
和 Y
距离 movementY
moveLocal.value[1].x = event.pageX
moveLocal.value[1].y = event.pageY
const movementX = moveLocal.value[1].x - moveLocal.value[0].x
const movementY = moveLocal.value[1].y - moveLocal.value[0].y
...
x += movementX
y += movementY
然后再赋值给初始点,进行下一次运算,并且在这个计算中要注意做好防抖处理
moveLocal.value[0].x = moveLocal.value[1].x
moveLocal.value[0].y = moveLocal.value[1].y
移动结束后清空选中状态和停止防抖
...
moveState.value = []
moveTimer.value && clearTimeout(moveTimer.value)
这部分只展示核心代码,具体代码在头部有贴出
github
的地址
结论
组件拖拽的核心思路就是通过监听鼠标操作,鼠标按下时记录文档地址,记录连续移动时计算的差值并不断更新初始位置,和通过差值计算组件的坐标就可以达到拖拽的连续效果。当然这个连续移动需要注意防抖,过快可能导致卡顿或者移出。