实现支持点击事件、刷新后保留位置拖拽指令的两种方案

拖拽原理: 鼠标按下时记录初始坐标以及元素的初始位置,在鼠标移动时计算当前坐标与初始坐标的差值,这个差值就是元素所移动的距离,注意最后移动的时候需要加上元素的初始位置。

image.png

观察指令的钩子,我们只需要用到mounted。

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

一个简单的拖拽指令

  • clientX 和 clientY 是鼠标事件对象中的属性,用于表示鼠标相对于浏览器窗口(视口)的水平和垂直坐标位置。
  • offsetLeft 和 offsetTop 是 DOM 元素的属性,用于表示该元素相对于其包含元素(父元素)的水平和垂直偏移量。换句话说,它们表示元素的位置,以像素为单位,相对于其父元素的左上角。在拖拽操作中,它们被用来记录元素的初始位置,以便在拖拽过程中计算新的位置。

定义几个变量,当调用 handleMouseDown 函数(鼠标按下元素时)时,将初始的鼠标坐标(event.clientX 和 event.clientY)以及元素的初始位置(el.offsetLeft 和 el.offsetTop)保存在 initialX、initialY、offsetX 和 offsetY 中。同时开启对鼠标移动和松开事件的监听。

import type { ObjectDirective } from 'vue';

const dragSimple: ObjectDirective<HTMLElement> = {
  mounted(el) {
    el.style.position = 'absolute'; 
    el.style.zIndex = '999'; 

    let initialX = 0;
    let initialY = 0;
    let offsetX = 0;
    let offsetY = 0;

    const handleMouseDown = (event: MouseEvent) => {
      if (event.button !== 0)
        return; // 检查是否是鼠标左键
      initialX = event.clientX;
      initialY = event.clientY;
      offsetX = el.offsetLeft;
      offsetY = el.offsetTop;

      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    };

    const handleMouseMove = (event: MouseEvent) => {
      const dx = event.clientX - initialX;
      const dy = event.clientY - initialY;

      el.style.left = `${offsetX + dx}px`;
      el.style.top = `${offsetY + dy}px`;
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };

    el.addEventListener('mousedown', handleMouseDown);
  },
};

export { dragSimple };
  • 使用
 <div v-drag-simple class="fixed-button">
      <a-button type="primary" size="large" shape="circle" @click="visible = true">
        <template #icon>
          <Icon icon="setting-outlined " />
        </template>
      </a-button>
    </div>

试了一下,拖拽非常的丝滑,没有问题,但是我们发现在拖拽结束的时候click事件被触发了,我和我们想要的效果不符,我们希望在拖拽的时候不要触发这个点击事件,只有在点击的时候才触发。下面提供两个解决方案。

使用阈值

设置一个阈值CLICK_THRESHOLD,当鼠标移动距离不超过阈值就认为是点击事件,超过则为拖拽事件。这种写法需要外部传递一个函数。

import type { ObjectDirective } from 'vue';

const CLICK_THRESHOLD = 5;

const dragSimple: ObjectDirective<HTMLElement> = {
  mounted(el, { value }) {
    el.style.position = 'absolute';
    el.style.zIndex = '999';

    let initialX = 0;
    let initialY = 0;
    let offsetX = 0;
    let offsetY = 0;
    let isDragging = false; // 添加一个标记,表示当前是否正在拖拽

    const handleMouseMove = (event: MouseEvent) => {
      const dx = event.clientX - initialX;
      const dy = event.clientY - initialY;

      el.style.left = `${offsetX + dx}px`;
      el.style.top = `${offsetY + dy}px`;

      // 判断是否开始拖拽
      if (!isDragging) {
        const distance = Math.sqrt(dx * dx + dy * dy);  // 计算直角三角形的斜边长度
        if (distance >= CLICK_THRESHOLD)
          isDragging = true;
          // 在阈值外触发拖拽事件
          // 这里可以触发拖拽事件的回调函数
      }
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      el.removeEventListener('mouseup', handleMouseUp); // 移除mouseup事件监听器

      if (!isDragging) {
        // 在阈值内触发点击事件
        // 这里可以触发点击事件的回调函数
        value();
      }

      isDragging = false; // 重置拖拽标记
    };

    const handleMouseDown = (event: MouseEvent) => {
      if (event.button !== 0)
        return; // 检查是否是鼠标左键
      initialX = event.clientX;
      initialY = event.clientY;
      offsetX = el.offsetLeft;
      offsetY = el.offsetTop;

      document.addEventListener('mousemove', handleMouseMove);
      el.addEventListener('mouseup', handleMouseUp); // 将mouseup事件监听器添加到元素el上
    };

    el.addEventListener('mousedown', handleMouseDown);
  },
};

export { dragSimple };
  • 使用
 <div v-drag-simple="() => visible = true" class="fixed-button">
      <a-button type="primary" size="large" shape="circle">
        <template #icon>
          <Icon icon="setting-outlined " />
        </template>
      </a-button>
    </div>

直接使用pointer-events

有一种更简便的方法,就是在鼠标移动的时候取消元素的 pointer-events,在鼠标松开的时候再恢复。

import type { ObjectDirective } from 'vue';

const dragSimple: ObjectDirective<HTMLElement> = {
  mounted(el) {
    el.style.position = 'absolute';
    el.style.zIndex = '999';

    let initialX = 0;
    let initialY = 0;
    let offsetX = 0;
    let offsetY = 0;

    const handleMouseDown = (event: MouseEvent) => {
      if (event.button !== 0)
        return; // 检查是否是鼠标左键
      initialX = event.clientX;
      initialY = event.clientY;
      offsetX = el.offsetLeft;
      offsetY = el.offsetTop;

      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    };

    const handleMouseMove = (event: MouseEvent) => {
      const dx = event.clientX - initialX;
      const dy = event.clientY - initialY;
      el.style.pointerEvents = 'none'; // 取消元素的 pointer-events,防止拖拽过程中触发子元素的事件

      el.style.left = `${offsetX + dx}px`;
      el.style.top = `${offsetY + dy}px`;
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);

      el.style.pointerEvents = ''; // 恢复元素的 pointer-events
    };

    el.addEventListener('mousedown', handleMouseDown);
  },
};

export { dragSimple };

加个小功能,刷新后还能保持位置

将坐标存入本地缓存就可以了。

/* eslint-disable @typescript-eslint/no-use-before-define */
import type { ObjectDirective } from 'vue';

const dragSimple: ObjectDirective<HTMLElement> = {
  mounted(el) {
    el.style.position = 'absolute';
    el.style.zIndex = '999';

    let initialX = 0;
    let initialY = 0;
    let offsetX = 0;
    let offsetY = 0;

    const POSITION_STORAGE_KEY = 'dragPosition';

    const handleMouseDown = (event: MouseEvent) => {
      if (event.button !== 0)
        return; // 检查是否是鼠标左键
      initialX = event.clientX;
      initialY = event.clientY;
      offsetX = el.offsetLeft;
      offsetY = el.offsetTop;

      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    };

    const handleMouseMove = (event: MouseEvent) => {
      const dx = event.clientX - initialX;
      const dy = event.clientY - initialY;
      el.style.pointerEvents = 'none'; // 取消元素的 pointer-events,防止拖拽过程中触发子元素的事件

      el.style.left = `${offsetX + dx}px`;
      el.style.top = `${offsetY + dy}px`;

      // 保存位置信息到 localStorage
      const positionInfo = {
        left: el.style.left,
        top: el.style.top,
      };
      localStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify(positionInfo));
    };

    const handleMouseUp = () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);

      el.style.pointerEvents = ''; // 恢复元素的 pointer-events
    };

    el.addEventListener('mousedown', handleMouseDown);

    // 页面加载时,从 localStorage 中恢复位置信息
    const storedPositionInfo = localStorage.getItem(POSITION_STORAGE_KEY);
    if (storedPositionInfo) {
      const { left, top } = JSON.parse(storedPositionInfo);
      el.style.left = left ?? 0;
      el.style.top = top ?? 0;
    }
  },
};

export { dragSimple };

文章结束啦~

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务