LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

拖动选中和拖拽排序

2022/10/27 demo js demo

前言:因为做项目时有类似于九宫格展示图片,然后可以点击选择图片,当时提了一下可以做一个鼠标按下拖动多选功能,后来因为其它需求,这个功能被舍弃掉,但我还是研究了一下,这里做了个demo,并添加了拖动排序等功能。

以下是demo效果图,这里是demo地址


1.html结构,css略过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
<li>11</li>
<li>12</li>
<li>13</li>
<li>14</li>
<li>15</li>
<li>16</li>
<li>17</li>
<li>18</li>
<li>19</li>
<li>20</li>
<li>21</li>
<div id="mask"></div>
</ul>
<ul class="list">
</ul>

第一个ul元素是用来存放待选择元素的,这里面有个div元素,平常是不可见的,只有鼠标按下时用来显示遮罩层的,效果是这样的
第二个ul元素是用来存放选中拖拽过来的元素

2. js

首先需要思考个问题,怎么才能得到被遮罩层罩住的元素呢?

最开始我是想用dom自带的 IntersectionObserver 来完成观察,后来没研究出来,最后使用坐标来判断

怎么使用坐标来判断呢?

首先拿到遮罩层mark在屏幕中的位置,因为这个是动态变化并赋值给遮罩层mark,所以在赋值的时候记录一下就可以了,然后获取所有li的节点信息,然后根据li的位置信息判断,如果li节点中的任意位置在遮罩层内部,那么认为该节点被选中,这个根据li节点的上下左右边框的位置与遮罩层mark的位置比较就能得出,然后将选中的li节点存放起来并设置一个背景色就可以了。

2.1 拿到遮罩层dom元素,并创建一个markInfo对象,用来存放遮罩层的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
let maskEl=$('#mask')[0]; 

let maskInfo ={ //用于存储c遮罩层的信息
isShow: false , //遮罩层是否显示 默认否
isSelectMode:true,//是否是选择模式 true 为选择 false 为取消
left:0, //遮罩层的left
width:0, //遮罩层的宽,默认0
rightBoundary:0, //遮罩层的右边界 rightBoundary = left + width
top:0,
height:0,
bottomBoundary:0 //下边界 bottomBoundary = top + height
}
let selectedLists= new Set([]);//拖拽多选选中的块集合 防止集合中有重复元素,因此这里选用set
** 注意:因为框选时可能会存在框选已选中的元素,元素智能被选中一次,因此用直接用set来存放选中的元素集合**

2.2 鼠标右键按下时,遮罩层开始显示,鼠标移动时,根据鼠标移动的距离来计算遮罩层的位置信息,因为这个距离是用鼠标按下的距离减去鼠标抬起的距离,因此有正负之分,刚好可以用来区分当前是要选择框选的元素(maskInfo.isSelectMode=true),还是要取消被框选元素的选择状态(maskInfo.isSelectMode=false)。然后再鼠标抬起时,就可以得到最终的遮罩层信息,用来计算哪些li被选中了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

$(".list").mousedown(function(event) {
//屏蔽官方的右键菜单
$(".list")[0].oncontextmenu = function(){return false;}
maskInfo.isShow = event.which === 3 ? true:false; //鼠标右键键选中
maskEl.style.top = event.pageY - event.currentTarget.offsetTop +'px';
maskEl.style.left = event.pageX - event.currentTarget.offsetLeft +'px';
maskInfo.left=event.pageX;
maskInfo.top=event.pageY;
// event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止事件冒泡
});




// 鼠标移动时计算遮罩的位置,宽 高
$(".list").mousemove(function(event) {
if(!maskInfo.isShow) return;//只有开启了拖拽,才进行mouseover操作
const distanceX = maskInfo.left - event.pageX
const distanceY = maskInfo.top - event.pageY
if (distanceX >0) maskEl.style.left = event.pageX - event.currentTarget.offsetLeft + 'px' //向左移动

maskEl.style.width = Math.abs(distanceX) + 'px'
maskInfo.width = Math.abs(distanceX)
if (distanceY > 0 ) maskEl.style.top = event.pageY - event.currentTarget.offsetTop + 'px' //鼠标向上
maskInfo.isSelectMode = (distanceY > 0 || distanceX >0 ) ? false:true

maskEl.style.height = Math.abs(distanceY) + 'px';
maskInfo.height = Math.abs(distanceY)
// event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止事件冒泡
});

//鼠标抬起时计算遮罩的right 和 bottom,找出遮罩覆盖的块,关闭拖拽选中开关,清除遮罩数据
$(".list").mouseup(function(event) {
if(!maskInfo.isShow) return;//只有开启了拖拽,才进行mouseover操作
if (maskInfo.left > event.pageX) maskInfo.left = event.pageX
if (maskInfo.top > event.pageY) maskInfo.top = event.pageY
maskInfo.rightBoundary = maskInfo.left + maskInfo.width
maskInfo.bottomBoundary =maskInfo.top + maskInfo.height

findSelected(); //找到被选中的li
resetMask(); //重置遮罩层数据
handleDrapable() //拖拽事件绑定
// event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止事件冒泡
});

2.3 找到被选中的li。

怎么找到哪些li是被选中的呢?这里我想到了几种发生在li元素上面的情况
  • 任意一个角被遮罩层罩住
  • 遮罩层在某一个li内部
  • 只有上边或者下边,或者上下边被罩住的情况
  • 只有左边或者右边,或者左右边被罩住的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  function findSelected(){
let lists=$('.list').find('li');
for(let i=0;i<lists.length;i++){
//计算每个块的定位信息
let left=$(lists[i]).offset().left;
let rightBoundary=$(lists[i]).width()+left;
let top=$(lists[i]).offset().top;
let bottomBoundary=$(lists[i]).height()+top;
//判断每个块是否被遮罩盖住(即选中) 只要有任意一个部位被遮住,即选中
let leftIn = maskInfo.left <= left && left <= maskInfo.rightBoundary; //元素的左侧在遮罩层中
let rightIn = maskInfo.left <= rightBoundary && rightBoundary <= maskInfo.rightBoundary; //元素的右侧在遮罩层中
let topIn = maskInfo.top <= top && top <= maskInfo.bottomBoundary; //元素的上侧在遮罩层中
let bottomIn= maskInfo.top <= bottomBoundary && bottomBoundary <= maskInfo.bottomBoundary; //元素的下侧在遮罩层中
let topAndBottomOut = maskInfo.top >= top && bottomBoundary >= maskInfo.bottomBoundary //元素上下边框都不在遮罩层中
let leftAndRightOut = maskInfo.left >= left && rightBoundary >= maskInfo.rightBoundary //元素左右边框都不在遮罩层中
let onlyLeftIn = leftIn && topAndBottomOut //只有左侧在遮罩层中
let onlyRightIn = rightIn && topAndBottomOut //只有右侧在遮罩层中
let leftAndRightIn = maskInfo.left <= left && rightBoundary <= maskInfo.rightBoundary && topAndBottomOut //只有左和右侧在遮罩层中

let onlyTopIn = topIn && leftAndRightOut //只有上侧在遮罩层中
let onlyBottomIn = bottomIn && leftAndRightOut //只有下侧在遮罩层中
let bottomAndTopIn = maskInfo.top <= top && bottomBoundary <=maskInfo.bottomBoundary && leftAndRightOut //只有上和下侧在遮罩层中

let allInEL = left <= maskInfo.left && rightBoundary >= maskInfo.rightBoundary && top <= maskInfo.top && bottomBoundary >= maskInfo.bottomBoundary//遮罩层全部在某个元素内部
//左上,左下,右上,上右下,任意一个角在遮罩层中或者遮罩层在某个元素内部,都为被选中元素
if(((leftIn || rightIn) && (topIn || bottomIn)) || onlyLeftIn || onlyRightIn || bottomAndTopIn || leftAndRightIn || onlyTopIn || onlyBottomIn || allInEL){ //左上,左下,右上,上右下,任意一个角在遮罩层中或者遮罩层在某个元素内部,都为被选中元素
if (maskInfo.isSelectMode) {
selectedLists.add(lists[i]);
$(lists[i]).addClass('selected');
lists[i].setAttribute('draggable','true')
}else{
selectedLists.delete(lists[i]);
$(lists[i]).removeClass('selected');
lists[i].setAttribute('draggable','false')
}
}
}
}

注意:因为被选中额元素是可以被拖动的,所以要将被选中的元素添draggable属性设置为true,被取消的元素设置为false

这样选择和取消功能就完成了。


3.选中元素的拖拽排序

我们怎么将元素插入到拖动的位置呢?
Node.insertBefore() 可以完美解决这个问题,详情看MDN
** 因此,只需要找到要背插入的元素和要插入到哪个元素之前就可以了 **

3.1 给要拖动的元素li的父元素绑定拖拽事件

1
2
3
4
5
6
7
8
9
const droppables = document.querySelectorAll('.list');
// 监听droppable的相关事件
for (const droppable of droppables) {
droppable.addEventListener('dragover', dragOver);
droppable.addEventListener('dragleave', dragLeave);
droppable.addEventListener('dragenter', dragEnter);
droppable.addEventListener('drop', dragDrop);
}

3.2 在拖拽事件中找到要插入的元素和插入元素的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function dragOver(e) {
//取消默认行为
event.preventDefault();
let target = event.target;
//因为dragover会发生在ul上,所以要判断是不是li
if (target.nodeName === "LI" && target !== currentDragDom) {
if (target && target.animated) return;
//getBoundingClientRect()用于获取某个元素相对于视窗的位置集合
let targetRect = target.getBoundingClientRect();
let dragingRect = currentDragDom.getBoundingClientRect();
console.log(target.nextSibling)
if (_index(currentDragDom) < _index(target)) {
//nextSibling 属性可返回某个元素之后紧跟的节点(处于同一树层级中)。
target.parentNode.insertBefore(currentDragDom, target.nextSibling);
} else {
target.parentNode.insertBefore(currentDragDom, target);
}
//给元素添加动画和样式
addAnimate(dragingRect, currentDragDom);
addAnimate(targetRect, target);

}
}


//获取元素在父元素中的index
function _index(el) {
let index = 0;
if (!el || !el.parentNode) {
return -1;
}
//previousElementSibling属性返回指定元素的前一个兄弟元素(相同节点树层中的前一个元素节点)。
while (el && (el = el.previousElementSibling)) {
//console.log(el);
index++;
}
return index;
}

4.元素拖拽至另一个容器

只需要将拖拽的元素插入到存放的容器即可
1
2
3
4
5
function dragDrop(e) {
//判断是 拖动元素至另一个容器还是改变顺序
//node.contains(el) 查找该节点是否存在后代节点el 存在返回true 否则为false
if(!e.target.contains(currentDragDom)) this.append(currentDragDom);
}
** 这里要注意的是,当前拖拽操作是拖拽排序还是拖拽至另一个容器**。

以上就是这个拖拽demo的所有内容了,完整代码看这里

img_show