效果预览

实现原理

  1. 选择一块区域作为允许鼠标框选的容器,取名:box(鼠标在此容器内拖拽才能产生框选效果,容器外无效);
  2. 准备一个div作为鼠标框选过程中产生的半透明遮罩,取名:area(上图中的蓝色半透明矩形);
  3. box中监听mousedown事件,记录鼠标按下时相对于box的位置:startX、startY;
  4. 在mousemove事件中同样(实时)记录鼠标相对于box的最新位置:endX、endY;
  5. 通过startX、startY和endX、endY,可以得到绘制area所必须的四个参数:left、top、width、height,进而实时绘制area
  6. 在mouseover事件中根据area的最新参数去判断其与box内各子元素的“接触”关系(也称碰撞检测),记录所有被area接触过的子元素的索引:idxs;
  7. 利用idxs便可实现后续的相应操作(复选框批量勾选、批量取消,或其他组件的其他变化)。

为了使“鼠标框选”的功能通用,我把1至6步封装成一个自定义指令:v-batch-select。这样,想在哪个元素上实现框选功能就在其标签上添加该指令即可。如:

<div class="box" v-batch-select>
  <ul>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>

v-batch-select

// main.js

Vue.directive('batch-select', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: (el, binding) => {
    // 设置被绑定元素el(即上述的box)的position为relative,目的是让蓝色半透明遮罩area相对其定位
    el.style.position = 'relative';
    // 记录el在视窗中的位置elPos
    const { x, y } = el.getBoundingClientRect()
    const elPos = { x, y }
    // 获取该指令调用者传递过来的参数:className
    // v-batch-select="{ className: '.el-checkbox', selectIdxs }"
    // 表示要使用鼠标框选类名为'.el-checkbox'的元素
    const optionClassName = binding.value.className
    const options = [].slice.call(el.querySelectorAll(optionClassName))
    // 获取被框选对象们的x、y、width、height
    const optionsXYWH = []
    options.forEach(v => {
      const obj = v.getBoundingClientRect()
      optionsXYWH.push({ 
        x: obj.x - elPos.x,
        y: obj.y - elPos.y,
        w: obj.width,
        h: obj.height
      })
    })
    // 创建一个div作为area区域,注意定位是absolute,visibility初始值是hidden
    const area = document.createElement('div')
    area.style = 'position: absolute; border: 1px solid #409eff; background-color: rgba(64, 158, 255, 0.1); z-index: 10; visibility: hidden;'
    area.className = 'area'
    area.innerHTML = ''
    el.appendChild(area)
    el.onmousedown = (e) => {
      // 获取鼠标按下时相对box的坐标
      const startX = e.clientX - elPos.x
      const startY = e.clientY - elPos.y
      // 判断鼠标按下后是否发生移动的标识
      let hasMove = false
      document.onmousemove = (e) => {
        hasMove = true
        // 显示area
        area.style.visibility = 'visible'
        // 获取鼠标移动过程中指针的实时位置
        const endX = e.clientX - elPos.x
        const endY = e.clientY - elPos.y
        // 这里使用绝对值是为了兼容鼠标从各个方向框选的情况
        const width = Math.abs(endX - startX)
        const height = Math.abs(endY - startY)
        // 根据初始位置和实时位置计算出area的left、top、width、height
        const left = Math.min(startX, endX)
        const top = Math.min(startY, endY)
        // 实时绘制
        area.style.left = `${left}px`
        area.style.top = `${top}px`
        area.style.width = `${width}px`
        area.style.height = `${height}px`
      }
      document.onmouseup = (e) => {
        document.onmousemove = document.onmouseup = null
        if (hasMove) {
          // 鼠标抬起时,如果之前发生过移动,则执行碰撞检测
          const { left, top, width, height } = area.style
          const areaTop = parseInt(top)
          const areaRight = parseInt(left) + parseInt(width)
          const areaBottom = parseInt(top) + parseInt(height)
          const areaLeft = parseInt(left)
          binding.value.selectIdxs.length = 0
          optionsXYWH.forEach((v, i) => {
            const optionTop = v.y
            const optionRight = v.x + v.w
            const optionBottom = v.y + v.h
            const optionLeft = v.x
            if (!(areaTop > optionBottom || areaRight < optionLeft || areaBottom < optionTop || areaLeft > optionRight)) {
              // 该指令的调用者可以监听到selectIdxs的变化
              binding.value.selectIdxs.push(i)
            }
          })
        }
        // 恢复以下数据
        hasMove = false
        area.style.visibility = 'hidden'
        area.style.left = 0
        area.style.top = 0
        area.style.width = 0
        area.style.height = 0
        return false
      }
    }
  }
})

demo.vue

<template>
  <div class="batch-select-page">
    <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange" style="margin: 10px 0;">全选</el-checkbox>
    <div class="wrapper" v-batch-select="{ className: '.el-checkbox', selectIdxs }">
      <el-checkbox-group v-model="checkedList" @change="handlecheckedListChange">
        <el-checkbox v-for="option in options" :label="option" :key="option">{{ option }}</el-checkbox>
      </el-checkbox-group>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      checkAll: false,
      options: ['00000', '00001', '00002', '00003', '00004', '00005', '00006', '00007', '00008', '00009', '00010', '00011'],
      checkedList: [],
      isIndeterminate: false,
      selectIdxs: []
    }
  },
  watch: {
    // 监听自定义指令v-batch-select返回的selectIdxs
    selectIdxs(idxs) {
      const res = this.checkedList
      idxs.forEach((v) => {
        const selectItem = this.options[v]
        const i = this.checkedList.indexOf(selectItem)
        // 没勾选,则勾选;已勾选,则取消
        if (i === -1) {
          res.push(selectItem)
        } else {
          res.splice(i, 1)
        }
      })
      this.handlecheckedListChange(res)
    }
  },
  methods: {
    handleCheckAllChange(val) {
      this.checkedList = val ? this.options : []
      this.isIndeterminate = false
    },
    handlecheckedListChange(value) {
      let checkedCount = value.length
      this.checkAll = checkedCount === this.options.length
      this.isIndeterminate = checkedCount > 0 && checkedCount < this.options.length
    }
  }
}
</script>

<style lang="scss">
.batch-select-page {
  .wrapper {
    width: 414px;
    height: 200px;
    padding: 20px;
    border: 1px solid #ccc;
    .el-checkbox {
      margin-top: 10px;
    }
  }
}
</style>
1+
分类: jsvue
浏览:3,315

2 条评论

叶梦响 · 2021年12月9日 上午9:39

你好,我想请教一些关于你这个自定义指令的相关功能的问题,我在ts版的vue项目里复现不了,可以有偿帮帮忙吗

0

    大海 · 2021年12月9日 上午10:50

    你好,给你发邮件了哈

    0

发表评论

电子邮件地址不会被公开。

你必须允许浏览器启用JavaScript才能看到验证码

Scroll Up