本文是可视化拖拽系列的第三篇,之前的两篇文章一共对 17 个功能点的技术原理进行了分析:
- 编辑器
- 自定义组件
- 拖拽
- 删除组件、调整图层层级
- 放大缩小
- 撤消、重做
- 组件属性设置
- 吸附
- 预览、保存代码
- 绑定事件
- 绑定动画
- 导入 PSD
- 手机模式
- 拖拽旋转
- 复制粘贴剪切
- 数据交互
- 发布
本文在此基础上,将对以下几个功能点的技术原理进行分析:
- 多个组件的组合和拆分
- 文本组件
- 矩形组件
- 锁定组件
- 快捷键
- 网格线
- 编辑器快照的另一种实现方式
如果你对我之前的两篇文章不是很了解,建议先把这两篇文章看一遍,再来阅读此文:
虽然我这个可视化拖拽组件库只是一个 DEMO,但对比了一下市面上的一些现成产品(例如 processon、墨刀),就基础功能来说,我这个 DEMO 实现了绝大部分的功能。
如果你对于低代码平台有兴趣,但又不了解的话。强烈建议将我的三篇文章结合项目源码一起阅读,相信对你的收获绝对不小。另附上项目、在线 DEMO 地址:
18. 多个组件的组合和拆分
组合和拆分的技术点相对来说比较多,共有以下 4 个:
- 选中区域
- 组合后的移动、旋转
- 组合后的放大缩小
- 拆分后子组件样式的恢复
选中区域
在将多个组件组合之前,需要先选中它们。利用鼠标事件可以很方便的将选中区域展示出来:
mousedown
记录起点坐标mousemove
将当前坐标和起点坐标进行计算得出移动区域- 如果按下鼠标后往左上方移动,类似于这种操作则需要将当前坐标设为起点坐标,再计算出移动区域
// 获取编辑器的位移信息 const rectInfo = this.editor.getBoundingClientRect() this.editorX = rectInfo.x this.editorY = rectInfo.y const startX = e.clientX const startY = e.clientY this.start.x = startX - this.editorX this.start.y = startY - this.editorY // 展示选中区域 this.isShowArea = true const move = (moveEvent) => { this.width = Math.abs(moveEvent.clientX - startX) this.height = Math.abs(moveEvent.clientY - startY) if (moveEvent.clientX < startX) { this.start.x = moveEvent.clientX - this.editorX } if (moveEvent.clientY < startY) { this.start.y = moveEvent.clientY - this.editorY } }
在 mouseup
事件触发时,需要对选中区域内的所有组件的位移大小信息进行计算,得出一个能包含区域内所有组件的最小区域。这个效果如下图所示:
这个计算过程的代码:
createGroup() { // 获取选中区域的组件数据 const areaData = this.getSelectArea() if (areaData.length <= 1) { this.hideArea() return } // 根据选中区域和区域中每个组件的位移信息来创建 Group 组件 // 要遍历选择区域的每个组件,获取它们的 left top right bottom 信息来进行比较 let top = Infinity, left = Infinity let right = -Infinity, bottom = -Infinity areaData.forEach(component => { let style = {} if (component.component == 'Group') { component.propValue.forEach(item => { const rectInfo = $(`#component${item.id}`).getBoundingClientRect() style.left = rectInfo.left - this.editorX style.top = rectInfo.top - this.editorY style.right = rectInfo.right - this.editorX style.bottom = rectInfo.bottom - this.editorY if (style.left < left) left = style.left if (style.top < top) top = style.top if (style.right > right) right = style.right if (style.bottom > bottom) bottom = style.bottom }) } else { style = getComponentRotatedStyle(component.style) } if (style.left < left) left = style.left if (style.top < top) top = style.top if (style.right > right) right = style.right if (style.bottom > bottom) bottom = style.bottom }) this.start.x = left this.start.y = top this.width = right - left this.height = bottom - top // 设置选中区域位移大小信息和区域内的组件数据 this.$store.commit('setAreaData', { style: { left, top, width: this.width, height: this.height, }, components: areaData, }) }, getSelectArea() { const result = [] // 区域起点坐标 const { x, y } = this.start // 计算所有的组件数据,判断是否在选中区域内 this.componentData.forEach(component => { if (component.isLock) return const { left, top, width, height } = component.style if (x <= left && y <= top && (left + width <= x + this.width) && (top + height <= y + this.height)) { result.push(component) } }) // 返回在选中区域内的所有组件 return result }
简单描述一下这段代码的处理逻辑:
- 利用 getBoundingClientRect() 浏览器 API 获取每个组件相对于浏览器视口四个方向上的信息,也就是
left
top
right
bottom
。 - 对比每个组件的这四个信息,取得选中区域的最左、最上、最右、最下四个方向的数值,从而得出一个能包含区域内所有组件的最小区域。
- 如果选中区域内已经有一个
Group
组合组件,则需要对它里面的子组件进行计算,而不是对组合组件进行计算。
组合后的移动、旋转
为了方便将多个组件一起进行移动、旋转、放大缩小等操作,我新创建了一个 Group
组合组件:
<template> <div class="group"> <div> <template v-for="item in propValue"> <component class="component" :is="item.component" :style="item.groupStyle" :propValue="item.propValue" :key="item.id" :id="'component' + item.id" :element="item" /> </template> </div> </div> </template> <script> import { getStyle } from '@/utils/style' export default { props: { propValue: { type: Array, default: () => [], }, element: { type: Object, }, }, created() { const parentStyle = this.element.style this.propValue.forEach(component => { // component.groupStyle 的 top left 是相对于 group 组件的位置 // 如果已存在 component.groupStyle,说明已经计算过一次了。不需要再次计算 if (!Object.keys(component.groupStyle).length) { const style = { ...component.style } component.groupStyle = getStyle(style) component.groupStyle.left = this.toPercent((style.left - parentStyle.left) / parentStyle.width) component.groupStyle.top = this.toPercent((style.top - parentStyle.top) / parentStyle.height) component.groupStyle.width = this.toPercent(style.width / parentStyle.width) component.groupStyle.height = this.toPercent(style.height / parentStyle.height) } }) }, methods: { toPercent(val) { return val * 100 + '%' }, }, } </script> <style lang="scss" scoped> .group { & > div { position: relative; width: 100%; height: 100%; .component { position: absolute; } } } </style>
Group
组件的作用就是将区域内的组件放到它下面,成为子组件。并且在创建 Group
组件时,获取每个子组件在 Group
组件内的相对位移和相对大小:
created() { const parentStyle = this.element.style this.propValue.forEach(component => { // component.groupStyle 的 top left 是相对于 group 组件的位置 // 如果已存在 component.groupStyle,说明已经计算过一次了。不需要再次计算 if (!Object.keys(component.groupStyle).length) { const style = { ...component.style } component.groupStyle = getStyle(style) component.groupStyle.left = this.toPercent((style.left - parentStyle.left) / parentStyle.width) component.groupStyle.top = this.toPercent((style.top - parentStyle.top) / parentStyle.height) component.groupStyle.width = this.toPercent(style.width / parentStyle.width) component.groupStyle.height = this.toPercent(style.height / parentStyle.height) } }) }, methods: { toPercent(val) { return val * 100 + '%' }, },
也就是将子组件的 left
top
width
height
等属性转成以 %
结尾的相对数值。
为什么不使用绝对数值?
如果使用绝对数值,那么在移动 Group
组件时,除了对 Group
组件的属性进行计算外,还需要对它的每个子组件进行计算。并且 Group
包含子组件太多的话,在进行移动、放大缩小时,计算量会非常大,有可能会造成页面卡顿。如果改成相对数值,则只需要在 Group
创建时计算一次。然后在 Group
组件进行移动、旋转时也不用管 Group
的子组件,只对它自己计算即可。
组合后的放大缩小
组合后的放大缩小是个大问题,主要是因为有旋转角度的存在。首先来看一下各个子组件没旋转时的放大缩小:
从动图可以看出,效果非常完美。各个子组件的大小是跟随 Group
组件的大小而改变的。
现在试着给子组件加上旋转角度,再看一下效果:
为什么会出现这个问题?
主要是因为一个组件无论旋不旋转,它的 top
left
属性都是不变的。这样就会有一个问题,虽然实际上组件的 top
left
width
height
属性没有变化。但在外观上却发生了变化。下面是两个同样的组件:一个没旋转,一个旋转了 45 度。
可以看出来旋转后按钮的 top
left
width
height
属性和我们从外观上看到的是不一样的。
接下来再看一个具体的示例:
上面是一个 Group
组件,它左边的子组件属性为:
transform: rotate(-75.1967deg); width: 51.2267%; height: 32.2679%; top: 33.8661%; left: -10.6496%;
可以看到 width
的值为 51.2267%
,但从外观上来看,这个子组件最多占 Group
组件宽度的三分之一。所以这就是放大缩小不正常的问题所在。