ant design vue框架下实现可拖动的a-popover

793 阅读3分钟

用 vueuse 的 useDraggable 不能实现吗?

可以,但需要做很大的改动。实现会变的很复杂。

vueuse 的 useDraggable 适合将原本不是绝对定位或不是fixed定位的元素,实现可拖动

其他说明

  • 所有基于ui框架(如:element-ui,element-plus,ant design vue)的弹层类组件实现可拖动都可采用该原理实现
  • 触摸屏也可以实现,只要再加上touchstart, touchmove, touchend事件监听即可,实现原理相同

实现原理

  1. 鼠标按键按下时: 获取鼠标按下时点的x,y坐标(这里记作: x3, y3)
  2. 鼠标按键按下时: 获取鼠标按下时a-popover对应的dom元素的top与left值(这里记作:x1, y1)
  3. 鼠标按键按下并移动时: 获取鼠标拖动时的点的x,y坐标(这里记作:x4, y4)
  4. 鼠标按键按下并移动时: 计算鼠标拖动时的点的x4,y4坐标与鼠标按下时点的x3,y3坐标之间的偏移量(这里记作:offsetX, offsetY)
  5. 鼠标按键按下并移动时: 更新a-popover对应的dom元素的top与left值:x2=x1+offsetX, y2=y1+offsetY
  6. 鼠标按键松开时: 清除"鼠标按键按下","鼠标按键按下并移动","鼠标按键松开"事件

image.png

实现细节点

蓝色区域是做什么的?

蓝色区域可监听鼠标按下事件,只有在该区域按下鼠标,才能对弹层进行拖动

如何获取的x3,y3值

在蓝色区域,监听mousedown事件,从事件对象的e.clientX, e.clientY就是x3,y3

如何获取x1,y1值

该值表示弹层dom的左顶点位置,取当前弹层dom的style对象的left值和top值,此时,可能存在两种情况

  • left,top值为百分比值

如果是百分比值需要转换为px值

styleLeft = +document.body.clientWidth * (+style.left.replace(/\%/g, '') / 100);
styleTop = +document.body.clientHeight * (+style.top.replace(/\%/g, '') / 100);
  • left,top值为px值
styleLeft = +style.left.replace(/\px/g, '');
styleTop = +style.top.replace(/\px/g, '');

如何获取dom元素的style对象

function getDomCurrentStyle(dom:HTMLElement):CSSStyleDeclaration{
  // @ts-ignore
  return dom.currentStyle as CSSStyleDeclaration || window.getComputedStyle(dom, null)
}

完整代码

<template>
  <a-popover ref="popoverRef" :open="visible" trigger="click" :destroyTooltipOnHide="true">
    <template #title>
      <div class="flex flex-row">
        <div class="flex-shrink-0">标题</div>
        <div class="flex-1 w-0 text-center cursor-move" ref="moveHotContainerRef" @mousedown="onMouseDown">
          中间部分
        </div>
        <div class="flex-shrink-0" @click="hide">关闭</div>
      </div>
    </template>
    <template #content>
      <div>
        <p>Content</p>
        <p>Content</p>
      </div>
    </template>
    <a-button type="primary" @click="onShow">点我</a-button>
  </a-popover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const popoverRef = ref<any>()
const moveHotContainerRef = ref<HTMLDivElement>()
const visible = ref(false)
function hide() {
  visible.value = false
}
function onShow() {
  visible.value = true
}

function onMouseDown(e: MouseEvent) {
  // 获取当前点击位置的x,y坐标
  const { clientX, clientY } = e
  const target = popoverRef.value.getPopupDomNode() as HTMLElement

  let styleLeft: number
  let styleTop: number
  // 获取可被拖动元素左顶点的x,y坐标
  const style = getDomCurrentStyle(target as HTMLElement)
  if (style.left.indexOf('%') >= 0) {
    styleLeft = +document.body.clientWidth * (+style.left.replace(/\%/g, '') / 100)
    styleTop = +document.body.clientHeight * (+style.top.replace(/\%/g, '') / 100)
  } else {
    styleLeft = +style.left.replace(/\px/g, '')
    styleTop = +style.top.replace(/\px/g, '')
  }
  
  function onMouseMove(evt: MouseEvent) {
    // 获取当前拖动到的点的x,y坐标,并计算x,y轴的偏移量
    const dx = evt.clientX - clientX
    const dy = evt.clientY - clientY
    // 可被拖动元素初始左顶点的x,y坐标加上偏移量,得到最新左顶点的x,y坐标
    target.style.left = `${styleLeft + dx}px`
    target.style.top = `${styleTop + dy}px`
    console.log('document.onmousemove', target, styleLeft + dx)
    return false
  }
  document.addEventListener('mousemove', onMouseMove)

  function onMouseUp() {
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
  }
  document.addEventListener('mouseup', onMouseUp)
}
function getDomCurrentStyle(dom: HTMLElement): CSSStyleDeclaration {
  // @ts-ignore
  return (dom.currentStyle as CSSStyleDeclaration) || window.getComputedStyle(dom, null)
}
</script>

<style scoped></style>

可改进点

加入边界处理,防止弹层被拖动到浏览器视口之外

参考资料

vue项目实现el-popover弹窗拖拽位置以及实现弹窗的拖拽调整大小功能-CSDN博客

对elementui中el-popover增加拖拽功能_怎么让element的popver组件可以拖拽-CSDN博客

elementUI 弹出框添加可自定义拖拽和拉伸功能,并处理边界问题 - 丶小馨 - 博客园 (cnblogs.com)