js 实现网格中拖曳排序功能

2024-07-12

一、效果演示

这里介绍的是最复杂的网格拖曳排序,涵盖了垂直列表拖曳排序和水平列表拖曳排序的情形。

二、代码实现

在页面上画一个 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>

返回首页

本文总阅读量  次
总访问量: 
总访客量: