?介绍
基于 vue3.x + CompositionAPI + typescript + vite 的可拖拽、缩放、旋转的组件
- 拖拽&区域拖拽
- 支持缩放
- 旋转
- 网格拖拽缩放
在线示例
源码地址
这节主要来分享如何使用es-drager,根据现有功能实现多个元素组合与拆分功能
es-drager的更新
es-drager 的1.x版本支持移动端啦
另外最近还在使用es-drager开发一个低代码编辑器(还未成型),也算是一个es-drager的综合使用案例吧,老铁们可以先到 编辑器案例 中查看
本章内容
- 使用svg绘制网格
- 元素组合与拆分
使用svg绘制网格
在开始讲组合之前,先来介绍一下如何使用svg画一个指定大小的网格。前面的demo都是使用css的方式,感觉还是不太灵活,有一定的局限性
这里直接抽离成了一个 vue 组件
<template>
<div class="grid-rect" :style="rectStyle">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern v-if="showSmall" id="smallGrid" :width="grid" :height="grid" patternUnits="userSpaceOnUse">
<path :d="`M ${grid} 0 L 0 0 0 ${grid}`" fill="none" :stroke="color.grid" stroke-width="0.5"/>
</pattern>
<pattern id="grid" :width="bigGrid" :height="bigGrid" patternUnits="userSpaceOnUse">
<rect v-if="showSmall" :width="bigGrid" :height="bigGrid" fill="url(#smallGrid)"/>
<path :d="`M ${bigGrid} 0 L 0 0 0 ${bigGrid}`" fill="none" :stroke="color.bigGrid" stroke-width="1"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
</template>
<script setup lang='ts'>
import { computed } from 'vue'
import { useAppStore } from '@/store'
const store = useAppStore()
const props = defineProps({
grid: { // 小网格的大小
type: Number,
default: 10
},
gridCount: { // 小网格的数量,默认为5个
type: Number,
default: 5
},
showSmall: { // 是否显示小网格
type: Boolean,
default: true
}
})
// 计算大网格的大小
const bigGrid = computed(() => props.grid * props.gridCount)
// 处理网站皮肤,可忽略
const color = computed(() => {
const colors = [['#e4e7ed', '#ebeef5'], ['#414243', '#363637']]
const [bigGrid, grid] = colors[store.isLight ? 0 : 1]
return { bigGrid, grid }
})
const rectStyle = computed(() => ({ '--border-color': color.value.bigGrid }))
</script>
<style lang='scss' scoped>
.grid-rect {
width: 100%;
height: 100%;
border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
</style>
可以看到,如果不加属性的话,整个网格组件还是挺简单的
-
在
<defs>
标签中定义了两个图案(pattern)元素。<pattern>
用于创建可重复使用的图案。这里定义了两个图案,一个是名为”smallGrid”的小网格图案,另一个是名为”grid”的大网格图案。 -
小网格的 id 为 smallGrid,它的大小默认是 grid=10
-
大网格的 id 为 grid,默认大小 grid*gridCount=50,由一个矩形(
<rect>
)和一个路径(<path>
)组成。矩形用于填充整个图案区域,其填充样式(fill)使用了名为”smallGrid”的小网格图案。路径用于创建四条边框线,从起点(50, 0)到(0, 0),再到(0, 50)。 -
最后,通过
<rect>
元素创建一个矩形,它的宽度和高度都设置为100%,填充样式(fill)使用了名为”grid”的大网格图案。
使用时直接包裹在画布元素里即可,当然我们也可以传入指定网格的大小
<template>
<div class="es-editor">
<GridRect />
</div>
</template>
<script setup lang='ts'>
import GridRect from '@/components/editor/GridRect.vue'
</script>
<style lang='scss' scoped>
.es-editor {
position: relative;
width: 800px;
height: 600px;
}
</style>
元素组合与拆分
选中区域
组合前,我们需要选中需要组合的元素,类似下图这样的效果
单独抽离区域选中组件 Area
<template>
<div v-show="show" class="es-editor-area" :style="areaStyle"></div>
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue'
const emit = defineEmits(['move', 'up'])
const show = ref(false)
const areaData = ref({
width: 0,
height: 0,
top: 0,
left: 0
})
const areaStyle = computed(()=> {
const { width, height, top, left } = areaData.value
return {
width: width + 'px',
height: height + 'px',
top: top + 'px',
left: left + 'px'
}
})
function onMouseDown(e: MouseEvent) {
show.value = true
// 鼠标按下的位置
const { pageX: downX, pageY: downY } = e;
const elRect = (e.target as HTMLElement)!.getBoundingClientRect()
// 鼠标在编辑器中的偏移量
const offsetX = downX - elRect.left
const offsetY = downY - elRect.top
const onMouseMove = (e: MouseEvent) => {
// 移动的距离
const disX = e.pageX - downX
const disY = e.pageY - downY
// 得到默认的left、top
let left = offsetX, top = offsetY
// 宽高取鼠标移动距离的绝对值
let width = Math.abs(disX), height = Math.abs(disY)
// 如果往左,将left减去增加的宽度
if (disX < 0) {
left = offsetX - width
}
// 如果往上,将top减去增加的高度
if (disY < 0) {
top = offsetY - height
}
areaData.value = {
width,
height,
left,
top
}
emit('move', { ...areaData.value })
}
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
show.value = false
areaData.value = {
width: 0,
height: 0,
top: 0,
left: 0
}
emit('up', areaData.value)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
defineExpose({
onMouseDown,
areaData
})
</script>
注意:由于这个onMouseDown是画布触发时调用的, 因此 e.target
获取的是画布元素
-
首先,将 show 的值设置为 true,以显示选中区域,获取鼠标按下的位置:通过鼠标事件对象 e 获取鼠标按下时的页面上的横坐标 downX 和纵坐标 downY。
-
获取画布的位置,从而计算选中区域的相对于画布的偏移量
-
在 onMouseMove 函数中计算区域的大小和位置:通过鼠标移动的距离 disX 和 disY 计算区域的宽度和高度,并根据移动的方向调整 left 和 top 的值,从而实现编辑区域的调整。
- 宽度和高度直接取各自移动距离的绝对值
- 如果 disX 为负数则left要减去增加的宽度,dixY同理
-
抬起鼠标 onMouseUp 中隐藏选区,重置选区数据
-
在 onMouseMove 和 onMouseUp 中都触发了相应的事件 move和up并传递零零选区的数据信息
有了这个组件,该如何使用呢?
先上使用代码,后面有详细解释
步骤解析
-
给画布注册mousedown事件
onEditorMouseDown
,如果已有选中的元素将其全部设置为非选状态,并且不触发这个区域选择事件,只有画布上没有选中的元素时触发区域的mousedown -
调用刚刚封装 Area 组件的 onMouseDown 方法并传入了事件对象,因此在在 Area 组件里的 onMouseDown 的
e.target
其实获取的是画布元素 -
监听 Area 组件的
move
事件onAreaMove
。当选区在 Area 组件中移动时,onAreaMove
会被触发。在该函数中,根据选区的数据去判断是否有元素在选区内。如果有元素在选区内,就将它们设置为选中状态。 -
判断元素是否在选区内的逻辑还是挺好理解的。对于每个元素,判断选区的
left
是否小于元素的left
,且选区的left + width
是否大于元素的left + width
。类似地,对于top
也进行类似的判断。只有当元素的左上角和右下角同时在选区内,才判定该元素为被选中状态。
移动选中的元素
移动多个区域选中的元素,类似下面的效果
要计算每个元素的移动距离,就需要es-drager提供的一些事件了
-
change
事件:change
事件主要用于更新最新的拖拽数据(dragData) -
drag-start
事件:
- 检查是否是区域选择状态(
areaSelected.value
),如果不是区域选择状态,则将所有选中的元素的selected
属性设置为false
,即将它们设为非选中状态。 - 选中当前元素(即
current
)并记录其初始left
和top
位置到extraDragData
中,以便后续计算多个选中元素的移动距离。
drag
事件:
- 通过当前拖拽的
dragData
和extraDragData
中记录的初始位置,计算出拖拽元素的移动距离disX
和disY
。 - 循环遍历所有元素,对于选中的元素(除了当前拖拽元素),更新其
left
和top
位置,以实现多选元素的联动移动。 - 更新
extraDragData
中的prevLeft
和prevTop
,以便下一次计算移动距离。
上面多了 areaSelected 记录是否是区域选择状态,那么在什么情况它的值才是true呢?
这时我们就要监听 Area 组件的 up 事件了
只有区域选中了元素,areaSelected才能是true,然后点击其它区域是设置为false
组合与拆分
完成上面的工作后,我们来看看如何将多个元素组合成一个,为了方便渲染我们先封装一个Group组件
Group 组件
这个组件的功能就是循环显示所有组合的元素
- 随后我们准备两个按钮,分别注册了makeGroup和cancelGroup点击事件
下面分别解释这两个函数
-
组合元素 (
makeGroup
函数):- 首先,获取所有选中的元素 (
selectedElements
)。 - 如果没有选中的元素,则直接返回,不执行组合操作。
- 对于选中的元素,遍历计算它们的最小
left
和top
值,以及最大left
和top
值,从而确定组合后元素的位置和尺寸。 - 然后,遍历选中的元素,根据计算得到的最小
left
和top
,更新它们的left
和top
值,使它们相对于组合后元素的位置发生偏移,从而将它们归置到组合后元素的内部。 - 创建一个名为
groupElement
的新元素,作为组合后的元素。该元素的属性包括:component
设置为 ‘es-group’,group
设置为true
,以及通过计算得到的dragData
信息和选中的元素列表selectedElements
。 - 将组合后的元素
groupElement
添加到data.value.componentList
中,同时保留其他非选中元素。
- 首先,获取所有选中的元素 (
-
取消组合 (
cancelGroup
函数):- 首先,检查当前选中的元素是否为一个组合元素(
current.component === 'es-group'
)。如果不是组合元素,直接返回,不执行拆分操作。 - 获取组合元素
current
的props.elements
,该属性存储了组合元素内部的所有元素列表。 - 对于组合元素内部的每个元素,计算其新的
left
和top
值,使它们相对于画布发生偏移,并考虑了组合元素的位置和角度。 - 创建一个新的元素列表
newElements
,该列表包含了拆分后的所有元素。 - 将组合元素
current
从data.value.componentList
中删除,同时将拆分后的元素列表newElements
添加到data.value.componentList
中。
- 首先,检查当前选中的元素是否为一个组合元素(
最后
本节只是对多个元素的组合与拆分的简单实现,对于组合后的旋转与缩放我想在后面的文章中介绍。
最后来看看在drawio中元素组合与拆分的效果
drawio在实现组合后缩放会有一点小问题,大家看下图
当然我们的目标是尽可能实现理想的组合后的缩放与旋转
原文链接:https://juejin.cn/post/7258337246024843319 作者:幽月之格
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。