实现元素拖拽排序,需要考虑一下几个问题
- 监听鼠标拖拽事件
- 触发监听的元素是window还是父元素
- DOM元素实现基本拖拽的属性 - draggable
- 拖拽停止后,如何对整个元素进行重新排序
关于【拖拽排序】的事件
dragstart:当被拖拽元素开始被拖拽时触发drag:当被拖拽元素被拖拽时触发dragenter:当被拖拽元素进入目标元素时触发dragover:当被拖拽元素在目标元素上移动时触发dragleave:当被拖拽元素离开目标元素时触发drop:当被拖拽元素在目标元素上,而且释放鼠标左键时触发dragend:当拖拽行为结束后触发
(当然,在这个例子中不需要监听全部的拖拽事件)
拖拽事件的【生命周期】
- 生命周期:
dragstart -> drag -> dragenter -> dragover -> dragleave -> drop -> dragend
事件监听
- mousedown
- 给当前将要被拖拽的元素添加【拖拽属性 - draggable】
- 缓存当前元素
- dragstart
- 增加拖拽样式
- 缓存当前拖拽元素在父元素中的下标
- dragenter
- 缓存目标元素在父元素中的下标
- 对比下标,元素重排
- dragend
- 给当前将要被拖拽的元素移除【拖拽属性】
- 移除拖拽样式
初始化一个对象属性 - dragSortObject
const dragSortObject = {
dragListNode: null, // 父元素
dragSortDom: null, // 子元素(被排序的元素)
currentIndex: 0, // 当前元素在父元素中的下标
tragetIndex: 0, // 目标元素在父元素中的下标
// 初始化拖动排序函数
initDragSort() {
dragListNode = document.getElementById('drag-list');
if (!dragListNode) return
// 监听【mousedown】
dragListNode.addEventListener('mousedown', event => this.handleMouseDown(event))
// 监听【dragstart】
dragListNode.addEventListener('dragstart', event => this.handleDragStartFunc(event))
// 监听【dragend】
dragListNode.addEventListener('dragend', event => this.handleDragEnd(event))
// 监听【dragenter】
dragListNode.addEventListener('dragenter', event => this.handleDragEnter(event))
// 监听【dragover】
dragListNode.addEventListener('dragover', event => this.handleDragOver(event))
},
}
handleMouseDown的实现
// mosueDown
handleMouseDown(event) {
if (dragListNode != event.target) {
dragSortDom = event.target;
// 设置当前元素可拖动
dragSortDom.draggable = true;
}
},
- 【问题】
- 到这里有人或许会问,为什么不把在
mousedown中的业务逻辑放到dragstart中去处理呢?
- 到这里有人或许会问,为什么不把在
- 【回答】
- 这个
draggable属性跟dragstart事件是有先后顺序的。draggable属性 触发 dragstart事件
- 这个
handleDragStartFunc的实现
// dragstart
handleDragStartFunc(event) {
// MDN属性解释 - https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/effectAllowed
// 为拖动源设置所需的拖动效果
event.dataTransfer.effectAllowed = 'move';
// 增加拖动样式 - 增加透明度
dragSortDom.classList.add('moving');
// 获取当前拖动元素在父元素中的下标
currentIndex = this.findIndexOfdragListNode(event);
},
在这里,寻找当前元素在父元素中的下标封装成一个公共方法 - findIndexOfdragListNode
findIndexOfdragListNode的实现
// 当前元素在父元素中的下标
findIndexOfdragListNode(event) {
const currentIndex = Array.prototype.indexOf.call(dragListNode.childNodes, event.target)
return currentIndex
},
handleDragEnter的实现
// dragenter
handleDragEnter(event) {
event.preventDefault();
// 如果target元素是父元素或者是被拖动元素自身, 则不做任何操作
if (event.target == dragListNode || event.target == dragSortDom) return;
// 记录目标元素在父元素中的下标
const targetIndex = this.findIndexOfdragListNode(event);
if (currentIndex < targetIndex) {
// 下标:当前元素 < 目标元素 => 上移
dragListNode.insertBefore(dragSortDom, event.target.nextSibling)
} else {
// 下标:当前元素 > 目标元素 => 下移
dragListNode.insertBefore(dragSortDom, event.target)
}
},
注意此处【下标:当前元素 < 目标元素 => 上移】的情况,是需要把当前元素插入到目标元素的下一个元素 - nextSibling
handleDragEnd的实现
// dragend
handleDragEnd(event) {
dragSortDom.draggable = false
// 给被拖动元素去除透明度
dragSortDom.classList.remove('moving')
},
这里为什么要在 dragstart 和 dragend 中才去给元素添加/去除 draggable,而不在HTML中直接写死呢?
直接在html中写死也可,但是从代码解析效率和可阅读性上考虑就不太友好
handleDragOver的实现
// dragover
handleDragOver(event) {
event.preventDefault();
}
HTML
<head>
<style>
*{
margin: 0;
padding: 0;
}
ul {
list-style-type: none;
}
li {
margin-bottom: 4px;
width: 200px;
padding: 15px;
background-color: antiquewhite;
border: 1px solid antiquewhite;
border-radius: 4px;
user-select: none;
}
.moving {
opacity: 0.3;
}
</style>
</head>
<body>
<ul id="drag-list">
<li>Item1</li>
<li>Item2</li>
<li>Item3</li>
<li>Item4</li>
<li>Item5</li>
</ul>
</body>
结果展示
dragenter VS dragover
dragenter事件- 是元素进入目标元素后触发
dragover事件- 是元素在目标元素上移动时触发, 100ms触发一次
代码参考
- GitLab: Vincent个人项目仓库 - 元素拖动排序