1
拖拽需求,我们工作中常常使用一些库去实现,比如:vue-draggable 或者 react-beautiful-dnd 但是,某些情况下,需要我们自己去实现,于是就有了本文...

问题描述

鼠标事件的拖拽示例

假设我们在页面上有一个div要设置可拖拽(相当于视口进行拖拽),比如:

<div id="draggable"></div>

想要拖拽,首先必须让其脱离文档流,也就是position: absolute;,所以样式控制如下:

<style>
    #draggable {
        width: 100px;
        height: 100px;
        background-color: red;
        /* 脱离文档流 */
        position: absolute;
        top: 50px;
        left: 50px;
        cursor: move;
    }

    #draggable:hover {
        background-color: blue;
    }
</style>
  • 然后,我们在鼠标按下mousedown的时候,计算这个div距离视口的左侧和上方的距离
  • 在鼠标移动的时候,通过鼠标的距离和元素的距离计算出相对于视口的left和top的值
  • 把left和top的值,赋值给元素,既可实现鼠标按下、拖拽移动元素
  • 当然,鼠标松开mouseup的时候,别忘了移除原先绑定的事件
  • 效果图如下:

核心代码:

<body>
    <div id="draggable"></div>
    <script>
        const draggable = document.getElementById('draggable');

        // 监听鼠标按下事件
        draggable.addEventListener('mousedown', (e) => {
            // 计算鼠标位置与元素位置的偏移
            const offsetX = e.clientX - draggable.getBoundingClientRect().left;
            const offsetY = e.clientY - draggable.getBoundingClientRect().top;

            // 更改元素坐标位置
            function onMouseMove(e) {
                draggable.style.left = e.pageX - offsetX + 'px';
                draggable.style.top = e.pageY - offsetY + 'px';
            }

            // 移动元素
            document.addEventListener('mousemove', onMouseMove);

            // 松开鼠标时移除事件监听
            function onMouseUp() {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            }

            // 监听鼠标松开事件
            document.addEventListener('mouseup', onMouseUp);

            // 防止拖动时选中文本
            e.preventDefault();
        });
    </script>
</body>
有了上方代码的基础,接下来,我们来说一下本文的重点,Drag and Drop

Drag and Drop

  • Drag and Drop的核心就是Drag和Drop(这不废话嘛)
  • 我们这样理解,把A元素Drag后,然后在B元素上Drop
  • 在此操作期间,会触发多个事件,我们把这个事件分为两类
  • 拖拽A元素,触发的相关事件
  • 放置在B元素上,触发的相关事件

拖拽A元素,触发的相关事件

  • 我们可以类比上方的mouse相关的事件学习Drag事件
  • 开始拖拽事件ondragstart(触发一次),类似于mousedown
  • 拖拽中事件ondrag(一直触发),类似于mousemove
  • 拖拽结束事件ondragend(触发一次),类似于mouseup
  • 如下效果图:

代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <style>
        .draggable {
            width: 100px;
            height: 100px;
            background-color: red;
            margin-bottom: 20px;
            cursor: move;
        }
    </style>
</head>

<body>
    <div 
        class="draggable" 
        draggable="true" 
        ondragstart="console.log('ondragstart triggered')"
        ondrag="console.log('ondrag triggered')" 
        ondragend="console.log('ondragend triggered')"
    >
        Drag me
    </div>
</body>

</html>
注意,元素默认不能拖拽,我们需要手动开启元素可拖拽,即为:<div draggable="true" />

将拖拽的A元素,放置在可拖放的区域B元素触发的相关事件

  • 这里大致可以分为4个事件:拖拽进入、拖拽悬浮、拖拽放手、拖拽离开事件
  • 比如 ondragenter 就是拖拽进入事件(进入可拖放区域B元素中)
  • ondragover 就是拖拽悬浮事件(悬浮在可拖放区域B元素上方)
  • ondrop 就是拖拽放手事件(把拖拽过来的A元素,在可拖放区域B元素中放手)
  • ondragleave 就是拖拽离开事件(带着拖拽过来的A元素,离开了可拖放区域B元素)
注意,必须在ondragover事件内,阻止默认行为,才能够执行后续的拖拽放手操作

效果图:

代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <style>
        .draggable {
            width: 100px;
            height: 100px;
            background-color: red;
            margin-bottom: 20px;
            cursor: move;
        }

        .droppable {
            width: 200px;
            height: 200px;
            background-color: lightblue;
            margin-top: 20px;
            padding: 10px;
        }
    </style>
</head>

<body>

    <!-- 可拖动元素 -->
    <div class="draggable" draggable="true" ondragstart="handleDragStart(event)">
        Drag me
    </div>

    <!-- 拖放目标区域 -->
    <div class="droppable" ondragenter="handleDragEnter(event)" ondragover="handleDragOver(event)"
        ondrop="handleDrop(event)" ondragleave="handleDragLeave(event)">
        Drop here
    </div>

    <script>
        function handleDragStart(event) {
            console.log('ondragstart 拖拽开始');
            // 可以传参:设置拖拽数据(比如文本)
            event.dataTransfer.setData("text/plain", "我是拖拽的元素数据");
        }

        function handleDragEnter(event) {
            console.log('ondragenter 进入可拖放区域');
            event.target.style.background = 'lightgreen';
        }

        function handleDragOver(event) {
            // 必须阻止默认行为,才能触发 drop
            event.preventDefault();
            console.log('ondragover 悬浮在可拖放区域上 100毫秒触发一次');
        }

        function handleDrop(event) {
            event.preventDefault(); // 阻止打开被拖拽的 URL
            console.log('ondrop 放下拖过来的元素');
            // 接收参数数据
            const data = event.dataTransfer.getData("text");
            event.target.innerText = `Dropped: ${data}`;
            event.target.style.background = 'green';
        }

        // 这个事件在拖拽元素离开可放置区域时触发(用的少一些)
        function handleDragLeave(event) {
            console.log('ondragleave 离开了可拖放区域');
            event.target.style.background = 'pink';
        }
    </script>
</body>

</html>
  • 值得一提的是,我们可以在拖拽开始的时候,传递一些数据event.dataTransfer.setData("text/plain", "someData");
  • 在拖拽放手的时候,接收数据const data = event.dataTransfer.getData("text");
  • 不过这种数据传递,用的不太多,更多的还是我们自己维护数据的变化
  • 拖拽前和拖拽后,通过自己的逻辑操作去控制数据

上述一共有七个事件:

事件触发时机
ondragstart拖拽开始
ondrag拖拽中
ondragend拖拽结束
ondragenter进入可拖放区域
ondragover悬浮在可拖放区域
ondrop在可拖放区域放手
ondragleave离开可拖放区域

不过,我们开发中,常用的一般是这三个:

ondragstart 拖拽开始ondragover 悬浮在可拖放区域ondrop 在可拖放区域放手

  • 当我们拖拽的时候,需要修改一下拖拽的样式,也会用到e.dataTransfer.effectAllowed属性
  • effectAllowed 的值有不少,详情见:https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer...
  • 一般,我们在拖拽开始的时候,会将其设置为'move' 默认是 'copy',具体移动过去还是复制过去,看大家需求
  • move和copy效果图如下:

move图示:

copy图示:

至此,Drag and Drop拖放知识点,就学习的差不多了,接下来,我们通过两个案例,来演示一下具体的应用

案例一 普通的拖拽(拖放)

效果图

思路分析

  • 需求就是把上方的内容表情emoji,拖拽到下方盒子里面去
  • 为了方便,我们的监听拖拽的事件,可以直接绑定在最外层(包含上方的emoji和下方的盒子)
  • 然后,只给需要拖拽的emoji开启draggable="true"
  • 而后在页面初始化的时候,绑定拖拽监听事件
  • 当拖拽过去放手的时候,把拖拽的emoji给添加到下方
  • 同时,把上方的原来的emoji给删除即可

代码

复制粘贴即用

<template>
  <div class="tenBox" ref="dragBoxWrap">
    <h3>拖拽元素示例</h3>
    <div class="emojiList">
      <div class="emojiItem" v-for="item in emojiList" :key="item.name">
        <div class="emojiItem-content">
          <div class="emojiItem-content-name">{{ item.name }}</div>
          <!-- draggable="true"开启元素可以拖拽,必须要加 -->
          <div class="emojiItem-content-emoji" draggable="true">{{ item.emoji }}</div>
        </div>
      </div>
    </div>
    <!-- 四个摆放盒子 -->
    <div class="boxList">
      <div class="boxItem" v-for="item in boxList" :key="item.name">
        <!-- 名字绝对定位 -->
        <div class="boxItem-content-name">{{ item.name }}</div>
        <div class="boxItem-content" :data-emoji="item.name" :draggable="item.emoji ? true : false">{{
          item.emoji }}</div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";

const emojiList = ref([
  {
    name: '微笑',
    emoji: '😊'
  },
  {
    name: '惊讶',
    emoji: '😲'
  },
  {
    name: '生气',
    emoji: '🤬'
  },
  {
    name: '尴尬',
    emoji: '😢'
  },
])

const boxList = ref([
  {
    name: '1',
    emoji: ''
  },
  {
    name: '2',
    emoji: ''
  },
  {
    name: '3',
    emoji: ''
  },
  {
    name: '4',
    emoji: ''
  }
])

const dragBoxWrap = ref(null)
const curDragEmoji = ref(null)

onMounted(() => {
  initDrag()
})

const initDrag = () => {

  dragBoxWrap.value.ondragstart = (e) => {
    // 获取当前拖拽的元素
    curDragEmoji.value = e.target
    // 设置拖拽效果
    e.dataTransfer.effectAllowed = "move";
  }

  dragBoxWrap.value.ondragover = (e) => {
    // 阻止默认行为,允许drop
    if (e.target.className === 'boxItem-content') {
      e.preventDefault()
    }
    // console.log(e.target.dataset.emoji)

    // let whichBox = Number(e.target?.dataset?.emoji)
    // if (whichBox) {
    //     if (boxList.value[whichBox - 1].emoji) {
    //         e.preventDefault()
    //         e.target.style.backgroundColor = 'red'
    //         return
    //     }
    // }
  }

  dragBoxWrap.value.ondrop = (e) => {
    // 下方添加的时候,首先判断是否有值,有值不允许添加 
    let whichBox = Number(e.target.dataset.emoji)
    if (boxList.value[whichBox - 1].emoji) {
      alert('已被占用')
      return
    }
    boxList.value[whichBox - 1].emoji = curDragEmoji.value.textContent

    // 上方删除
    // 方式一(不太建议)
    // curDragEmoji.value.textContent = ''
    // 方式二(不太建议)
    // curDragEmoji.value.remove()
    // 方式三(不太建议)
    // curDragEmoji.value.style.display = 'none'

    // 方式四(建议)
    let curDragEmojiContent = curDragEmoji.value.textContent
    let curDragEmojiIndex = emojiList.value.findIndex(item => item.emoji === curDragEmojiContent)
    emojiList.value[curDragEmojiIndex].emoji = ''
  }
}

</script>

<style>
.tenBox {
  width: 100%;
  height: 90vh;
  position: relative;
}

.emojiList {
  display: flex;
  flex-wrap: wrap;
}

.emojiItem {
  width: 100px;
  height: 100px;
  border: 1px solid #ccc;
  border-radius: 10px;
  margin: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.emojiItem-content {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.emojiItem-content-name {
  font-size: 12px;
  color: #333;
}

.emojiItem-content-emoji {
  font-size: 36px;
  color: #333;
  cursor: move;
  user-select: none;
}

.boxList {
  display: flex;
  flex-wrap: wrap;
  margin-top: 54px;
}

.boxItem {
  width: 100px;
  height: 100px;
  border: 1px solid #ccc;
  border-radius: 10px;
  margin: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.boxItem-content {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 36px;
}

.boxItem-content-name {
  position: absolute;
  top: -60px;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

案例二 表格的拖拽(拖放)

效果图

思路分析

  • 上述效果图想要实现的思路和案例一的差不多
  • 不过需要注意的一点就是,一定要预先通过js把表格的单元格给设置为可拖拽、可放置

完整代码

<template>
  <div class="boxWrap">
    <div class="main">
      <div class="down">
        <div class="leftTable" ref="dragBoxWrap">
          <div class="t1">
            <el-table :data="tableData1" border style="width: 100%">
              <el-table-column prop="type" label="类型" width="74" align="center" />
            </el-table>
          </div>
          <div class="t2">
            <el-table ref="table2" height="400" :data="tableData2" border style="width: 100%">
              <el-table-column prop="way" label="对应策略管理办法" align="center" />
            </el-table>
          </div>
          <div class="t3">
            <el-table ref="table3" :data="tableData3" border style="width: 100%; margin-left: 24px;">
              <el-table-column prop="way" label="可选策略" align="center" />
            </el-table>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from "vue";

const dragBoxWrap = ref(null); // 总拖拽dom

const tableData1 = ref([
  { type: 'A类' },
  { type: 'A类' },
  { type: 'A类' },
  { type: 'A类' },
  { type: 'B类' },
  { type: 'B类' },
  { type: 'B类' },
  { type: 'C类' },
  { type: 'C类' },
])

const tableData2 = ref([
  { way: '', },
  { way: '', },
  { way: '', },
  { way: '', },
  { way: '', },
  { way: '', },
  { way: '', },
  { way: '', },
  { way: '', },
])

const tableData3 = ref([
  { way: '投入更多时间' },
  { way: '投入更多金钱' },
  { way: '想办法提升效率' },
  { way: '想办法扩张规模' },
  { way: '招聘更加优秀的人才' },
  { way: '团队优化' },
  { way: '寻求咨询公司的指导建议' },
  { way: '求神问佛' },
  { way: '躺平摆烂' },
])

const table2 = ref(null);
const table3 = ref(null);

onMounted(() => {
  setTimeout(() => {
    initDarg();
  }, 0);
})

const curTd = ref(null); // 当前拖拽的td   

const initDarg = () => {
  // 允许t3的单元格被拖拽
  const t3Rows = table3.value.$el.querySelectorAll('tbody tr');
  t3Rows.forEach((tr, index) => {
    const td = tr.querySelector('td');
    if (td) {
      td.draggable = true;
      td.dataset.index = index;
      td.style.cursor = 'move';
    }
  });

  // 允许t2的单元格被放置上去
  const t2Rows = table2.value.$el.querySelectorAll('tbody tr');
  t2Rows.forEach((tr, index) => {
    const td = tr.querySelector('td');
    if (td) {
      td.classList.add('flag');
      td.dataset.index = index;
    }
  });
  // 拖拽开始事件
  dragBoxWrap.value.ondragstart = (e) => {
    if (!e.target.innerText) {
      return;
    }
    // 改变样式
    e.dataTransfer.effectAllowed = "move";
    // 存一份
    curTd.value = e.target;
  }
  // 拖拽悬浮事件
  dragBoxWrap.value.ondragover = (e) => {
    // 允许元素被拖拽放上去
    if (
      e.target.classList.contains("flag")
    ) {
      e.preventDefault();
    }
  };
  // 拖拽放下事件
  dragBoxWrap.value.ondrop = (e) => {
    if (!curTd.value.innerText) {
      return;
    }
    // t2赋值
    tableData2.value[e.target.dataset.index].way = curTd.value.innerText;
    // t3清空值
    tableData3.value[curTd.value.dataset.index].way = null;
  }
}   
</script>

<style scoped lang="less">
.boxWrap {
  width: 100%;
  height: 100%;
  position: relative;
  box-sizing: border-box;

  .star {
    position: absolute;
    writing-mode: vertical-lr;
    font-weight: 700;
    color: #2c5896;
    letter-spacing: 12px;
    font-size: 20px;
    left: 85px;
    top: 0;
    user-select: none;
  }

  .main {
    width: 850px;
    height: 558px;
    background-color: #fff;
    margin-left: 120px;
    text-align: center;
    padding: 0 15px;
    padding-top: 12px;

    .title {
      height: 45px;
      line-height: 45px;
      text-align: center;
      color: #fff;
      font-size: 24px;
      background-color: #589fd7;
      margin-bottom: 6px;
    }

    .up {
      text-align: left;
      font-size: 14px;

    }

    .down {
      display: flex;
      align-items: center;
      margin-top: 8px;
      width: 100%;

      .leftTable {
        width: 100%;
        display: flex;

        .t1 {
          width: 9%;
          position: relative;
          right: -6px;
          z-index: 111;
        }

        .t2 {
          width: 40%;
        }

        .t3 {
          width: 40%;
          margin-left: 36px;
        }
      }

      :deep(.el-table) {
        font-size: 13px;
        font-weight: 600;

        .el-table__header th {
          background-color: #3874c8 !important;
          color: #fff;
          font-size: 13px;
        }

        .el-table__header th:last-child {
          background-color: #3874c8 !important;
          color: #fff;
          font-size: 13px;
        }

        thead {
          font-size: 16px;
        }

        tr {
          height: 40px;
        }
      }
    }
  }
}
</style>
参考文章:https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_an...

欢迎大家多多star,您的支持,是我创作的动力呦 😊😊😊


水冗水孚
1.1k 声望597 粉丝

每一个不曾起舞的日子,都是对生命的辜负