浏览器拖放操作 API
HTML 拖放(Drag and Drop)接口使应用程序能够在浏览器中使用拖放功能。例如,用户可使用鼠标选择可拖拽(draggable)元素,将元素拖拽到可放置(droppable)元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。
具体可以参考 MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API
Demo 演示:
原始列
元素1
元素2
元素3
元素4
元素5
元素6
元素7
目标列1
复制元素1
复制元素2
目标列2
源码:
vue
<template>
<div
class="container"
@dragstart="handleSourceDragStart"
@dragover.prevent=""
@dragenter="handleDragEnter"
@drop="handleDrop"
ref="dragContainer"
>
<div class="source-list">
<div class="title">原始列</div>
<div class="list">
<div
class="item"
v-for="(item, index) in sourceList"
draggable="true"
data-effect="copy"
:data-index="index"
>
<div class="item-node">
{{ item.name }}
</div>
</div>
</div>
</div>
<div class="target-list" data-model="first">
<div class="title">目标列1</div>
<div class="list" data-drop="copy" data-drop-type="list">
<div
class="item"
v-for="(item, index) in targetList.first"
data-drop="copy"
data-drop-type="item"
:data-index="index"
>
<div class="item-node">
{{ item.name }}
</div>
</div>
</div>
</div>
<div class="target-list" data-model="second">
<div class="title">目标列2</div>
<div class="list" data-drop="copy" data-drop-type="list">
<div
class="item"
v-for="(item, index) in targetList.second"
data-drop="copy"
data-drop-type="item"
:data-index="index"
>
<div class="item-node">
{{ item.name }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
// 放置提示样式
const LIST_DROPPABLE = 'droppable'
// 拖拽容器node
const dragContainer = ref()
// 当前正在拖拽的元素
let dragTarget
// 原始列表
const sourceList = ref(
Array(7)
.fill('')
.map((_, idx) => ({
name: `元素${idx + 1}`
}))
)
// 接受数据的列表
const targetList = reactive({
first: [
{
name: '复制元素1'
},
{
name: '复制元素2'
}
],
second: []
})
/**
* 清除所有放置样式
*/
const clearDropStyle = () => {
dragContainer.value.querySelectorAll(`.${LIST_DROPPABLE}`).forEach(node => {
node.classList.remove(LIST_DROPPABLE)
})
}
/**
* 处理开始拖拽的逻辑
* @param {*} evt
*/
const handleSourceDragStart = evt => {
dragTarget = evt.target
const { effect, index } = dragTarget.dataset
console.log('effect', effect)
evt.dataTransfer.effectAllowed = effect
evt.dataTransfer.setData(
'dataSource',
JSON.stringify(sourceList.value[Number(index)])
)
}
/**
* 处理进入拖放区域的逻辑
* @param {*} evt
*/
const handleDragEnter = evt => {
clearDropStyle()
let dropNode = evt.target.closest('[data-drop]')
if (
dropNode &&
dropNode.dataset.drop &&
dropNode.dataset.drop === evt.dataTransfer.effectAllowed
) {
dropNode.classList.add(LIST_DROPPABLE)
}
}
const getTransferDataSource = evt => {
try {
let res = JSON.parse(evt.dataTransfer.getData('dataSource'))
return res
} catch {
return undefined
}
}
/**
* 处理放置逻辑
* @param {*} evt
*/
const handleDrop = evt => {
clearDropStyle()
let dataSource = getTransferDataSource(evt)
let dropNode = evt.target.closest('[data-drop]')
if (!evt.dataTransfer || !dataSource) return
if (!dataSource || !dropNode) return
const wrapperNode = dropNode.closest(`.target-list[data-model]`)
const modelName = wrapperNode.dataset.model
const { dropType = 'list', index, drop } = dropNode.dataset
console.log('drop', drop, dragTarget.dataset)
if (drop !== dragTarget.dataset.effect) return
handleListAdd(modelName, dataSource, dropType, Number(index))
}
const handleListAdd = (modelName, dataSource, dropType, index) => {
dataSource.name = '复制' + dataSource.name
if (!(modelName in targetList)) return
if (dropType === 'item') {
targetList[modelName].splice(index + 1, 0, dataSource)
} else {
targetList[modelName].push(dataSource)
}
}
</script>
<style lang="less" scoped>
.container {
display: flex;
.source-list,
.target-list {
background-color: #f2f2f2;
width: 200px;
margin-right: 16px;
border: solid 1px #ddd;
.title {
height: 56px;
line-height: 56px;
padding: 0 16px;
border-bottom: solid 1px #ddd;
background-color: #fff;
}
.list {
padding: 16px;
min-height: 500px;
&.droppable {
background-color: aqua;
}
}
.item {
&::after {
content: '';
display: none;
height: 10px;
background-color: aqua;
}
&.droppable {
&:after {
display: block;
}
}
}
.item-node {
padding: 0 16px;
height: 56px;
line-height: 56px;
background-color: #fff;
border: solid 1px #ddd;
}
}
}
</style>
<template>
<div
class="container"
@dragstart="handleSourceDragStart"
@dragover.prevent=""
@dragenter="handleDragEnter"
@drop="handleDrop"
ref="dragContainer"
>
<div class="source-list">
<div class="title">原始列</div>
<div class="list">
<div
class="item"
v-for="(item, index) in sourceList"
draggable="true"
data-effect="copy"
:data-index="index"
>
<div class="item-node">
{{ item.name }}
</div>
</div>
</div>
</div>
<div class="target-list" data-model="first">
<div class="title">目标列1</div>
<div class="list" data-drop="copy" data-drop-type="list">
<div
class="item"
v-for="(item, index) in targetList.first"
data-drop="copy"
data-drop-type="item"
:data-index="index"
>
<div class="item-node">
{{ item.name }}
</div>
</div>
</div>
</div>
<div class="target-list" data-model="second">
<div class="title">目标列2</div>
<div class="list" data-drop="copy" data-drop-type="list">
<div
class="item"
v-for="(item, index) in targetList.second"
data-drop="copy"
data-drop-type="item"
:data-index="index"
>
<div class="item-node">
{{ item.name }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
// 放置提示样式
const LIST_DROPPABLE = 'droppable'
// 拖拽容器node
const dragContainer = ref()
// 当前正在拖拽的元素
let dragTarget
// 原始列表
const sourceList = ref(
Array(7)
.fill('')
.map((_, idx) => ({
name: `元素${idx + 1}`
}))
)
// 接受数据的列表
const targetList = reactive({
first: [
{
name: '复制元素1'
},
{
name: '复制元素2'
}
],
second: []
})
/**
* 清除所有放置样式
*/
const clearDropStyle = () => {
dragContainer.value.querySelectorAll(`.${LIST_DROPPABLE}`).forEach(node => {
node.classList.remove(LIST_DROPPABLE)
})
}
/**
* 处理开始拖拽的逻辑
* @param {*} evt
*/
const handleSourceDragStart = evt => {
dragTarget = evt.target
const { effect, index } = dragTarget.dataset
console.log('effect', effect)
evt.dataTransfer.effectAllowed = effect
evt.dataTransfer.setData(
'dataSource',
JSON.stringify(sourceList.value[Number(index)])
)
}
/**
* 处理进入拖放区域的逻辑
* @param {*} evt
*/
const handleDragEnter = evt => {
clearDropStyle()
let dropNode = evt.target.closest('[data-drop]')
if (
dropNode &&
dropNode.dataset.drop &&
dropNode.dataset.drop === evt.dataTransfer.effectAllowed
) {
dropNode.classList.add(LIST_DROPPABLE)
}
}
const getTransferDataSource = evt => {
try {
let res = JSON.parse(evt.dataTransfer.getData('dataSource'))
return res
} catch {
return undefined
}
}
/**
* 处理放置逻辑
* @param {*} evt
*/
const handleDrop = evt => {
clearDropStyle()
let dataSource = getTransferDataSource(evt)
let dropNode = evt.target.closest('[data-drop]')
if (!evt.dataTransfer || !dataSource) return
if (!dataSource || !dropNode) return
const wrapperNode = dropNode.closest(`.target-list[data-model]`)
const modelName = wrapperNode.dataset.model
const { dropType = 'list', index, drop } = dropNode.dataset
console.log('drop', drop, dragTarget.dataset)
if (drop !== dragTarget.dataset.effect) return
handleListAdd(modelName, dataSource, dropType, Number(index))
}
const handleListAdd = (modelName, dataSource, dropType, index) => {
dataSource.name = '复制' + dataSource.name
if (!(modelName in targetList)) return
if (dropType === 'item') {
targetList[modelName].splice(index + 1, 0, dataSource)
} else {
targetList[modelName].push(dataSource)
}
}
</script>
<style lang="less" scoped>
.container {
display: flex;
.source-list,
.target-list {
background-color: #f2f2f2;
width: 200px;
margin-right: 16px;
border: solid 1px #ddd;
.title {
height: 56px;
line-height: 56px;
padding: 0 16px;
border-bottom: solid 1px #ddd;
background-color: #fff;
}
.list {
padding: 16px;
min-height: 500px;
&.droppable {
background-color: aqua;
}
}
.item {
&::after {
content: '';
display: none;
height: 10px;
background-color: aqua;
}
&.droppable {
&:after {
display: block;
}
}
}
.item-node {
padding: 0 16px;
height: 56px;
line-height: 56px;
background-color: #fff;
border: solid 1px #ddd;
}
}
}
</style>