web系列之拖拽(实现一个课程表拖拽)

2,407 阅读6分钟

前言

拖拽(Draggle)是一种用户交互方式,更加直观地操作网页元素,提高用户体验。

本文将介绍拖拽、应用场景以及实现一个课程表拖拽案例。

效果如下:

course-ezgif.com-video-to-gif-converter.gif

1. 拖拽介绍

HTML 拖拽(Drag and Drop)接口使应用程序能够在浏览器中使用拖放功能。

例如,用户可使用鼠标选择可拖拽(draggable)元素,将元素拖拽到可放置(droppable)元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。

1.1 DataTransfer接口

介绍两个属性:

  • DataTransfer.dropEffect:获取当前选定的拖放操作类型或者设置的为一个新的类型。值必须为 nonecopylink 或 move

  • DataTransfer.effectAllowed:提供所有可用的操作类型。必须是 nonecopycopyLinkcopyMovelinklinkMovemoveall or uninitialized 之一。

通过这个接口,我们可以控制拖拽元素的显示,和操作类型。

例如:当effectAllowed设置为move时,拖拽元素是不会出现+号。

1.2 常用事件介绍

主要介绍四个事件,一个拖拽元素操作过程会分别触发

  • dragstart:当用户开始拖动元素或选择文本时触发此事件
  • dragover:当将元素或文本选择拖动到有效放置目标(每几百毫秒)上时,会触发此事件
  • dragenter:当拖动的元素或选择文本输入有效的放置目标时,会触发此事件
  • drop:拖动元素或选择文本时放开时触发此事件,注意:需要阻止浏览器默认行为

2. 拖拽技术的应用场景

拖拽技术常用场景有:

  1. 列表排序:通过拖拽列表项,用户可以轻松地调整列表的顺序。
  2. 图片编辑:在图片编辑工具中,用户可以通过拖拽来调整图片的位置、大小或角度。
  3. 窗口管理:在桌面或移动应用中,用户可以通过拖拽来移动、调整或关闭窗口。
  4. 数据可视化:在数据可视化应用中,用户可以通过拖拽来筛选、排列或操作数据等

3. 实现课程表案例

3.1 准备静态页面

左侧课程,右侧表格

3.2 添加拖拽事件

要想完成拖拽就需要给左侧的元素添加draggable为true的属性,即可完成拖拽。开始定义拖拽事件:

// 拖拽的元素
container.ondragstart = function (e) {
      
}
// 经过哪个元素
container.ondragover = function (e) {
    // 需要阻止默认行为,否则drop事件无法触发
    e.preventDefault()
       
}
// 放在哪个元素上方
container.ondragenter = function (e) {
        
}
// 松手  浏览器有默认行为,阻止拖放
container.ondrop = function (e) {
        
}

3.3 实现具体逻辑

  1. 第一个要考虑的问题从左边拖拽的元素是需要显示添加效果,而在课程表中的放置的元素是不需要显示添加效果

通过dataTransfer.effectAllowed进行控制。如何进行控制,就需要在拖拽元素上添加自定义data-effect属性为copy,在开始的事件中进行定义,后面拖拽完成只需要修改为move即可。

container.ondragstart = function (e) {
    // 通过dataset设置默认效果
    e.dataTransfer.effectAllowed = e.target.dataset.effect
}
  1. 拖拽到目标元素添加背景颜色,如何确定目标元素是否允许添加

可以通过自定义属性data-drop定义为copy,说明该元素允许接受copy,即可添加背景色,例如添加类drop-over,添加之前需要先清除。例如左侧只接受data-dropmove,不需要每个元素添加自定义属性,只需要在父元素添加一个即可,所以查找元素时,只要自身或者父元素满足条件即可。

// 清除元素的背景颜色
function clearDropStyle() {
    document.querySelectorAll('.drop-over').forEach(node => {
        node.classList.remove('drop-over')
    })
}
// 只要自身或者父元素满足条件即可
function getDropNode(node) {
    while (node) {
        if (node.dataset && node.dataset.drop) {
            return node
        }
        // 否则向上查找
        node = node.parentNode
    }
}

// 放在哪个元素上方
container.ondragenter = function (e) {
    // 需要清除之前的drop-over
    clearDropStyle()
    // 添加背景颜色,需要判断dataset属性为copy并且是不是等于拖拽元素的e.dataTransfer.effectAllowed 
    const dropNode = getDropNode(e.target)
    if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
        dropNode.classList.add('drop-over')
    }
}

  1. 拖动到指定单元格

到达目标元素,需要清除样式,获取拖拽元素,

  • 判断是否为copy状态,如果是,先清空目标节点内容,修改data-effectmove,避免再次拖拽,添加到目标节点即可
  • 如果原节点不是可copy状态,直接删除即可
// 拖拽的哪个元素
let sourceNode
container.ondragstart = function (e) {
    // 默认是copy,通过dataset设置默认效果
    e.dataTransfer.effectAllowed = e.target.dataset.effect
    sourceNode = e.target
}
// 松手  浏览器有默认行为,阻止拖放
container.ondrop = function (e) {
    console.log('打印***e.target', e.target)
    // 需要清除之前的drop-over
    clearDropStyle()
    const dropNode = getDropNode(e.target)
    // 目标节点是copy
    if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
        if (dropNode.dataset.drop === 'copy') {
            // 避免多次添加
            dropNode.innerHTML = ''
            const cloneNode = sourceNode.cloneNode(true)
            cloneNode.dataset.effect = 'move'
            dropNode.appendChild(cloneNode)
        } else {
            // 删除拖拽节点
            sourceNode.remove()
        }
    }
}

至此,所有的逻辑就完成了。

完整代码:

```html
<style>
    .container {
            margin: 0;
            display: flex;
            align-items: center;
            width: 100vw;
            height: 100vh;

    }

    .left {
            display: flex;
            justify-content: space-around;
            align-items: center;
            flex-direction: column;
            width: 100px;
            height: 400px;
            flex-shrink: 0;
            background-color: whitesmoke;
    }

    .item {
            width: 80px;
            height: 50px;
            text-align: center;
            line-height: 50px;
    }

    .color1 {
            background-color: pink;
    }

    .color2 {
            background-color: red;
    }

    .color3 {
            background-color: orange;
    }

    .color4 {
            background-color: yellow;
    }

    .color5 {
            background-color: green;
    }

    .color6 {
            background-color: blue;
    }


    .right {
            background-color: whitesmoke;
            margin: 0 20px 0 12px;
    }

    .right table td {
            width: 80px;
            height: 50px;
    }

    .drop-over {
            background-color: #999;
    }
</style>


<div class="container">
    <div class="left" data-drop="move">
        <div data-effect="copy" draggable="true" class="color1 item">语文</div>
        <div data-effect="copy" draggable="true" class="color2 item">数学</div>
        <div data-effect="copy" draggable="true" class="color3 item">英语</div>
        <div data-effect="copy" draggable="true" class="color4 item">历史</div>
        <div data-effect="copy" draggable="true" class="color5 item">地理</div>
        <div data-effect="copy" draggable="true" class="color6 item">政治</div>
    </div>
    <div class="right">
        <table border>
            <caption>
                <h1>课程表</h1>
            </caption>
            <colgroup>
                <col />
                <col />
                <col />
                <col />
                <col />
                <col />
                <col />
                <col />
            </colgroup>
            <thead>
                <tr>
                    <td></td>
                    <th>星期一</th>
                    <th>星期二</th>
                    <th>星期三</th>
                    <th>星期四</th>
                    <th>星期五</th>
                    <th>星期六</th>
                    <th>星期天</th>
                </tr>
            </thead>
            <tbody>
                    <tr>
                        <th rowspan="4" class="span">上午</th>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>

                    <tr>
                        <td colspan="8"></td>

                    </tr>

                    <tr>
                        <th rowspan="4" class="span">上午</th>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
                    <tr>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                        <td data-drop="copy"></td>
                    </tr>
            </tbody>
        </table>
    </div>
</div>


<script>

const container = document.querySelector('.container')
// 拖拽的哪个元素
let sourceNode
container.ondragstart = function (e) {
        // 默认是copy,通过dataset设置默认效果
        e.dataTransfer.effectAllowed = e.target.dataset.effect
        sourceNode = e.target
}
// 经过哪个元素
container.ondragover = function (e) {
        e.preventDefault()
        // console.log('打印***e.target', e.target)
}

// 清除元素的背景颜色
function clearDropStyle() {
        document.querySelectorAll('.drop-over').forEach(node => {
                node.classList.remove('drop-over')
        })
}

function getDropNode(node) {
        while (node) {
                if (node.dataset && node.dataset.drop) {
                        return node
                }
                // 否则向上查找
                node = node.parentNode
        }
}

// 放在哪个元素上方
container.ondragenter = function (e) {
        // 需要清除之前的drop-over
        clearDropStyle()
        // 添加背景颜色,需要判断dataset属性为copy并且是不是等于拖拽元素的e.dataTransfer.effectAllowed 
        const dropNode = getDropNode(e.target)
        if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {

                dropNode.classList.add('drop-over')
        }
}
// 松手  浏览器有默认行为,阻止拖放
container.ondrop = function (e) {
        console.log('打印***e.target', e.target)
        // 需要清除之前的drop-over
        clearDropStyle()
        const dropNode = getDropNode(e.target)
        // 目标节点是copy
        if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
                if (dropNode.dataset.drop === 'copy') {
                        // 避免多次添加
                        dropNode.innerHTML = ''
                        const cloneNode = sourceNode.cloneNode(true)
                        cloneNode.dataset.effect = 'move'
                        dropNode.appendChild(cloneNode)
                } else {
                        // 删除拖拽节点
                        sourceNode.remove()
                }
        }
}
</script>

4. 总结

最后总结一下,主要介绍了 HTML 拖拽接口(Drag and Drop API)的基本概念,包括 DataTransfer 接口和常用的拖拽事件。

详细介绍了如何在静态页面中实现一个课程表的拖拽功能。首先,通过设置元素的 draggable 属性为 true,使元素可拖拽。然后,通过监听拖拽事件,在事件处理函数中实现拖拽过程中的逻辑,包括拖拽元素的显示效果、拖拽元素的移动和放置。

记录一下,如有错误,请指正O^O!