【从零开始集成低代码平台】编辑器-组件拖拽

4,368 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第25天,点击查看活动详情

开源地址:github.com/zmkwjx/baik…

摘要

思维导图中,我们提及了低代码有两大组成部分:编辑器和渲染器,要使得我们的编辑器可以操作,那么最重要的就是要能对组件进行拖拽、拉伸、旋转等等。这部分作者将从零开始实现组件拖拽功能,一点点的去学习和思考其中的实现方案,当然 github 在这方面已经有很多优秀的案例了,有兴趣的朋友可以自行检索。

PS:最近很忙所以断更了很久,实现这部分利用了下班时间花费了大概1个多星期去实现,问题还是有的,大概思路应该不会有太大问题。

思考

  • 如何实现拖拽?

之前跟小伙伴聊天的时候,他们的第一反应是,H5不是有一个拖放功能吗?确实,拖放(Drag 和 drop)是 HTML5 标准的组成部分。我们去菜鸟教程上可以发现这部分有很详细的说明:

拖放是一种常见的特性,即抓取对象以后拖到另一个位置。

在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放。

我们可以将元素的 draggable 属性设置为 true,通过事件 ondragstart setData 告知放置元素事件 ondragover 如何进行放置 ondrop

菜鸟教程的案例

  • Drag\Drop标准是否是我们需要的效果?

在得到这一结论之后,作者急哄哄的去打开 vscode 行云流水般的开始敲击键盘。啪的一下很快,屏幕上出现了一行又一行的代码,然后高潮来了,执行完 yarn dev 发现:

无标题.png

无论我怎么拖拽,都只能实现从 A1A2 的效果,并且这个过程中 A1 并不会跟着你的鼠标改变位置,除非你放下鼠标,随后我就进入了贤者时间。

  • 如何实现连贯的可拖拽操作?

这个时候我找到小伙伴,狠狠的玩弄了他一番。他流着泪喘息着跟我说:我错了,我错了。我们分析了一下,发现Drag\Drop 该标准更注重结果,并不关心过程,这意味着我们很难在拖拽的过程中对该元素进行监控。要实现连贯的拖拽操作,必定会对当前元素的dom进行操作,并不断的修正它的坐标值。

但是这次我学乖了,我找到了小伙伴,画了一张示意图,并表明这是我要的效果:

d3317494-ae05-4cc4-9271-94cb10d84e0c.png

实现

技术结构

vite3vue3sass

数据结构

由于我们是想把该拖拽思路做成一个组件的形式、我们将直接放入一个 Array 进行传入

[
  {
    x: 0,
    y: 0,
    z: 1,
    w: 100,
    h: 100,
    draggable: true,
    resizable: true,
    rotatable: true
  }
]
  • 参数说明:
参数说明数据类型默认值
x坐标X,指绝对定位中 left 位置,单位 pxNumber | String0
y坐标Y,指绝对定位中 top 位置,单位 pxNumber | String0
w元素宽度,单位 pxNumber | String0
h元素高度,单位 pxNumber | String0
draggable元素是否能拖拽Booleantrue
resizable元素是否能拉伸Booleantrue
rotatable元素是否能旋转Booleantrue

视图方案

我们将平面设计为三个维度:view(布局)、item(拖拽视图)、slot(拖拽内容)

  • view:负责控制组件的初始坐标、父级元素、空白点击效果等
  • item:负责组件的拖拽、拉伸、旋转、裁剪等功能、可以通过该元素设置一些基础属性等
  • slot:插槽,主要是展示组件内容,通过 #default="{ item }"传递数据

无标题1.png

具体编码

通过 item 标定触发和选中组件、监听 window 中的 onmousemoveonmouseup控制元素的移动的取消,通过 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(被鼠标按下时的组件序号)。鼠标按下时采集初始点的 pageXpageY,并判断当该组件以及被判断选中时,不记录 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 距离 movementXY 距离 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 的地址

开源地址:github.com/zmkwjx/baik…

结论

组件拖拽的核心思路就是通过监听鼠标操作,鼠标按下时记录文档地址,记录连续移动时计算的差值并不断更新初始位置,和通过差值计算组件的坐标就可以达到拖拽的连续效果。当然这个连续移动需要注意防抖,过快可能导致卡顿或者移出。