掌握原生拖拽,類似拖拽需求,一網(wǎng)打盡

初識(shí)拖拽
首先我們必須知道了解幾個(gè)拖拽API[1]

dragstart 當(dāng)一個(gè)元素被拖拽時(shí)觸發(fā)【拖拽元素上綁定】

dragend 當(dāng)一個(gè)被拖拽元素結(jié)束拖拽時(shí)觸發(fā)【拖拽元素上綁定】

dragover 被拖拽元素拖入目標(biāo)區(qū)域后就會(huì)觸發(fā)該事件【目標(biāo)區(qū)域綁定事件】

drop 當(dāng)被拖拽元素拖入目標(biāo)區(qū)域結(jié)束是會(huì)觸發(fā)該事件【在目標(biāo)區(qū)域綁定】

開(kāi)始一個(gè)項(xiàng)目
首先先搭建了一個(gè)基本的頁(yè)面,我們先看下左邊區(qū)域

<template>
  <div class="drap">
    <div class="left">
      <h3>原始區(qū)域{{ $route.fullPath }}</h3>
      <div class="content">
        <fieldset v-for="(item, index) in listTree" :key="index">
          <legend>{{ item.title }}</legend>
          <span
            v-for="(citem, cindex) in item.data"
            :key="cindex"
            draggable="true"
            @dragstart="handleDragStart(citem)"
            @dragend="handleDragEnd"
            >{{ citem.name }}</span
          >
        </fieldset>
      </div>
    </div>
    <div class="right">
      <h3>目標(biāo)區(qū)域</h3>
      ...
    </div>
  </div>
</template>
對(duì)應(yīng)的頁(yè)面如下


大概就是原始區(qū)域的對(duì)應(yīng)名單,只能拖到對(duì)應(yīng)的名單里面去,比如左側(cè)冠軍名單只能拖入右側(cè)的冠軍名單,左側(cè)的亞軍名單只能拖入右側(cè)的亞軍名單中去,所有冠軍與亞軍名單都可以拖入中獎(jiǎng)名單中去
并且我們看到在左側(cè)區(qū)域被拖拽的元素上綁定了dragstart,dragend事件,并且我們需要在被拖走元素上指定draggable: true(這樣設(shè)置后,該元素就默認(rèn)可以拖拽了)

 <div class="left">
      <h3>原始區(qū)域{{ $route.fullPath }}</h3>
      <div class="content">
        <fieldset v-for="(item, index) in listTree" :key="index">
          <legend>{{ item.title }}</legend>
          <span
            v-for="(citem, cindex) in item.data"
            :key="cindex"
            draggable="true"
            @dragstart="handleDragStart(citem)"
            @dragend="handleDragEnd"
            >{{ citem.name }}</span
          >
        </fieldset>
      </div>
</div>
mock數(shù)據(jù)
左側(cè)的名單數(shù)據(jù)我是用mockjs隨機(jī)模擬生成的

import Mock from 'mockjs';
const randomData = (len, type) => {
  const result = new Array(len).fill(1);
  return result.map(() => {
    return {
      id: Mock.mock('@id'),
      name: Mock.mock('@cname'),
      type
    };
  });
};
const sourceData1 = Mock.mock({
  list: randomData(10, 'a')
});
const sourceData2 = Mock.mock({
  list: randomData(10, 'b')
});
在listTree中

export default {
  name: 'draw',
  data() {
    return {
      listTree: [
        {
          title: '冠軍名單',
          data: sourceData1.list
        },
        {
          title: '亞軍名單',
          data: sourceData2.list
        }
      ],
      targetSourceData2: [], // 冠軍
      targetSourceData2: [], // 亞軍
      targetSourceData3: [], // 獲獎(jiǎng)名單
      current: null, // 當(dāng)前元素
      currentActive: {
        a: false,
        b: false,
        c: false
     }
  }
}
在右側(cè)區(qū)域中,我們可以發(fā)現(xiàn)

 <div class="right">
      <div class="title-bar">
        <h3>目標(biāo)區(qū)域</h3>
        <p>
          <a href="javascript:void(0)"
            @click="handleClear">清除</a>
        </p>
      </div>
      <fieldset
        :class="[
          'content content-1',
          currentActive.a ? 'content-1-border' : ''
        ]"
        data-type="a"
        @dragover="handleDragOver"
        @drop.prevent="handleDrag"
        @dragenter.prevent="handleDragEnter"
        @dragleave.prevent="handleLeave"
      >
        <legend>冠軍名單</legend>
        <span
          draggable="true"
          v-for="(item, index) in targetSourceData1"
          :key="index"
          >{{ item.name }}</span
        >
      </fieldset>
      <fieldset
        :class="[
          'content content-2',
          currentActive.b ? 'content-2-border' : ''
        ]"
        data-type="b"
        @dragover.prevent="() => {}"
        @drop.prevent="handleDrag"
        @dragenter.prevent="handleDragEnter"
        @dragleave.prevent="handleLeave"
      >
        <legend>亞軍名單</legend>
        <span
          draggable="true"
          v-for="(item, index) in targetSourceData2"
          :key="index"
          >{{ item.name }}</span
        >
      </fieldset>
      <fieldset
        :class="[
          'content content-3',
          currentActive.c ? 'content-3-border' : ''
        ]"
        data-type="c"
        @dragover.prevent="() => {}"
        @drop.prevent="handleDrag"
        @dragenter.prevent="handleDragEnter"
        @dragleave.prevent="handleLeave"
      >
        <legend>中獎(jiǎng)名單</legend>
        <span
          v-for="(item, index) in targetSourceData3"
          :key="index"
          draggable="true"
          :data-index="index"
          >{{ item.name }}</span
        >
      </fieldset>
  </div>
當(dāng)我們拖拽一個(gè)元素時(shí),那么此時(shí)就要確定這個(gè)被拖拽的元素要被拖入哪個(gè)目標(biāo)區(qū)域中

data-type

因此你可以看到我們用data-type標(biāo)識(shí)了被拖入元素與目標(biāo)元素的對(duì)應(yīng)關(guān)系,正因?yàn)橛羞@個(gè)標(biāo)識(shí)區(qū)域,所以才可以控制對(duì)應(yīng)目標(biāo)元素的拖入

@dragover

這個(gè)是當(dāng)拖拽元素拖入目標(biāo)元素中時(shí),就會(huì)一直觸發(fā),當(dāng)離開(kāi)時(shí)就會(huì)停止觸發(fā),默認(rèn)情況拖入目標(biāo)區(qū)域時(shí),被拖拽元素會(huì)一個(gè)回彈效果,這里需要阻止默認(rèn)事件

有兩種方式

1、利用vue的事件修飾符prevent

 @dragover.prevent="() => {}"
2、原生處理

@dragover="handleDragOver"
handleDragOver

 handleDragOver (e) {
  console.log('drag0ver...');
  // 阻止回彈
  e.preventDefault();
 },
確認(rèn)了目標(biāo)區(qū)域拖拽的事件后,我們看下具體對(duì)應(yīng)綁定的方法

被拖拽元素上綁定的事件
export default {
  name: 'draw',
  data () {
    return {
      ...
      current: null, // 當(dāng)前拖拽元素的數(shù)據(jù)
      currentActive: {
        a: false,
        b: false,
        c: false
      }
    };
  },
  methods: {
    handleDragStart (item) {
      console.log('dragstart...');
      // 將當(dāng)前拖拽的元素?cái)?shù)據(jù)賦值給current
      this.current = item;
    },
    handleDragEnd () {
      console.log('dragEnd...');
      this.current = null;
    },
    handleDragOver (e) {
      console.log('drag0ver...');
      // 阻止回彈
      e.preventDefault();
    },
  }
  ...
};






目標(biāo)元素上的事件
export default {
  name: 'draw',
  data () {
    return {
      listTree: [
        {
          title: '冠軍名單',
          data: sourceData1.list
        },
        {
          title: '亞軍名單',
          data: sourceData2.list
        }
      ],
      targetSourceData1: [], // 冠軍
      targetSourceData2: [], // 亞軍
      targetSourceData3: [], // 獲獎(jiǎng)名單
      current: null, // 當(dāng)前元素
      currentActive: {
        a: false,
        b: false,
        c: false
      }
    };
  },
  methods: {
    ...
    messageWarn () {
      this.$message.warning(`${this.current.name}已經(jīng)存在,不允許重復(fù)添加啦`);
    },
    handleDrag (e) {
      const type = e.target.dataset.type;
      console.log(type);
      console.log(this.current);
      console.log('釋放了');
      if (this.current.type === type) {
        if (type === 'a') {
          if (
            this.targetSourceData1.findIndex(v => v.id === this.current.id) === -1
          ) {
            // 如果已經(jīng)添加,就不允許重復(fù)添加
            this.targetSourceData1.push(this.current);
          } else {
            this.targetSourceData1.length > 0 && this.messageWarn();
          }
        }
        // 如果已經(jīng)添加,就不允許重復(fù)添加
        if (type === 'b') {
          if (
            this.targetSourceData2.findIndex(v => v.id === this.current.id) === -1
          ) {
            this.targetSourceData2.push(this.current);
          } else {
            this.targetSourceData2.length > 0 && this.messageWarn();
          }
        }
      } else {
        if (
          this.targetSourceData3.findIndex(v => v.id === this.current.id) === -1 &&
          type === 'c'
        ) {
          this.targetSourceData3.push(this.current);
        } else {
          if (this.current.type === 'b') {
            this.$message.warning(`拖入?yún)^(qū)域有誤,請(qǐng)拖入亞軍區(qū)域中`);
          } else if (this.current.type === 'a') {
            this.$message.warning(`拖入?yún)^(qū)域有誤,請(qǐng)拖入冠軍區(qū)域中`);
          }
          this.targetSourceData3.length > 0 && this.messageWarn();
        }
      }
      this.currentActive[e.target.dataset.type] = false;
    },
    handleLeave (e) {
      console.log('離開(kāi)了');
      this.currentActive[e.target.dataset.type] = false;
    },
    handleDragEnter (e) {
      console.log('觸發(fā)了');
      this.currentActive[e.target.dataset.type] = true;
    },
      handleClear () {
      ['targetSourceData3', 'targetSourceData2', 'targetSourceData1'].forEach(
        key => {
          this[key] = [];
        }
      );
    }
  }
};
ok,基本的功能已經(jīng)實(shí)現(xiàn)了



另外還有幾篇關(guān)于拖拽的文章可供參考學(xué)習(xí)html5-draganddrop[2],html5-drag-drop[3]

總結(jié)
拖拽核心API,dragstart、dragend,被拖拽元素draggable: true即可拖拽

目標(biāo)區(qū)域dragover要設(shè)置阻止默認(rèn)行為防止拖拽元素回彈

目標(biāo)區(qū)域drop事件,拖拽結(jié)束觸發(fā)

dragenter被拖拽元素拖入目標(biāo)元素上觸發(fā)

dragleave被拖拽元素離開(kāi)目標(biāo)元素上觸發(fā)

本文示例code example[4]

參考資料
[1]
API: https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/drag_event

[2]
html5-draganddrop: https://www.runoob.com/html/html5-draganddrop.html

[3]
html5-drag-drop: https://www.zhangxinxu.com/wordpress/2011/02/html5-drag-drop-拖拽與拖放簡(jiǎn)介

[4]
code example: https://github.com/maicFir/lessonNote/tree/master/vue/04-select下拉框虛擬列表&拖拽/elem-select





作者:Maic


歡迎關(guān)注微信公眾號(hào) :web技術(shù)學(xué)苑