这里介绍的是最复杂的网格拖曳排序,涵盖了垂直列表拖曳排序和水平列表拖曳排序的情形。
在页面上画一个 3x3 的网格,子项添加 draggable="true"
,目的是让元素变的可拖动,如下图。如果没有 draggable 属性,是没有这种动画的。
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
width: 300px;
border: 1px solid #ccc;
.item {
background-color: aquamarine;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
.item.dragging {
opacity: 0.2;
}
}
</style>
<div class="grid">
<div class="item" draggable="true">1</div>
<div class="item" draggable="true">2</div>
<div class="item" draggable="true">3</div>
<div class="item" draggable="true">4</div>
<div class="item" draggable="true">5</div>
<div class="item" draggable="true">6</div>
<div class="item" draggable="true">7</div>
<div class="item" draggable="true">8</div>
<div class="item" draggable="true">9</div>
</div>
获取网格父元素,添加 dragstart 事件,将当前拖动的元素赋值给 sourceEl,称为源元素,同时加一个 dragging 的类名,让当前拖动的元素区别于其它子项变的半透明,如下图。
const gridEl = document.querySelector('.grid')
// 被拖动元素
let sourceEl
gridEl.addEventListener('dragstart', e => {
sourceEl = e.target
sourceEl.classList.add('dragging')
})
网格父元素添加 dragenter 事件。比如拖动 1,把它放到 2 上面,2 会触发 dragenter 事件,此时 e.target 表示的是 2,我们称为目标元素 targetEl。
如果目标元素 (targetEl) 是拖动元素 (sourceEl),或者目标元素 (targetEl) 是网格元素 (gridEl),直接返回不做处理。
compareDocumentPosition() 这个 api 可以比较两个子项的前后位置。
知道源元素 (sourceEl) 和目标元素 (targetEl) 的前后位置后,就可以判断出拖动的方向,从而利用 insertBefore() api 将源元素插入到目标元素的前面或者后面。
gridEl.addEventListener('dragenter', e => {
e.preventDefault()
const targetEl = e.target
if (targetEl === sourceEl || targetEl === gridEl) {
return
}
// sourceEl 在 targetEl 前面,返回 4
// sourceEl 在 targetEl 后面,返回 2
const compareMask = sourceEl.compareDocumentPosition(targetEl)
// 从后往前拖动
if (compareMask === 2) {
targetEl.parentNode.insertBefore(sourceEl, targetEl)
}
// 从前往后拖动
if (compareMask === 4) {
targetEl.parentNode.insertBefore(sourceEl, targetEl.nextSibling)
}
})
到这里,已经可以改变子项排序了,但还有两个小问题:一个是拖动结束后子项仍然是透明的(第2步dragstart事件里面添加的类名导致),另一个是拖动结束后有个很奇怪的动画。如下图:
添加 dragend 事件,拖动结束,也就是鼠标松开之后,移除源元素的 dragging 类名。
gridEl.addEventListener('dragend', e => {
sourceEl.classList.remove('dragging')
})
添加 dragover 事件,阻止 dragover 默认行为,可以解决拖动结束后有个奇怪动画的问题。
gridEl.addEventListener('dragover', e => {
e.preventDefault()
})
完整代码如下:
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
width: 300px;
border: 1px solid #ccc;
.item {
background-color: aquamarine;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
.item.dragging {
opacity: 0.2;
}
}
</style>
<div class="grid">
<div class="item" draggable="true">1</div>
<div class="item" draggable="true">2</div>
<div class="item" draggable="true">3</div>
<div class="item" draggable="true">4</div>
<div class="item" draggable="true">5</div>
<div class="item" draggable="true">6</div>
<div class="item" draggable="true">7</div>
<div class="item" draggable="true">8</div>
<div class="item" draggable="true">9</div>
</div>
<script>
const gridEl = document.querySelector('.grid')
// 被拖动元素
let sourceEl
gridEl.addEventListener('dragstart', e => {
sourceEl = e.target
sourceEl.classList.add('dragging')
})
gridEl.addEventListener('dragenter', e => {
e.preventDefault()
const targetEl = e.target
if (targetEl === sourceEl || targetEl === gridEl) {
return
}
// sourceEl 在 targetEl 前面,返回 4
// sourceEl 在 targetEl 后面,返回 2
const compareMask = sourceEl.compareDocumentPosition(targetEl)
// 从后往前拖动
if (compareMask === 2) {
targetEl.parentNode.insertBefore(sourceEl, targetEl)
}
// 从前往后拖动
if (compareMask === 4) {
targetEl.parentNode.insertBefore(sourceEl, targetEl.nextSibling)
}
})
gridEl.addEventListener('dragend', e => {
sourceEl.classList.remove('dragging')
})
gridEl.addEventListener('dragover', e => {
e.preventDefault()
})
</script>
↶ 返回首页