效果预览
实现原理
- 选择一块区域作为允许鼠标框选的容器,取名:
box
(鼠标在此容器内拖拽才能产生框选效果,容器外无效); - 准备一个div作为鼠标框选过程中产生的半透明遮罩,取名:
area
(上图中的蓝色半透明矩形); - 在
box
中监听mousedown事件,记录鼠标按下时相对于box
的位置:startX、startY; - 在mousemove事件中同样(实时)记录鼠标相对于
box
的最新位置:endX、endY; - 通过startX、startY和endX、endY,可以得到绘制
area
所必须的四个参数:left、top、width、height,进而实时绘制area
; - 在mouseover事件中根据
area
的最新参数去判断其与box
内各子元素的“接触”关系(也称碰撞检测),记录所有被area
接触过的子元素的索引:idxs; - 利用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+
2 条评论
叶梦响 · 2021年12月9日 上午9:39
你好,我想请教一些关于你这个自定义指令的相关功能的问题,我在ts版的vue项目里复现不了,可以有偿帮帮忙吗
大海 · 2021年12月9日 上午10:50
你好,给你发邮件了哈