手摸手实现vue拖拽组件-限制范围

194 阅读4分钟

手摸手实现vue拖拽组件-限制拖拽范围

代码&效果

先上效果

未命名.gif

再上代码(急急国王们先拿走)- vue3+ts

hooks


/*
 * @version: 1.0
 * @Author: fubobo
 * @Date: 2025-03-03 10:38:47
 * @LastEditors: fubobo
 * @LastEditTime: 2025-03-04 15:18:44
 * @Descripttion: box移动,限制在父元素移动
 */
class UseDrag {
  parentDomRef = ref<HTMLElement>();
  dragDomRef = ref<HTMLElement>();
  // 移动元素
  dragElement = ref({
    isMouseDown: false, // 保存鼠标点击下去的状态
    x: 240,
    y: 0,
    preX: 240, // 上一次box的X
    preY: 0, // 上一次box的Y
    maxX: 0, // 可移动最大X
    maxY: 0, // 可移动最大Y
  });
  // 鼠标原始点
  mousePoint = ref({
    x: 0,
    y: 0,
  });

  // 子元素样式
  computeStyle = computed(() => {
    return `transform: translate(${this.dragElement.value.x}px, ${this.dragElement.value.y}px)`;
  });

  constructor() {
    onMounted(() => {
      this.calculateBoundary();
      this.dragDomRef.value?.addEventListener('mousedown', this.startDrag);
      window.addEventListener('mousemove', this.drag);
      window.addEventListener('mouseup', this.stopDrag);
    });

    onBeforeUnmount(() => {
      window.removeEventListener('mousemove', this.drag);
      window.removeEventListener('mouseup', this.stopDrag);
      this.dragDomRef.value?.removeEventListener('mousedown', this.startDrag);
    });
  }

  // 计算边界限制
  protected calculateBoundary() {
    const parentRect = this.parentDomRef.value?.getBoundingClientRect();
    const sonRect = this.dragDomRef.value?.getBoundingClientRect();
    this.dragElement.value.maxX =
      (parentRect?.width as number) - (sonRect?.width as number);
    this.dragElement.value.maxY =
      (parentRect?.height as number) - (sonRect?.height as number);
  }

  // 鼠标点击后开始移动
  protected startDrag = (e: MouseEvent) => {
    this.dragElement.value.isMouseDown = true;
    const parentRect = this.parentDomRef.value?.getBoundingClientRect();

    // 储存原始点,以父box坐上角为坐标轴,计算鼠标初始坐标
    this.mousePoint.value = {
      x: e.clientX - (parentRect?.x as number),
      y: e.clientY - (parentRect?.y as number),
    };
  };

  // 拖动
  protected drag = (e: MouseEvent) => {
    const parentRect = this.parentDomRef.value?.getBoundingClientRect();
    if (this.dragElement.value.isMouseDown) {
      // 计算鼠标相对于拖拽box的偏移量
      const offsetX = this.mousePoint.value.x - this.dragElement.value.preX;
      const offsetY = this.mousePoint.value.y - this.dragElement.value.preY;

      // 限制边界
      const newX = Math.min(
        e.clientX - (parentRect?.x as number) - offsetX,
        this.dragElement.value.maxX
      );
      const newY = Math.min(
        e.clientY - (parentRect?.y as number) - offsetY,
        this.dragElement.value.maxY
      );
      this.dragElement.value.x = newX > 0 ? newX : 0;
      this.dragElement.value.y = newY > 0 ? newY : 0;
    }
  };

  // 停止拖动
  protected stopDrag = () => {
    // 更新上一次拖拽box的坐标
    this.dragElement.value.preX = this.dragElement.value.x;
    this.dragElement.value.preY = this.dragElement.value.y;
    this.dragElement.value.isMouseDown = false;
  };
}

export default UseDrag;

inde.vue

  <div class="home-box" ref="parentDomRef">
    <div class="drag-box" ref="dragDomRef" :style="computeStyle"></div>
  </div>
</template>
<script lang="ts" setup>
import UseDrag from "./hooks/useDrag";
// 元素移动相关
const { parentDomRef, dragDomRef, computeStyle } = new UseDrag();
</script>
<style>
.home-box {
  position: relative;
  margin: 50px auto;
  width: 360px;
  height: 500px;
  background: #ccc;
}
.drag-box {
  position: absolute;
  width: 120px;
  height: 245px;
  background: black;
  cursor: move;
}
</style>

实现思路

  • 明白鼠标拖动dom的基本实现

基本实现首先要清楚dom上的一些基本知识,这些属性可以通过DOM API getBoundingClientRect()来获取

{
  bottom:430, //元素底部距离窗口顶部的距离 (等于 y + height)
  height:340, //元素的高度
  left:120, //元素左侧距离窗口左侧的距离
  right:460, //元素右侧距离窗口左侧的距离(等于 x + width)
  top:90, //元素顶部距离窗口的距离
  width:340, //元素的宽度
  x:120, //元素左上角相对于视口的横坐标
  y:90 //元素左上角相对于视口的纵坐标
}

其次就是一些鼠标事件(这里只列举了组件用到的鼠标事件)

mousedown:鼠标按下左键时被触发
mousemove: 鼠标移动时出触发
mouseup: 鼠标左键松开触发

通过鼠标的这三个事件可以做到监听鼠标的拖动,需要借助一个变量(isMouseDown)来标识鼠标是否按下下,这样可以确保鼠标移动的时候只有在点击状态时触发拖拽事件。

一般拖拽实现是通过监听鼠标拖拽的变化值来去改变需要被拖拽box的left和right(也可以使用transform: translate(x,y)来实现),基本拖拽的实现原理鼠标、被拖拽box都是相对于浏览器左上角为坐标轴来进行定位。

  • 转化鼠标以及拖拽box相对父元素的坐标

那有直接父元素的话,鼠标和被拖拽物体就可以基于父元素来进行定位,其实在没有直接父元素的情况下,html不就是父元素吗,所以我的实现思路就是借助来这一点。如何转化?

    // 在鼠标刚点击时候,记录鼠标相对于父box的下x,y轴
    const parentRect = this.parentDomRef.value?.getBoundingClientRect();
    // 储存原始点,以父box坐上角为坐标轴,计算鼠标初始坐标
    this.mousePoint.value = {
      x: e.clientX - (parentRect?.x as number),
      y: e.clientY - (parentRect?.y as number),
    };

转化完成后,当鼠标移动的时候,记录其与初始位置的移动距离,计算公式如下

  被拖拽box新位置.x = 当前鼠标相对于父元素的位置.x - (鼠标原始点.x - 被拖拽box拖拽开始前的位置.x)
  y轴同理

最后在鼠标松开时候(mouseup)更新被拖拽物位置信息,为下一次拖拽保存被拖拽物拖拽开始前的位置,如果没有这步,那在下一次点击时候被拖拽box会回到最原始的位置

  • 计算限制区域范围

最后计算范围还是很简单的,因为现在已经根据父元素来定位了,所以maxX = fatherWidth - sonWidth,Y也同理,极小值的话都是0。 有一个点需要注意,鼠标移动以及鼠标抬起需要挂载到windows下来监听,因为鼠标可能会移除被拖拽box

结尾

瞎编不易,~