可拖拽、缩放、旋转组件之 – 多元素组合与拆分功能

🌈介绍

基于 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 获取的是画布元素

  1. 首先,将 show 的值设置为 true,以显示选中区域,获取鼠标按下的位置:通过鼠标事件对象 e 获取鼠标按下时的页面上的横坐标 downX 和纵坐标 downY。

  2. 获取画布的位置,从而计算选中区域的相对于画布的偏移量

  3. 在 onMouseMove 函数中计算区域的大小和位置:通过鼠标移动的距离 disX 和 disY 计算区域的宽度和高度,并根据移动的方向调整 left 和 top 的值,从而实现编辑区域的调整。

  • 宽度和高度直接取各自移动距离的绝对值
  • 如果 disX 为负数则left要减去增加的宽度,dixY同理
  1. 抬起鼠标 onMouseUp 中隐藏选区,重置选区数据

  2. 在 onMouseMove 和 onMouseUp 中都触发了相应的事件 move和up并传递零零选区的数据信息

有了这个组件,该如何使用呢?

先上使用代码,后面有详细解释

步骤解析

  1. 给画布注册mousedown事件 onEditorMouseDown,如果已有选中的元素将其全部设置为非选状态,并且不触发这个区域选择事件,只有画布上没有选中的元素时触发区域的mousedown

  2. 调用刚刚封装 Area 组件的 onMouseDown 方法并传入了事件对象,因此在在 Area 组件里的 onMouseDown 的 e.target 其实获取的是画布元素

  3. 监听 Area 组件的 move 事件 onAreaMove。当选区在 Area 组件中移动时,onAreaMove 会被触发。在该函数中,根据选区的数据去判断是否有元素在选区内。如果有元素在选区内,就将它们设置为选中状态。

  4. 判断元素是否在选区内的逻辑还是挺好理解的。对于每个元素,判断选区的 left 是否小于元素的 left,且选区的 left + width 是否大于元素的 left + width。类似地,对于 top 也进行类似的判断。只有当元素的左上角和右下角同时在选区内,才判定该元素为被选中状态。

移动选中的元素

移动多个区域选中的元素,类似下面的效果

可拖拽、缩放、旋转组件之 - 多元素组合与拆分功能

要计算每个元素的移动距离,就需要es-drager提供的一些事件了

  1. change 事件:change 事件主要用于更新最新的拖拽数据(dragData)

  2. drag-start 事件:

  • 检查是否是区域选择状态(areaSelected.value),如果不是区域选择状态,则将所有选中的元素的 selected 属性设置为 false,即将它们设为非选中状态。
  • 选中当前元素(即 current)并记录其初始 lefttop 位置到 extraDragData 中,以便后续计算多个选中元素的移动距离。
  1. drag 事件:
  • 通过当前拖拽的 dragDataextraDragData 中记录的初始位置,计算出拖拽元素的移动距离 disXdisY
  • 循环遍历所有元素,对于选中的元素(除了当前拖拽元素),更新其 lefttop 位置,以实现多选元素的联动移动。
  • 更新 extraDragData 中的 prevLeftprevTop,以便下一次计算移动距离。

上面多了 areaSelected 记录是否是区域选择状态,那么在什么情况它的值才是true呢?

这时我们就要监听 Area 组件的 up 事件了

只有区域选中了元素,areaSelected才能是true,然后点击其它区域是设置为false

组合与拆分

可拖拽、缩放、旋转组件之 - 多元素组合与拆分功能

完成上面的工作后,我们来看看如何将多个元素组合成一个,为了方便渲染我们先封装一个Group组件

Group 组件

这个组件的功能就是循环显示所有组合的元素

  • 随后我们准备两个按钮,分别注册了makeGroup和cancelGroup点击事件

下面分别解释这两个函数

  1. 组合元素 (makeGroup 函数):

    • 首先,获取所有选中的元素 (selectedElements)。
    • 如果没有选中的元素,则直接返回,不执行组合操作。
    • 对于选中的元素,遍历计算它们的最小 lefttop 值,以及最大 lefttop 值,从而确定组合后元素的位置和尺寸。
    • 然后,遍历选中的元素,根据计算得到的最小 lefttop,更新它们的 lefttop 值,使它们相对于组合后元素的位置发生偏移,从而将它们归置到组合后元素的内部。
    • 创建一个名为 groupElement 的新元素,作为组合后的元素。该元素的属性包括:component 设置为 ‘es-group’,group 设置为 true,以及通过计算得到的 dragData 信息和选中的元素列表 selectedElements
    • 将组合后的元素 groupElement 添加到 data.value.componentList 中,同时保留其他非选中元素。
  2. 取消组合 (cancelGroup 函数):

    • 首先,检查当前选中的元素是否为一个组合元素(current.component === 'es-group')。如果不是组合元素,直接返回,不执行拆分操作。
    • 获取组合元素 currentprops.elements,该属性存储了组合元素内部的所有元素列表。
    • 对于组合元素内部的每个元素,计算其新的 lefttop 值,使它们相对于画布发生偏移,并考虑了组合元素的位置和角度。
    • 创建一个新的元素列表 newElements,该列表包含了拆分后的所有元素。
    • 将组合元素 currentdata.value.componentList 中删除,同时将拆分后的元素列表 newElements 添加到 data.value.componentList 中。

最后

本节只是对多个元素的组合与拆分的简单实现,对于组合后的旋转与缩放我想在后面的文章中介绍。

最后来看看在drawio中元素组合与拆分的效果

可拖拽、缩放、旋转组件之 - 多元素组合与拆分功能

drawio在实现组合后缩放会有一点小问题,大家看下图

当然我们的目标是尽可能实现理想的组合后的缩放与旋转

原文链接:https://juejin.cn/post/7258337246024843319 作者:幽月之格

(0)
上一篇 2023年7月23日 上午10:34
下一篇 2023年7月23日 上午10:44

相关推荐

发表回复

登录后才能评论