天天看點

實作支援點選事件、重新整理後保留位置拖拽指令的兩種方案

作者:涼茶社
拖拽原理: 滑鼠按下時記錄初始坐标以及元素的初始位置,在滑鼠移動時計算目前坐标與初始坐标的內插補點,這個內插補點就是元素所移動的距離,注意最後移動的時候需要加上元素的初始位置。
實作支援點選事件、重新整理後保留位置拖拽指令的兩種方案

觀察指令的鈎子,我們隻需要用到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 };
           

文章結束啦~

繼續閱讀