原生JS实现拖拽排序

6,294 阅读3分钟

拖拽(这两个字看了几遍已经不认识了)

说到拖拽,应用场景不可谓不多。无论是打开电脑还是手机,第一眼望去的界面都是可拖拽的,靠拖拽实现APP或者应用的重新布局,或者拖拽文件进行操作文件。

先看效果图,如何实现一个如图HTML元素的拖拽,并排序

Rec 0001.GIF

HTML中的拖拽事件(drag & drop)

参考MDN中文文档

事件类型

  • drag : 当拖拽的元素或者选中的文本时触发

  • dragend : 当拖拽元素结束时触发

  • dragenter : 当拖拽元素或选中的文本到一个可释放目标时触发

  • dragleave : 当拖拽元素或选中的文本离开一个可释放目标时触发

  • dragover : 当元素或选中的文本被拖到一个可释放目标上时触发(每 100 毫秒触发一次)

  • dragstart : 当用户开始拖拽一个元素或选中的文本时触发

  • drop : 当元素或选中的文本在可释放目标上被释放时触发

Coding

写一段简单的CSS和html ,实现初始的页面

css
        *{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        ul{
            margin: 200px auto;
            width: 200px;
            list-style-type: none;
        }
        li{
            margin: 5px;
            text-align: center;
            width: 200px;
            height: 30px;
            background: skyblue;
        }
        .list .moving{
            background: transparent;
            color: transparent;
            border: 1px dashed #ccc;
        }  
html
    <ul class="list">
        <li >1</li>
        <li >2</li>
        <li >3</li>
        <li >4</li>
        <li >5</li>
    </ul>

此时我们的页面如下图

image.png

现在还不可以进行拖拽操作,为了可以实现拖拽操作,我们必须给每个元素设置 draggable="true"

    <ul class="list">
        <li draggable="true">1</li>
        <li draggable="true">2</li>
        <li draggable="true">3</li>
        <li draggable="true">4</li>
        <li draggable="true">5</li>
    </ul>

元素已经可以基础的拖拽

image.png

接下来我们需要在JS中对DOM元素进行一系列操作来实现对应的效果

  1. 实现拖出去的元素,原位置样式变为透明虚线
  2. 实现拖动到其他元素上时,列表顺序发生改变
        let list = document.querySelector('.list')
        let currentLi      // 记录拖拽元素

我们用事件委托,监听 "dragstart" 事件,给拖动的元素添加类名,修改样式,这里会出现奇怪的一幕就是,拖动的样式和原来的样式同时变成了透明。

        list.addEventListener('dragstart',(e)=>{
            e.dataTransfer.effectAllowed = 'move'   // 拖动样式改为 "move"
            currentLi = e.target  
            currentLi.classList.add('moving')
        })

这里会出现奇怪的一幕就是,拖动的样式和原来的样式同时变成了透明。这是因为跟随鼠标拖动的元素的样式在拖动的那一刻是原始元素的样式,所以也会添加"moving", 那么在这里我们加一个异步

image.png

        list.addEventListener('dragstart',(e)=>{
            e.dataTransfer.effectAllowed = 'move'
            currentLi = e.target
            setTimeout(()=>{
                currentLi.classList.add('moving')
            })
        })

image.png

到这里距离目标又更近了一步,

接下来我们需要在拖动的过程中对列表的元素进行重新的排序

Node.insertBefore():方法在参考节点之前插入一个拥有指定父节点的子节点

        list.addEventListener('dragenter',(e)=>{
            e.preventDefault()  // 阻止默认事件
            if(e.target === currentLi||e.target === list){   // 当移动到当前拖动元素,或者父元素上面我们不做操作
                return
            }
            let liArray = Array.from(list.childNodes)
            let currentIndex = liArray.indexOf(currentLi)   // 获取到拖动元素的下标
            let targetindex = liArray.indexOf(e.target)     // 获取到目标元素的下标

            if(currentIndex<targetindex){
                list.insertBefore(currentLi,e.target.nextElementSibling)
            }else{
                list.insertBefore(currentLi,e.target)
            }
        })

最后我们需要在拖拽结束将元素的moving类名移除,以及阻止拖拽到一个目标上的默认事件(否则会出现禁止)


        list.addEventListener('dragover',(e)=>{
            e.preventDefault()
        })
        list.addEventListener('dragend',(e)=>{
            currentLi.classList.remove('moving')
        })

至此,一个简单的拖拽排序功能就实现了

image.png

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        *{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        ul{
            margin: 200px auto;
            width: 200px;
            list-style-type: none;
        }
        li{
            margin: 5px;
            text-align: center;
            width: 200px;
            height: 30px;
            background: skyblue;
        }
        .list .moving{
            background: transparent;
            color: transparent;
            border: 1px dashed #ccc;
        }  
        </style>
</head>
<body>
    <ul class="list">
        <li draggable="true">1</li>
        <li draggable="true">2</li>
        <li draggable="true">3</li>
        <li draggable="true">4</li>
        <li draggable="true">5</li>
    </ul>

    <script>
        let list = document.querySelector('.list')
        let currentLi
        list.addEventListener('dragstart',(e)=>{
            e.dataTransfer.effectAllowed = 'move'
            currentLi = e.target
            setTimeout(()=>{
                currentLi.classList.add('moving')
            })
        })

        list.addEventListener('dragenter',(e)=>{
            e.preventDefault()
            if(e.target === currentLi||e.target === list){
                return
            }
            let liArray = Array.from(list.childNodes)
            let currentIndex = liArray.indexOf(currentLi)
            let targetindex = liArray.indexOf(e.target)

            if(currentIndex<targetindex){
     
                list.insertBefore(currentLi,e.target.nextElementSibling)
            }else{
      
                list.insertBefore(currentLi,e.target)
            }
        })
        list.addEventListener('dragover',(e)=>{
            e.preventDefault()
        })
        list.addEventListener('dragend',(e)=>{
            currentLi.classList.remove('moving')
        })
    </script>
</body>
</html>