拖拽容器
DragContainer 元素拖拽容器的使用
查看代码
vue
<template>
<a-form layout="vertical">
<a-form-item label="基本用法" help="通过index交换数据;第一个固定,不参与拖拽">
<DragContainer @draged="dragBase">
<a-tag :class="{ 'drag-item': index != 0 }" :color="item" v-for="(item, index) in baseData">
{{ item }}
</a-tag>
</DragContainer>
</a-form-item>
<a-form-item label="拖拽插入" help="带动画交互">
<DragContainer darg-item-selector=".ant-tag" @draged="dragInsert" ref="dargRef">
<a-tag :color="item" v-for="item in insertData">{{ item }}</a-tag>
</DragContainer>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { Form as AForm, FormItem as AFormItem, Tag as ATag } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import DragContainer from './DragContainer.vue'
import Flip from './Flip'
const baseData = ref(['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple'])
function dragBase(sourceIndex, targetIndex) {
const temp = baseData.value[targetIndex + 1]
baseData.value[targetIndex + 1] = baseData.value[sourceIndex + 1]
baseData.value[sourceIndex + 1] = temp
}
const insertData = ref(['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple'])
const dargRef = ref<InstanceType<typeof DragContainer>>()
const flip = ref<Flip>()
onMounted(() => {
flip.value = new Flip(dargRef.value.dragInfo.dragDoms)
})
function dragInsert(sourceIndex, targetIndex) {
const doms = dargRef.value.dragInfo.dragDoms
const target = doms.at(targetIndex)
const source = doms.at(sourceIndex)
const parent = target.parentNode
if (targetIndex > sourceIndex) {
if (targetIndex === doms.length - 1) {
const t = document.createElement('span')
parent?.appendChild(t)
parent?.insertBefore(source, t)
parent?.removeChild(t)
} else {
parent?.insertBefore(source, target.nextSibling)
}
} else {
parent?.insertBefore(source, target)
}
dargRef.value.getItems()
flip.value?.play()
}
</script>
<style scoped>
.ant-tag {
transition: none;
margin-bottom: 8px;
}
</style>vue
<template>
<div
@dragover="dragOver"
@dragstart="dragStart"
@drop="drop"
@touchend="touchEnd"
@touchstart="touchStart"
class="drag-container"
ref="dragContainerRef"
>
<slot />
</div>
</template>
<script lang="ts" setup>
import { useMutationObserver } from '@vueuse/core'
import { onMounted, ref, shallowReactive } from 'vue'
const props = defineProps<{
/** 拖拽子元素的选择器,默认.drag-item */
dargItemSelector?: string
}>()
const emit = defineEmits<{
draged: [sourceIndex: number, targetIndex: number]
}>()
const dragContainerRef = ref()
const dragInfo = shallowReactive({
dragDoms: [] as HTMLElement[],
dragingDom: {} as HTMLElement,
touchingDom: {} as HTMLElement
})
useMutationObserver(
dragContainerRef,
() => {
getItems()
},
{ childList: true, subtree: true }
)
onMounted(() => {
getItems()
})
const getItems = () => {
const dargItemSelector = props.dargItemSelector || '.drag-item'
const eles = dragContainerRef.value.querySelectorAll(dargItemSelector) as NodeListOf<HTMLElement>
eles.forEach(item => {
item.setAttribute('draggable', 'true')
})
dragInfo.dragDoms = Array.from(eles)
}
const moveItem = (source: HTMLElement, target: HTMLElement) => {
const doms = dragInfo.dragDoms
const sourceIndex = doms.findIndex(dom => dom === source)
const targetIndex = doms.findIndex(d => d === target)
if (sourceIndex !== -1 && targetIndex !== -1) {
emit('draged', sourceIndex, targetIndex)
}
}
const touchStart = (e: TouchEvent) => {
if (e.touches.length === 1 && e.targetTouches.length === 1) {
// 如果屏幕上只有一个触点且要移动的节点上有一个触点,记录要移动的dom
dragInfo.touchingDom = e.target as HTMLElement
}
}
const touchEnd = (e: TouchEvent) => {
// 触摸结束的时候消失的只有一个触点且是记录移动的dom,则改变其距离
if (e.changedTouches.length === 1 && e.changedTouches.item(0)?.target === dragInfo.touchingDom) {
const doms = dragInfo.dragDoms
const { clientX = 0, clientY = 0 } = e.changedTouches.item(0) || {}
const target = doms.find(dom => {
const { x, y, width, height } = dom.getBoundingClientRect()
return clientX > x && clientX < x + width && clientY > y && clientY < y + height
})
target && moveItem(dragInfo.touchingDom, target)
}
}
const dragStart = (e: DragEvent) => {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'move'
}
dragInfo.dragingDom = e.target as HTMLElement
}
const drop = (e: DragEvent) => {
const el = e.target as HTMLElement
const isSelf = dragInfo.dragDoms.find(dom => dom === el)
if (isSelf) {
moveItem(dragInfo.dragingDom, el)
} else {
const parent = dragInfo.dragDoms.find(dom => dom.contains(el))
parent && moveItem(dragInfo.dragingDom, parent)
}
}
const dragOver = (e: DragEvent) => {
const el = e.target as HTMLElement
if (el === dragInfo.dragingDom || dragInfo.dragingDom.contains(el)) return
const self = dragInfo.dragDoms.find(dom => dom === el || dom.contains(el))
if (e.dataTransfer && self) {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
}
}
defineExpose({ dragInfo, getItems })
</script>ts
/**
* Flip 动画解决方案(渡一袁老师)
* F: First 记录起始位置
* L: Last 记录结束位置
* I: Invert 反转元素到起始位置
* P: Play 播放动画回到结束位置
*/
export default class Flip {
private position = new WeakMap()
constructor(
private doms:
| (HTMLDivElement | HTMLSpanElement)[]
| NodeListOf<HTMLDivElement | HTMLSpanElement>
) {
this.init()
}
private init() {
this.doms.forEach(dom => this.position.set(dom, { x: dom.offsetLeft, y: dom.offsetTop }))
}
play() {
this.doms.forEach(dom => {
// 起始位置
const { x: x1, y: y1 } = this.position.get(dom)
// 最终位置
const x2 = dom.offsetLeft
const y2 = dom.offsetTop
let x = 0
let y = 0
if (x2 > x1) {
x = -(x2 - x1)
} else if (x2 < x1) {
x = x1 - x2
}
if (y2 > y1) {
y = -(y2 - y1)
} else if (y2 < y1) {
y = y1 - y2
}
if (x !== 0 || y !== 0) {
// 清除过渡
dom.style.removeProperty('transition')
// 先移动到初始位置
dom.style.setProperty('transform', `translate(${x}px,${y}px)`)
// 利用定时器任务实现以上代码渲染后的重新渲染
setTimeout(() => {
dom.style.setProperty('transition', 'all 0.5s')
// 删除过渡效果
dom.style.removeProperty('transform')
})
}
})
this.init()
}
}