Vue3 + Canvas实现选中鼠标框选范围元素

在开发中遇到一个需求,类似windows的鼠标绘制一个框选区域,在框选区域中的元素被选中并且能够设置背景颜色,同时按下shift、ctrl键能够追加选中的元素,接下来的实现用到了Canvas封装的一些方法,可以参考之前的文章,链接如下:

Canvas扩展 判断点击位置是否位于绘制图形中 - 掘金 (juejin.cn)

1、初始化

因为框选的区域是针对整个浏览器的,所以画布的宽度要与浏览器可视区域的宽高保持一致。因为在鼠标移动和鼠标左键抬起时,也需要用到画布的相关方法,这时需要吧画布对象暴露给全局。具体代码如下:

html 复制代码
<template>
  <div class="wrapper">
    <canvas id="canvas" class="canvas"></canvas>
  </div>
</template>
typescript 复制代码
const drawer = ref<Drawer>()

const initCanvas = () => {
  const canvas = document.getElementById('canvas') as HTMLCanvasElement
  canvas.width = window.innerWidth - 20
  canvas.height = window.innerHeight - 20
  drawer.value = new Drawer({ view: canvas })
}

onMounted(() => {
  initCanvas()
})

到这里画布的初始化就完成了,然后添加一些元素用于框选,因为被框选的元素在canvas的上方,所以需要调整css布局,调整代码如下:

html 复制代码
<template>
  <div class="wrapper">
    <div class="rect-wrapper">
      <div v-for="i in 8" :key="i" :data-id="i" class="rect-style"></div>
    </div>
    <canvas id="canvas" class="canvas"></canvas>
  </div>
</template>
css 复制代码
.wrapper {
  height: 100vh;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
}
.rect-wrapper {
  height: 150px;
  width: 300px;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
}
.rect-style {
  width: 50px;
  height: 50px;
  background-color: #490eff;
  margin: 10px;
}
.canvas {
  position: absolute;
  top: 0;
  left: 0;
}

到这里,画布和被框选元素的准备工作完成。

2、绘制框选区域

鼠标左键按下时开始绘制,记录当前鼠标点击的坐标作为开始坐标,鼠标左键按下之后开始记录鼠标移动的坐标作为结束坐标,获取开始坐标和结束坐标后能够绘制鼠标框选区域,绘制之前需要先清除之前绘制的区域,当鼠标左键抬起时,清空画布。 由此可知,我们需要监听鼠标左键按下、移动、抬起三个事件,代码如下:

typescript 复制代码
type AreaPoint = {
  startX: number
  startY: number
  endX: number
  endY: number
}
const areaPoint = reactive<AreaPoint>({
  startX: 0,
  startY: 0,
  endX: 0,
  endY: 0
})

// 是否开始获取坐标
const startMove = ref(false)

const mouseDown = (e: MouseEvent) => {
  startMove.value = true
  const { x, y } = e
  areaPoint.startX = x
  areaPoint.startY = y
}
const mouseMove = (e: MouseEvent) => {
  if (startMove.value) {
    const { x, y } = e
    areaPoint.endX = x
    areaPoint.endY = y

    drawer.value?.clear()

    const { startX, startY, endX, endY } = areaPoint
    const rect = new Rect(
      { x: startX, y: startY, width: endX - startX, height: endY - startY, isFill: false },
      'rect'
    )

    drawer.value?.add(rect)
  }
}
const mouseUp = (e: MouseEvent) => {
  const { screenX, screenY } = e
  areaPoint.endX = screenX
  areaPoint.endY = screenY
  startMove.value = false
  drawer.value?.clear()
}

onMounted(() => {
  bindEvent()
  ...
})

onBeforeUnmount(() => {
  window.removeEventListener('mousedown', mouseDown)
  window.removeEventListener('mousemove', mouseMove)
  window.removeEventListener('mouseup', mouseUp)
})

3、选中框选区域中的元素

因为根据框选区域是根据坐标来计算的,所以需要在框选之前获取到所有的坐标及宽、高,代码如下:

typescript 复制代码
type RectInfo = {
  id: number
  x: number
  y: number
  width: number
  height: number
}

const rectInfoList = ref<RectInfo[]>([])

const getAllDomPoint = () => {
  const getAllDom = document.querySelector('.rect-wrapper')!.children
  for (const key of getAllDom) {
    const { x, y, width, height } = key.getBoundingClientRect()
    rectInfoList.value.push({
      id: Number((key as HTMLElement).dataset.id),
      x,
      y,
      width,
      height
    })
  }
}

onMounted(() => {
  getAllDomPoint()
  ...
})

根据鼠标的移动,需要判断被框选元素是否在框选区域中,鼠标框选示例如下:

由图中可以看出,框选区域主要有以上4类,为了方便处理,设置最终的开始坐标永远小于结束坐标,代码如下:

typescript 复制代码
const computedIsSelected = (areaPoint: AreaPoint, rectInfo: RectInfo) => {
  const { startX, startY, endX, endY } = areaPoint
  const { x, y, width, height } = rectInfo
  const finalStartX = startX > endX ? endX : startX
  const finalStartY = startY > endY ? endY : startY
  const finalEndX = startX > endX ? startX : endX
  const finalEndY = startY > endY ? startY : endY
}

示例图中的判断被框选元素是否在框选区域中需要根据框选元素的四个点的坐标,所以需要先计算四个点的坐标,代码如下:

typescript 复制代码
const { startX, startY, endX, endY } = areaPoint
const { x, y, width, height } = rectInfo
...
const rectPointTopLeft = {
  x,
  y
}
const rectPointTopRight = {
  x: x + width,
  y
}
const rectPointBottomLeft = {
  x,
  y: y + height
}
const rectPointBottomRight = {
  x: x + width,
  y: y + height
}

图例与判断条件的对应关系如下:

至此,在框选区域中的元素能够被选中,代码如下:

html 复制代码
<template>
  <div class="wrapper">
    <div class="rect-wrapper">
      <div
        ...
        :style="{
          backgroundColor: highRectList.find((item) => item.id === i) ? 'green' : ''
        }"
      ></div>
    </div>
  </div>
</template>
typescript 复制代码
const mouseMove = (e: MouseEvent) => {
  if (startMove.value) {
    ...
    rectInfoList.value.forEach((item) => {
      const { id } = item
      if (computedIsSelected(areaPoint, item)) {
        !highRectList.value.find((item) => item.id === id) &&
          highRectList.value.push({
            id
          })
      }
    })
    drawer.value?.add(rect)
  }
}

但是在鼠标框选还未抬起鼠标左键之前,有可能会缩小框选区域,缩小之前被选中的元素应该取消选中,这里通过遍历被选中的元素,过滤出在框选区域中的元素,代码如下:

typescript 复制代码
const mouseMove = (e: MouseEvent) => {
  if (startMove.value) {
   ...
    rectInfoList.value.forEach((item) => {
      const { id } = item
      if (computedIsSelected(areaPoint, item)) {
       ...
      } else {
        highRectList.value = highRectList.value.filter((item) => item.id !== id)
      }
    })
    drawer.value?.add(rect)
  }
}

至此,通过鼠标移动选中元素的功能基本完成。在系统中,按下shift然后继续框选,之前的应该也应该保留。

4、按下shift、ctrl继续框选

按下shift、ctrl继续框选是在第一次框选之后,所以在鼠标抬起时需要对选中的元素添加已选中的标记,修改代码如下:

typescript 复制代码
type RectInfo = {
  ...
  isSelected: boolean
}

const mouseUp = (e: MouseEvent) => {
  ...
  highRectList.value.forEach((item) => {
    item.isSelected = true
  })

添加键盘监听事件,判断shift、ctrl是否按下,代码如下:

typescript 复制代码
const keyIsPress = ref(false)

const keyDown = (e: KeyboardEvent) => {
  if (e.key === 'Shift') {
    keyIsPress.value = true
  }

  if (e.key === 'Control') {
    keyIsPress.value = true
  }
}
const keyUp = () => {
  keyIsPress.value = false
}

function bindEvent() {
  ...
  window.addEventListener('keydown', keyDown)
  window.addEventListener('keyup', keyUp)
}

修改鼠标移动时判断元素是否被框选的判断条件,如果元素中isSelected为true,说明已被选中,按下shift、ctrl再次选中时不清除,当按下shift、ctrl框选到已经选中的元素,isSelected设置为false,当缩小框选区域是能够取消选中,否则查找当前被选中的元素,修改代码如下:

typescript 复制代码
const mouseMove = (e: MouseEvent) => {
  if (startMove.value) {
    ...

    rectInfoList.value.forEach((item) => {
      const { id } = item
      if (computedIsSelected(areaPoint, item)) {
       const target = highRectList.value.find((item) => item.id === id)
        if (target) {
          target.isSelected = false
        } else {
          highRectList.value.push({
            id
          })
        }
      } else {
        const tempArr: HighRect[] = []
        highRectList.value.forEach((item) => {
          if (item.isSelected) {
            tempArr.push(item)
          } else {
            if (item.id !== id) {
              tempArr.push(item)
            }
          }
        })
        highRectList.value = [...tempArr]
      }
    })
    drawer.value?.add(rect)
  }
}

至此,鼠标移动框选的功能基本完成。

代码地址:

stackblitz.com/edit/vitejs...

相关推荐
bug总结3 分钟前
新学一个JavaScript 的 classList API
开发语言·javascript·ecmascript
网络安全-老纪18 分钟前
网络安全-js安全知识点与XSS常用payloads
javascript·安全·web安全
API_technology21 分钟前
电商API安全防护:JWT令牌与XSS防御实战
前端·安全·xss
yqcoder26 分钟前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
十八朵郁金香1 小时前
通俗易懂的DOM1级标准介绍
开发语言·前端·javascript
计算机-秋大田1 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计
GDAL2 小时前
HTML 中的 Canvas 样式设置全解
javascript
m0_528723812 小时前
HTML中,title和h1标签的区别是什么?
前端·html
Dark_programmer2 小时前
html - - - - - modal弹窗出现时,页面怎么能限制滚动
前端·html
GDAL2 小时前
HTML Canvas clip 深入全面讲解
前端·javascript·canvas