最近看到了一个比较有意思的功能,就是鼠标画一个区域,在这个区域内的元素都被选中。类似于windows桌面的鼠标滑动选中了图标一样。就觉得这个功能用在低代码平台上很有用,比如可以批量删除,批量移动位置等。
先看个效果图:
这个功能可以分两步走:
- 画出鼠标滑动的区域
- 计算是否被框选
绘制鼠标滑动区域
鼠标滑动区域绘制需要监听mousedown、mousemove、mouseup
这三个方法。
首先准备一个容器和绘制区域的容器。给container
加上三个监听事件,给area
加上实时绘制的样式。
html
<template>
<div class="container"@mousedown="mousedown" @mousemove="mousemove" @mouseup="mouseup">
<div class="area" :style="areaStyle"></div>
</div>
</template>
当完成这一套划区的操作首先是在mousedown
监听事件中记录鼠标点击的位置,在mousemove
监听函数中实时计算滑动区域的宽高,在mouseup
中取消划区的操作即可。
首先创建一个对象保存滑动区域的信息
javascript
// 鼠标滑动区域信息
const areaInfo = ref({
startX: 0, // 开始滑动的x坐标
startY: 0, // 开始滑动的y坐标
width: 0, // 宽度
height: 0 // 高度
})
接着获取开始位置和计算宽高
javascript
const isStart = ref(false) // 标识开始划区
const mousedown = (e) => {
isStart.value = true
// 获取开始点的位置信息
areaInfo.value.startX = e.clientX
areaInfo.value.startY = e.clientY
}
const mousemove = (e) => {
if (!isStart.value) { return }
const { startX, startY } = areaInfo.value
const { clientX, clientY } = e
// 计算出滑动区域的宽高
areaInfo.value.width = clientX - startX
areaInfo.value.height = clientY - startY
}
const mouseup = () => {
areaInfo.value = {} // 停止划区
isStart.value = false
}
这样就拿到了划区的关键信息,接着添加样式看下效果
js
// 框选区域的样式
const areaStyle = computed(() => {
return areaInfo.value.width && { // 这里需要判断当前信息是否存在,不存在就不显示划区
width: areaInfo.value.width + 'px',
height: areaInfo.value.height + 'px',
left: areaInfo.value.startX + 'px',
top: areaInfo.value.startY + 'px',
}
})
加上css
css
<style scoped lang="less">
.container{
user-select: none;
position: relative;
height: 100vh;
.area{
position: fixed;
background-color: rgba(0, 140, 255, .3);
}
}
</style>
看结果
好像完成了,但是仔细看并不是那么回事。当鼠标从左上方向右下方滑动可以触发划区,其他任何方向滑动都不行。
去mousemove
方法中打印当前划区信息,就能发现宽高可能会是负数。造成这一结果的原因就是在计算宽高的时候没有规划到负值,但负值也是宽高。那么加上Math.abs()
方法
js
// 计算出滑动区域的宽高
areaInfo.value.width = Math.abs(clientX - startX)
areaInfo.value.height = Math.abs(clientY - startY)
看效果
东西是出来了,但是位置不对,仔细观察之后,现在的划区生成方式都是从左上到右下。
造成这种问题的原因是,当从右下角往左上角滑动的时候,当前生成的划区的开始坐标和结束坐标还是在一开始鼠标按下的那一个位置。此时应当改变开始和结束的坐标。 在mousemove
中加一个判断
js
const mousemove = (e) => {
if (!isStart.value) { return }
const { startX, startY } = areaInfo.value
const { clientX, clientY } = e
// 计算出滑动区域的宽高
areaInfo.value.width = Math.abs(clientX - startX)
areaInfo.value.height = Math.abs(clientY - startY)
// ----------------------------------------------------------新增
if (clientX < startX) {
areaInfo.value.left = clientX
}
if (clientY < startY) {
areaInfo.value.top = clientY
}
// ----------------------------------------------------------新增
}
但这样会引发另一个问题,此时宽高和开始坐标都在实时变化,那么宽高计算出来的一定会是0。要避免这个问题那么开始的坐标是不能直接改的,需要引入新的变量。
js
// 鼠标滑动区域信息
const areaInfo = ref({
startX: 0,
startY: 0,
width: 0,
height: 0,
// ----------新增
left: 0,
top: 0
// ----------新增
})
// 框选区域的样式
const areaStyle = computed(() => {
return areaInfo.value.width && {
width: areaInfo.value.width + 'px',
height: areaInfo.value.height + 'px',
// ----------------------------------------------替换
// left: areaInfo.value.startX + 'px',
// top: areaInfo.value.startY + 'px',
left: areaInfo.value.left + 'px',
top: areaInfo.value.top + 'px',
// ----------------------------------------------替换
}
})
// left和top同样也需要在mousedown中赋值, 当从左上到右下滑动时left、top不变
const mousedown = (e) => {
isStart.value = true
// 获取开始点的位置信息
// ------------------------------------------------------替换
// areaInfo.value.startX = e.clientX
// areaInfo.value.startY = e.clientY
areaInfo.value.startX = areaInfo.value.left = e.clientX
areaInfo.value.startY = areaInfo.value.top = e.clientY
// ------------------------------------------------------替换
}
看效果
完美实现
添加框选
首先生成十个随机位置和大小的元素
js
const componentsList = ref([
{ id: 1, selected: false, style: { left: 112, top: 59, width: 78, height: 100, borderRadius: 10 }},
{ id: 2, selected: false, style: { left: 200, top: 100, width: 23, height: 55 }},
{ id: 3, selected: false, style: { left: 435, top: 223, width: 88, height: 50, borderRadius: 30 }},
{ id: 4, selected: false, style: { left: 225, top: 500, width: 180, height: 180 }},
{ id: 5, selected: false, style: { left: 376, top: 50, width: 20, height: 190, borderRadius: 30 }},
{ id: 6, selected: false, style: { left: 200, top: 250, width: 100, height: 100 }},
{ id: 7, selected: false, style: { left: 250, top: 150, width: 100, height: 100, borderRadius: '50%' }},
{ id: 8, selected: false, style: { left: 550, top: 200, width: 100, height: 100, borderRadius: 30 }},
{ id: 9, selected: false, style: { left: 800, top: 460, width: 100, height: 100 }},
{ id: 10, selected: false, style: { left: 900, top: 100, width: 100, height: 100, borderRadius: 30 }},
])
html
<template>
<div class="container" @mousedown="mousedown" @mousemove="mousemove" @mouseup="mouseup">
<div class="area" :style="areaStyle"></div>
<!-- ---------------------------新增------------------------------- -->
<div
class="component"
v-for="item in componentsList"
:key="item"
:style="{
left: item.style?.left + 'px',
top: item.style?.top + 'px',
width: item.style?.width + 'px',
height: item.style?.height + 'px',
borderRadius: typeof item.style?.borderRadius == 'string' ? item.style?.borderRadius : item.style?.borderRadius + 'px'
}"
:class="[item.selected && 'selected']">
{{ item.id }}
</div>
<!-- ---------------------------新增------------------------------- -->
</div>
</template>
css
.component{
background-color: rgb(55, 165, 255);
text-align: center;
color: #fff;
position: absolute;
}
.selected{
background-color: green;
opacity: 0.5;
}
这样就生成了十个元素。由于是用在低代码平台中,元素都有自己的位置和大小等信息,就不主动获取页面目标元素的大小位置。
要判断元素被完全框选住了,就是判断两个点。划区的左上角的点与目标元素左上角对比,划区的右下角与目标元素的右下角对比。划区元素左上角坐标要比目标元素小,划区元素右下角坐标要比目标元素大。
在mousemove
中添加判断
js
const mousemove = (e) => {
if (!isStart.value) { return }
const { startX, startY } = areaInfo.value
const { clientX, clientY } = e
// 计算出滑动区域的宽高
areaInfo.value.width = Math.abs(clientX - startX)
areaInfo.value.height = Math.abs(clientY - startY)
if (clientX < startX) {
areaInfo.value.left = clientX
}
if (clientY < startY) {
areaInfo.value.top = clientY
}
// --------------------------------------------------------------------新增
componentsList.value.forEach(item => {
const { left, top, width, height } = item.style
const { left: aLeft, top: aTop, width: aWidth, height: aHeight } = areaInfo.value
if (
aLeft < left && aTop < top && // 左上角
aLeft + aWidth > left + width && aTop + aHeight > top + height // 右下角
) {
item.selected = true
} else {
item.selected = false
}
})
// --------------------------------------------------------------------新增
}
看效果
完美!