Skip to content

拖拽容器

DragContainer 元素拖拽容器的使用

下载封装资源

pinkredorangegreencyanbluepurple
pinkredorangegreencyanbluepurple
查看代码
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()
  }
}