vue3+ts 原生 js drag drop 实现
一直以来没有涉及的一个领域就是 drag
drop
拖动操作,研究了下,实现了,所以写个教程。
官方说明页面及实例:https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
最终效果:
一、拖动的 html 结构
比如我要将右侧的元素拖动到左边,这里 html 有几个组成部分。
- 左边绿色的
.target 目标
- 右侧蓝色的
.source 源
- 右侧内部的
.source-item 待拖动元素
二、标记元素可被拖动
要想拖动某个元素,需要标记这个元素为可拖动
元素,如果不标记,在拖动
某个元素的时候,鼠标上并不会实时跟随被拖动的元素到鼠标指针下。
要想实现这样,只需要添加 draggable
属性即可,这个例子里,上面的多个 .source-item
为需要被拖动的元素。
<div class="source-item" draggable="true"></div>
三、最终要实现的结果
这个例子里,我定义了两个数组,一个源数组,一个目标数组。
const sourceArray = ref<Array<number>>([1,2,3,4,5,6])
const targetArray = ref<Array<number>>([])
拖动要实现的功能是,拖动时,将元素从 源数组
中移动到 目标数组
中。
四、拖动过程、对应的事件
这个过程中被拖动元素
和 释放区域
都需要绑定相应的事件,才能完成整个拖动过程。整个过程需要完成好多个事件的响应。
拖动事件是这样进行的:
- 拖动元素,(给事件添加绑定这个拖动行为的数据)
- 拖到目标区域上方,释放。(获取当前拖动事件的数据,进行下一步操作)
1. 被拖动元素需要响应的事件
被拖动元素需要响应的事件有:
ondragstart
拖动开始,在元素被拖动时触发。在这个事件里添加当前对应拖动行为的数据,比如被拖动元素的 Index 等需要的数据ondragend
拖动结束,在元素被释放时触发。这个是取消拖动的操作,本例不作操作。
本例中我在被拖动元素中添加了 data="数据"
的属性,这个在 ondragstart
的时候取用里面的数据,并设置到 .dataTransfer
中。
被拖动元素和最终释放到的元素之间是通过这个 event.dataTransfer
传递数据的。
<div class="source-item"
:data="item"
:ondragstart="dragstart"
:ondragend="dragend"
draggable="true"
v-for="item in sourceArray" :key="item">
<span>item-{{item}}</span>
</div>
/**
* Drag Item
*/
function dragstart(event: DragEvent){
let data = (event.target as HTMLElement).getAttribute('data') as string // 获取 html 里的 data 属性
event.dataTransfer!.dropEffect = 'copy' // copy | move | link 不知道干嘛的,好像也没什么效果
event.dataTransfer!.setData('text/plain', data) // 添加数据
console.log('item-drag-start: ')
}
function dragend(event: DragEvent){
console.log('item-drag-end')
}
这样,关于被拖动元素需要设置的东西就是这些了。
这里先了解一下这个 DragEvent
里都有什么:
上面在 dataTransfer
里添加的数据,在 console.log()
里是看不到的,但它是在里面的,后面会从这个事件里提取。先看一下是怎样的,这个后面会具体说:
// 设置数据
event.dataTransfer!.setData('text/plain', data)
// 提取数据
const originalData = Number(event.dataTransfer!.getData("text/plain"))
2. 释放区域的事件
拖动释放区域,也就是接收区域 div.target
需要实现的事件是:
ondragover
当拖动元素悬于释放区域时,这个事件是连续触发的,每动一个像素都会被触发。ondragenter
当拖动元素进入释放区域时,触发一次,不会连续触发ondragleave
当拖动元素离开释放区域时,触发一次,不会连续触发ondrop
当拖动元素在释放区域释放时,触发一次
<div
:class="['target', {'is-drag-entered': isDragEntered}]"
:ondragover="onTargetDragover"
:ondragenter="onTargetDragenter"
:ondragleave="onTargetDragLeave"
:ondrop="handleDrop"
>
<div class="source-item"
v-for="item in targetArray" :key="item">
<span>item-{{item}}</span>
</div>
</div>
const refTargetZone = ref()
onMounted(()=>{
nextTick(()=>{
refTargetZone.value.addEventListener('drop', handleDrop)
// ondrop 的事件需要以这样的方式添加,直接写到 html 中不生效,不知道为什么
})
})
/**
* Drag Target
*/
const isDragEntered = ref(false) // 实现拖动元素进入释放区域时,改变释放区域的样式,就是为了给个操作反馈
function onTargetDragover(event: DragEvent){
event.preventDefault() // 这里就特别注意,这行很关键
}
function onTargetDragenter(event: DragEvent){
console.log('drag-enter: ')
isDragEntered.value = true
}
function onTargetDragLeave(event: DragEvent){
console.log('drag-enter: ',)
isDragEntered.value = false
}
// 释放拖动的元素到目标区域时
function handleDrop(event: DragEvent){
isDragEntered.value = false
event.preventDefault() // 这里就特别注意,这行很关键
const originalData = Number(event.dataTransfer!.getData("text/plain")) // 取事件中的数据,这个数据是拖动开始时设置的。
// 数据变化
// 因为 vue 是数据驱动的,这里只需要操作 源、目标 数据,即可实现页面上界面的变化,
// 不需要像原生 dom 那样去操作 dom 来实现拖动的变化。
targetArray.value.push(originalData) // 目标数组添加对应值
sourceArray.value = sourceArray.value.filter(item => item !== originalData) // 源数组删除对应值
console.log('--- on drop:', originalData)
}
五、更进一步
上面的例子里传递的是普通字符串,它也可以传递文件什么的,看官方具体是如何操作的。
拖动到某个序列的某个位置,可能就需要对事件的坐标位置进行进一步判断了。
官方说明页面及实例:https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
六、完整代码
Drag.vue
<template>
<div class="drag-container">
<el-row :gutter="50">
<el-col :span="12">
<div
:class="['target', {'is-drag-entered': isDragEntered}]"
:ondragover="onTargetDragover"
:ondragenter="onTargetDragenter"
:ondragleave="onTargetDragLeave"
:ondrop="handleDrop"
>
<div class="source-item"
v-for="item in targetArray" :key="item">
<span>item-{{item}}</span>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="source">
<div class="source-item"
:data="item"
:ondragstart="dragstart"
:ondragend="dragend"
draggable="true"
v-for="item in sourceArray" :key="item">
<span>item-{{item}}</span>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
const isDragEntered = ref(false)
const sourceArray = ref<Array<number>>([1,2,3,4,5,6])
const targetArray = ref<Array<number>>([])
/**
* Drag Item
*/
function dragstart(event: DragEvent){
let data = (event.target as HTMLElement).getAttribute('data') as string
event.dataTransfer!.dropEffect = 'move'
event.dataTransfer!.setData('text/plain', data)
console.log('item-drag-start:' ,event)
}
function dragend(event: DragEvent){
console.log('item-drag-end')
}
function handleDrop(event: DragEvent){
isDragEntered.value = false
event.preventDefault()
const originalData = Number(event.dataTransfer!.getData("text/plain"))
// 数据变化
targetArray.value.push(originalData)
sourceArray.value = sourceArray.value.filter(item => item !== originalData)
console.log('--- on drop:', originalData)
}
/**
* Drag Target
*/
function onTargetDragover(event: DragEvent){
event.preventDefault()
// console.log('drag-over: ', event)
}
function onTargetDragenter(event: DragEvent){
console.log('drag-enter: ')
isDragEntered.value = true
}
function onTargetDragLeave(event: DragEvent){
console.log('drag-enter: ',)
isDragEntered.value = false
}
</script>
<style scoped lang="scss">
.drag-container{
padding: 30px;
}
.source, .target{
padding: 20px;
height: 400px;
display: flex;
flex-flow: row wrap;
-webkit-border-radius: 20px;
-moz-border-radius: 20px;
border-radius: 20px;
border: 2px solid #007AFF;
background-color: white;
&:hover{
border-style: dashed;
}
.source-item{
background-color: white;
display: flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
height: 60px;
width: 100px;
margin-bottom: 5px;
margin-right: 5px;
padding: 10px;
text-align: center;
border: 2px solid black;
&:hover{
background-color: #4CD964;
cursor: pointer;
user-select: none;
}
}
}
.source{
}
.target{
border-color: #4CD964;
&.is-drag-entered{
background-color: #4CD964;
}
}
</style>