学学拖拽 - draggable

如图实现了一个demo:把一个水果从一个盒子拿起来,然后放到另一个盒子。总结以下内部。

DataTransfer

DataTransfer 对象用于保存拖动并放下(drag and drop)过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。

可以在拿起水果时记录一些数据(比如从哪个盒子拿的什么水果),并在往另外的盒子里放的时候读取(判断下这个拿能不能放下这种水果)

水果上事件和属性

  1. 属性 draggable ,设置为 "true",可以让它变的可拖拽。
  2. 事件 dragstart,用户开始拖动元素时调用,也可以理解为拿成水果时触发。

盒子上事件

  1. 事件 dragenter,拖动的元素或者进入一个有效的放置目标时触发,也就是拿起水果进入了盒子范围(松手就掉进去了)时触发。
  2. 事件 dragover,拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。拿着东西在盒子上面晃悠时,盒子每几百毫秒喊会一声。
  3. 事件 dragleave,拖动的元素离开一个有效的放置目标时被触发,和dragenter相反。
  4. 事件 drop,在元素被放置到有效的放置目标上时触发,放进盒子里时触发。为确保 drop 事件始终按预期触发,应当在处理 dragover 事件的代码部分始终包含 preventDefault() 调用。

实现

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <style>
        body,
        html {
            user-select: none;
            height: 100%;
            margin: 0;
        }

        #draggable {
            text-align: center;
            background: white;
        }

        #app {
            display: flex;
            height: 100%;
        }

        .box {
            width: 200px;
            border: 1px solid blueviolet;
            margin: 10px;
            padding: 10px;
            transition: all 0.5s;
        }

        .box.dragover {
            box-shadow: 0 0 5px blueviolet;
        }

        .block-item {
            text-align: center;
            background: blueviolet;
            margin: 5px;
        }
    </style>
</head>

<body>
    <div id="app">
        <!-- 放置目标 -->
        <div v-for="(v, k) in boxList"
            class="box"
            :key="k"
            @dragenter="boxDragEnter($event, k)"
            @dragover="boxDrapOver($event, k)"
            @dragleave="boxDragLeave($event, k)"
            @drop="boxDop($event, k)">

            <span>{{ k }}</span>

            <!-- 拖拽元素 -->
            <div v-for="block in boxList[k]"
                class="block-item"
                draggable="true"
                :key="block.name"
                @dragstart="blockDrapStart($event, block, k)">
                {{block.name}}
            </div>

        </div>
    </div>

    <script>
        const { createApp, reactive, onMounted } = Vue

        createApp({
            setup() {
                // 俩个box,分别包含一个水果
                const boxList = reactive({
                    box1: [{ name: '🍎' }],
                    box2: [{ name: '🍐' }]
                });

                // 该事件在放置目标上触发
                // 拖动的元素进入一个有效的放置目标时触发
                function boxDragEnter(event, box) {
                    console.log("【box】DragEnter:", box, event)
                    if (!event.target.classList.contains("dragover")) {
                        event.target.classList.add("dragover");
                    }
                }

                // 该事件在放置目标上触发
                // 拖动的元素离开一个有效的放置目标时触发
                function boxDragLeave(event, box) {
                    console.log("【box】DragLeave:", box, event)
                    if (event.target.classList.contains("dragover")) {
                        event.target.classList.remove("dragover");
                    }
                }

                // 该事件在放置目标上触发
                function boxDrapOver(event, box) {
                    console.log("【box】DrapOver:", box)
                    // event.preventDefault(),使目标容器能够接收 drop 事件。
                    event.preventDefault()
                }

                // 元素被放置到目标元素上时触发.
                // 为确保 drop 事件始终按预期触发,应当在处理 dragover 事件的代码部分始终包含 preventDefault() 调用。
                function boxDop(event, box) {
                    console.log("【box】Dop:", box, event)
                    // 读取数据
                    const { sourceBox, data } = JSON.parse(event.dataTransfer.getData('text/plain'));
                    if (sourceBox !== box) {
                        boxList[box].push(data);
                        const sourceIndex = boxList[sourceBox].findIndex(item => item.name === data.name);
                        boxList[sourceBox].splice(sourceIndex, 1)
                    }
                }

                // 开始拖动元素时调用,在拖拽元素上触发
                function blockDrapStart(event, data, sourceBox) {
                    console.log("【block】DrapStart:", data, sourceBox, event)
                    // 拖拽的内容存一下
                    event.dataTransfer.setData("text/plain", JSON.stringify({ sourceBox, data }))
                }

                return {
                    boxList,
                    boxDragEnter,
                    boxDragLeave,
                    boxDrapOver,
                    boxDop,
                    blockDrapStart
                }
            }
        }).mount('#app')

    </script>
</body>

</html>

遇到的问题

盒子上绑定的事件,其中的每个子元素也会触发,如上图动作 触发顺序为

css 复制代码
[水果]dragenter -> [水果]dragover -> [box]dragenter -> [水果]draglave

导致了子元素也会加上放置反馈的样式。

解决方式:增加元素判断

js 复制代码
// 该事件在放置目标上触发
// 拖动的元素进入一个有效的放置目标时触发
function boxDragEnter(event, box) {
    console.log("【box】DragEnter:", box, event.target)
    const isBox = event.target.classList.contains("box")
    if (isBox && !event.target.classList.contains("dragover")) {
        event.target.classList.add("dragover");
    }
}

// 该事件在放置目标上触发
// 拖动的元素离开一个有效的放置目标时触发
function boxDragLeave(event, box) {
    console.log("【box】DragLeave:", box, event.target)
    const isBox = event.target.classList.contains("box")
    if (isBox && event.target.classList.contains("dragover")) {
        event.target.classList.remove("dragover");
    }
}

类似问题:dragleave-of-parent-element-fires-when-dragging-over-children-elements

相关文档

npm package

相关推荐
前端 贾公子15 分钟前
uni-app工程化实战:基于vue-i18n和i18n-ally的国际化方案 (下)
前端
@zulnger23 分钟前
selenium 操作浏览器
前端·javascript·selenium
xiaofeichaichai34 分钟前
Symbol 与 Iterator / Generator
前端·javascript
维双云40 分钟前
小程序店铺装修模板怎么选?从首页布局、商品展示到下单路径这样看更实际
前端·小程序
YHL1 小时前
📖前端 HTTP 请求 & LLM 接口开发
前端·https
西部荒野子1 小时前
4.JS Bundle 执行流程
前端
假如让我当三天老蒯1 小时前
State和Props区别和左右(自学用)
前端·react.js
西部荒野子1 小时前
1. 建立源码地图
前端
西部荒野子1 小时前
3.RCTRootView 加载 Bundle 流程
前端
西部荒野子1 小时前
2.iOS 启动到 RCTRootView
前端