在这个例子中,我们将创建一个可排序的列表,其项目可以在其中拖放:
<div id="list">
<div class="draggable">A</div>
<div class="draggable">B</div>
<div class="draggable">C</div>
<div class="draggable">D</div>
<div class="draggable">E</div>
</div>
每个项目都有 draggable
指示用户可以拖动它的类:
.draggable {
cursor: move;
user-select: none;
}
为了使其可拖动,我们需要处理两个事件:
mousemove
(在document
上):计算鼠标移动了多远,并确定元素的位置mouseup
(在document
上):删除document
上的事件处理程序
// 当前拖动项
let draggingEle
// 鼠标相对于拖动元素的当前位置
let x = 0
let y = 0
const mouseDownHandler = function (e) {
draggingEle = e.target
// 计算鼠标位置
const rect = draggingEle.getBoundingClientRect()
x = e.pageX - rect.left
y = e.pageY - rect.top
// 将监听器附加到 document
document.addEventListener('mousemove', mouseMoveHandler)
document.addEventListener('mouseup', mouseUpHandler)
}
const mouseMoveHandler = function (e) {
// 设置拖动元素的位置
draggingEle.style.position = 'absolute'
draggingEle.style.top = `${e.pageY - y}px`
draggingEle.style.left = `${e.pageX - x}px`
}
mouseup
事件处理程序会删除拖动的项目,并清理事件处理程序的位置样式:
const mouseUpHandler = function () {
// 移除位置样式
draggingEle.style.removeProperty('top')
draggingEle.style.removeProperty('left')
draggingEle.style.removeProperty('position')
x = null
y = null
draggingEle = null
// 移除 mousemove 和 mouseup 的处理程序
document.removeEventListener('mousemove', mouseMoveHandler)
document.removeEventListener('mouseup', mouseUpHandler)
}
我们现在有以下几个项目:
ABCDE
例如,当我们拖动一个项目 C
时,下一个项目(D
)将向上移动到顶部,并占据拖动元素(C
)的区域。为了解决这个问题,我们创建了一个动态占位符元素,并将其插入到拖动元素之前。占位符的高度必须与拖动元素的高度相同。
占位符在鼠标移动期间创建一次,因此我们添加一个新标志 isDraggingStarted
来跟踪它:
let placeholder
let isDraggingStarted = false
const mouseMoveHandler = function (e) {
const draggingRect = draggingEle.getBoundingClientRect()
if (!isDraggingStarted) {
// 更新标志
isDraggingStarted = true
// 让占位符占据拖动元素的高度
// 所以下一个元素不会向上移动
placeholder = document.createElement('div')
placeholder.classList.add('placeholder')
draggingEle.parentNode.insertBefore(placeholder, draggingEle.nextSibling)
// 设置占位符的高度
placeholder.style.height = `${draggingRect.height}px`
}
// ...
}
一旦用户放下项目,占位符将被删除:
const mouseUpHandler = function () {
// 移除占位符
placeholder && placeholder.parentNode.removeChild(placeholder)
// 重置标志
isDraggingStarted = false
// ...
}
首先,我们需要一个辅助函数来检查一个项目是在另一个项目之上还是之下。
如果 nodeA
的水平中心点小于 nodeB
,则 nodeA
被视为与 nodeB
相同。节点的中心点可以通过取其顶部和高度的一半之和来计算:
const isAbove = function (nodeA, nodeB) {
// 获取节点的边界矩形
const rectA = nodeA.getBoundingClientRect()
const rectB = nodeB.getBoundingClientRect()
return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2
}
当用户移动项目时,我们定义上一个和下一个同级项目:
const mouseMoveHandler = function (e) {
const prevEle = draggingEle.previousElementSiblin
const nextEle = placeholder.nextElementSibling
}
如果用户将项目移动到顶部,我们将交换占位符和上一个项目:
const mouseMoveHandler = function (e) {
// ...
// 用户将项目移动到顶部
if (prevEle && isAbove(draggingEle, prevEle)) {
swap(placeholder, draggingEle)
swap(placeholder, prevEle)
return
}
}
同样,如果我们检测到用户将项目向下移动到底部,我们将交换下一个和拖动项目:
const mouseMoveHandler = function (e) {
// ...
// 用户将拖动元素移动到底部
if (nextEle && isAbove(nextEle, draggingEle)) {
swap(nextEle, placeholder)
swap(nextEle, draggingEle)
}
}
在这里,swap
是一个用于交换两个节点:
const swap = function (nodeA, nodeB) {
const parentA = nodeA.parentNode
const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling
// 将 nodeA 移到 nodeB 之前
nodeB.parentNode.insertBefore(nodeA, nodeB)
// 将 nodeB 移到 nodeA 的兄弟节点之前
parentA.insertBefore(nodeB, siblingA)
}