最近博主公司出现一个需求,做一个云盘项目(或许未来的你能用得上),让仿照windows系统实现多选功能,虽然项目上就放一个Ctrl+多选,但我一开心,历经两天,其他的也都实现了。查阅各种资料,某度上天花乱坠,但很多方法还是严重不合格。所以将个人研究的公布出来,大家喜欢可借鉴,研发不易,希望给个赞👍。
框选效果
Ctrl 多选效果
Shift 连选效果
300行代码实现所有功能,简单做了一些注释,将思路给大家,专研一下,能看懂。需要注意的是,毕竟html页面实现,性能上还需继续优化。
实现功能:
1、点击Ctrl ,可多选,可取消选过文件。
2、点击shift,点击两个,中间可连选。
3、按下鼠标移动,可框选。
4、单机,可单选。
5、按下空白处,取消所有选项。
研发思路:
1、所有实现中,都使用列表中的index作为选中标识。并放进一个独立数组中,我起名为 selectList 。
2、所有list列表中添加一个标识,作为是否选中的样式判断,这样做是最方便的,标识没有自己手动添加,或让后端给加入一个标识字段,对他们来说很简单。
3、每次选中后,都将遍历所有list列表,将符合条件的index对象中,标识变为true,否则为false。
4、监听键盘事件,获取到shift 和ctrl的keycode,由于博主mac本,所以需要多加一个command 。
一、多选思路:
点击时判断 selectList 数组中有没有重复index,有则删除,没有则添加。
二、连选思路:
a、连选是随意点击两个li,并将中间的全部选中。
b、判断 selectList.length 是否大于1,判断最低是两个数,可以实现连选。
c、用户可能从前往后选,也可能从后往前选,所以判断 selectList 的第一个index 和最后一个 index 的大小,并从小到大排序,做一个for循环,将中间index都填入到 selectList 中,遍历点亮数组。
注意:本次实现,将ctrl点击的一同带入,所以和 windows 系统略有差异。
三、框选思路:
a、css禁用浏览器默认选择效果,本次用的fixed定位。
b、js实现选框的效果。
c、判断框选的覆盖范围,与list的交集,拿到交集的index。
框选避坑处:
a、现实开发中,最外层盒子和ul的高度未必统一,以及ul 的高度未必100%,我们可能回根据最外层盒子做框选效果,所以建议用fixed定位。
b、获取 li 坐标。因为所有 li 的盒子的坐标,是针对父盒子做的定位,并不是针对窗口坐标位置,所以需要加上父盒子针对窗口的坐标高度,就是li 针对窗口的坐标。
c、页面滚动时,父盒子针对窗口的坐标高度不会更新,所以监听滚动时间,实时更新变化。
b、注意性能问题,本次没有实时更新 li 坐标,而是每个 li 直接加上父盒子针对窗口的坐标高度。
代码展示:
<template>
<div id='se'
@mousedown.stop="handleBox">
<ul id="ul">
<li v-for="(item,index) in list"
:key="index"
@click.stop="handleClick(index)"
:class="item.isS?'isS':''"
class="li">{{item.name+ (index+1)}}</li>
</ul>
<div v-show="isShowSeBox"
id="selection"><span>VX:VillinWeChat</span><span>🙏谢谢送个👍</span></div>
</div>
</template>
<script>
export default {
components: {},
data() {
return {
list: [
{
name: '列表',
id: 1,
isS: false,
},
{
name: '列表',
id: 2,
isS: false,
},
{
name: '列表',
id: 3,
isS: false,
},
{
name: '列表',
id: 4,
isS: false,
},
{
name: '列表',
id: 5,
isS: false,
},
{
name: '列表',
id: 6,
isS: false,
}, {
name: '列表',
id: 7,
isS: false,
}, {
name: '列表',
id: 8,
isS: false,
}, {
name: '列表',
id: 9,
isS: false,
}, {
name: '列表',
id: 10,
isS: false,
},
{
name: '列表',
id: 11,
isS: false,
},
{
name: '列表',
id: 12,
isS: false,
},
],
isshift: false, // 是否按下 shift
isctrl: false, // 是否按下 ctrl
isonkeydown: false, // 是否按下键盘
selectBox: null, // 框选盒子
isShowSeBox: false, // 框选是否显示div
selectList: [], // 每次点击,push进去点击的index
lipL: 0, // 框选li的父盒子针对于窗口的left
lipT: 0, // 框选li的父盒子针对于窗口的top
};
},
mounted() {
this.selectBox = document.getElementById("selection");
this.parentBox = document.getElementById("se");
this.handleScroll()
},
computed: {},
watch: {},
methods: {
handleBox(even) {
if (!this.isctrl && !this.isshift && this.selectList.length != 0) {
this.selectList = []
this.loop()
}
// 按下时的坐标
this.downX = even.clientX
this.downY = even.clientY
this.selectBox.style.left = this.downX + "px";
this.selectBox.style.top = this.downY + "px";
document.body.addEventListener('mousemove', this.moveselecttion)
document.body.addEventListener('mouseup', this.mouseupSelection)
window.addEventListener("scroll", this.handleScroll)
window.addEventListener('keydown', this.clickKeydown)
window.addEventListener('keyup', this.clickKeyup)
},
// 页面滚动 滚动时,实时更新父盒子横纵坐标
handleScroll() {
//获取li父元素的纵坐标(相对于窗口)
this.lipT = this.parentBox.getBoundingClientRect().top
//获取li父元素的横坐标(相对于窗口)
this.lipL = this.parentBox.getBoundingClientRect().left
},
// 键盘按下
clickKeydown(e) {
switch (e.keyCode) {
case 16:
this.isshift = true;
break;
case 17: // window 键盘
this.isctrl = true;
break;
case 91: // mac command 按键
this.isctrl = true;
break;
}
},
// 键盘松开
clickKeyup(e) {
this.isshift = false
this.isctrl = false
},
handleClick(index) {
// shift 连选
if (this.isshift) {
if (this.selectList.indexOf(index) == -1) {
this.selectList.push(index)
}
if (this.selectList.length > 1) {
// 按 shift 连选,通过判断第一次点击index和最后一次index,中间全部选中
let le = this.selectList.length
var min = Math.min(this.selectList[0], this.selectList[le - 1])
var max = Math.max(this.selectList[0], this.selectList[le - 1])
this.selectList = [] // 防止最初始的两次点击,多处的两个index
for (let i = min; i <= max; i++) {
this.selectList.push(i)
}
}
this.loop()
}
// ctrl 多选 可重复单机取消
else if (this.isctrl) {
if (this.selectList.indexOf(index) == -1) {
this.selectList.push(index)
} else {
// 此处判断是否存在高亮index,如果存在,再次点击后,则删除此index,达到二次点击取消选区效果
this.selectList.splice(this.selectList.indexOf(index), 1)
}
this.loop()
}
// 单选
else {
this.selectList = []
this.selectList.push(index)
this.loop()
}
},
// 鼠标移动事件
moveselecttion(even) {
this.isShowSeBox = true;
this.selectBox.style.left = Math.min(even.clientX, this.downX) + "px";
this.selectBox.style.top = Math.min(even.clientY, this.downY) + "px";
this.selectBox.style.width = Math.abs(this.downX - even.clientX) + "px";
this.selectBox.style.height = Math.abs(this.downY - even.clientY) + "px";
this.covered()
},
// 鼠标松开事件
mouseupSelection(even) {
this.isShowSeBox = false;
this.selectBox.style.width = 0 + "px";
this.selectBox.style.height = 0 + "px";
document.body.removeEventListener('mousemove', this.moveselecttion)
document.body.removeEventListener('mouseup', this.mouseupSelection)
},
// 是否被覆盖
covered() {
let li = document.getElementsByClassName('li')
if (!this.isctrl && !this.isctrl) {
this.selectList = []
}
let l = parseInt(this.selectBox.style.left)
let t = parseInt(this.selectBox.style.top)
let w = parseInt(this.selectBox.style.width)
let h = parseInt(this.selectBox.style.height)
for (let i = 0; i < li.length; i++) {
if (this.selectList.indexOf(i) == -1) {
// 子元素是相对父元素定位的,所以父元素有定位高度,offsetLeft 和 offsetTop不准
let sl = li[i].offsetWidth + li[i].offsetLeft + this.lipL;
let st = li[i].offsetHeight + li[i].offsetTop + this.lipT;
if (sl > l && st > t && li[i].offsetLeft + this.lipL < l + w && li[i].offsetTop + this.lipT < t + h) {
this.selectList.push(i)
}
this.loop()
}
}
},
// 循环遍历,在这里,可以拿到所选的item
loop() {
this.list.forEach((e, index) => {
e.isS = false
this.selectList.filter(item => {
if (item == index) {
e.isS = true
}
})
});
},
},
destroyed() {
document.body.removeEventListener('mousemove', this.moveselecttion)
document.body.removeEventListener('mouseup', this.mouseupSelection)
window.removeEventListener("scroll", this.handleScroll)
window.removeEventListener('keydown', this.clickKeydown)
window.removeEventListener('keyup', this.clickKeyup)
}
}
</script>
<style lang='less' scoped>
#se {
position: relative;
width: 100%;
height: 100%;
-moz-user-select: none; /*火狐*/
-webkit-user-select: none; /*webkit浏览器*/
-ms-user-select: none; /*IE10*/
-khtml-user-select: none; /*早期浏览器*/
user-select: none;
}
ul {
display: flex;
flex-wrap: wrap;
width: 500px;
margin: 50px auto;
li {
box-sizing: border-box;
padding: 5px 10px;
border: 1px solid #ccc;
margin: 10px;
cursor: pointer;
}
.isS {
background: #99f;
}
}
#selection {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: linear-gradient(
to top left,
rgba(218, 27, 198, 0.5),
rgba(255, 222, 32, 0.2)
);
color: rgb(36, 138, 129);
font-size: 16px;
width: 0px;
height: 0px;
overflow: hidden;
}
</style>
欢迎提出宝贵意见。
喜欢请点赞!