Skip to content

弹窗(元素)拖拽的方案

上次更新 2024年10月30日星期三 2:51:32 字数 0 字 时长 0 分钟

背景简述

  • 为什么要搞这个?

这个需求很常见的,一般依赖于组件库提供功能或使用 vususe 实现。 最近 同事 xingji 搞了个项目,项目很冗杂,拖拽的需求很多 可能是由于没有 review,所以好的实现 很冗杂。 比如 使用 vue 自定义指令 定义了若干指令 实现该功能 还有 使用 组件插槽的方案 将弹窗根组件 添加鼠标事件实现拖拽效果 还有通过代码混入 minxin 实现拖拽效果 一个项目中有多种代码实现同一个功能是非常不合适的

  • 主要原因

多人协作 并没有讨论过,导致代码冗余 多人协作 没有 review 的部分 后期维护 加入新需求 保持能用就行的态度促成屎山

实现的基本知识

  1. 定位 absolute
  2. 鼠标的 mousedown 事件 mousemove 事件 mouseup 事件
  3. 拖拽元素的 offsetLeft offsetTop 属性 clientX clientY 属性等
  4. 拖拽位置变化 tansform: translateX(x) translateY(y) (或者 直接调整 top left 值)
  5. 细节问题 可退拽区域范围

实现

js
// 1. 创建元素
// 2. 添加事件监听
// 3. 位置变换
// 4. 将元素添加到目标容器(这里的目标容器是为了限制拖拽范围 默认应该是 body)
// 注意 为了语义增强 可以使用dialog 标签

class DraggableElement {
  /**
   * 构造函数
   *
   * 初始化元素及其交互区域和目标区域
   *
   * @param {HTMLElement} element - 主体元素
   * @param {HTMLElement} interactiveArea - 交互区域元素,用于触发交互行为
   * @param {HTMLElement} target - 目标区域,默认 body
   */
  constructor(element, interactiveArea, target) {
    this.element = document.querySelector(element);
    this.interactiveArea = this.element.querySelector(interactiveArea);
    this.interactiveArea.cursor = "pointer";
    this.target =
      document.querySelector(target) || document.querySelector("body");
    this.isDragging = false;
    this.offsetX = 0;
    this.offsetY = 0;
    this.handleMouseDown = this.handleMouseDown.bind(this);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.handleMouseUp = this.handleMouseUp.bind(this);
    this.interactiveArea.addEventListener("mousedown", this.handleMouseDown);
    this.interactiveArea.style.userSelect = "none";
  }
  handleMouseDown(event) {
    this.isDragging = true;
    this.offsetX = event.clientX - this.element.offsetLeft;
    this.offsetY = event.clientY - this.element.offsetTop;
    this.element.style.cursor = "move";
    document.addEventListener("mousemove", this.handleMouseMove);
    document.addEventListener("mouseup", this.handleMouseUp);
  }
  handleMouseMove(event) {
    if (!this.isDragging) return;
    let left = event.clientX - this.offsetX;
    let top = event.clientY - this.offsetY;
    // 限制拖拽范围
    if (left < 0) {
      left = 0;
    } else if (left > this.target.offsetWidth - this.element.offsetWidth) {
      left = this.target.offsetWidth - this.element.offsetWidth;
    }
    if (top < 0) {
      top = 0;
    } else if (top > this.target.offsetHeight - this.element.offsetHeight) {
      top = this.target.offsetHeight - this.element.offsetHeight;
    }
    this.element.style.left = `${left}px`;
    this.element.style.top = `${top}px`;
  }
  handleMouseUp() {
    this.isDragging = false;
    this.element.style.cursor = "pointer";
    document.removeEventListener("mousemove", this.handleMouseMove);
    document.removeEventListener("mouseup", this.handleMouseUp);
  }
  destroy() {
    this.element.removeEventListener("mousedown", this.handleMouseDown);
  }
}
document.addEventListener("DOMContentLoaded", function () {
  const draggable = new DraggableElement("#drag", ".drag-title", ".target");
});
js
// 推荐
// 一般常见的需要再mounted和updated上实现相同的行为
// 除此之外不需要其他的钩子,可以直接用一个函数来定义指令
app.directive("draggable", (el,binding)=>{
  // !TODO 值得考虑问题:元素的获取方式
  // - 现有现有想法直接通过类名获取 缺点笨拙每次都需要书写类名
  // - v-directive:arg="value"  通过arg获取类名
  const draggableEl = new DraggableElement("#drag", '.drag-title', ".target");
})
……
js

……
js
function useDraggable(selector, target) {
    const draggable = new DraggableElement("#drag", '.drag-title', ".target");

    return draggable;
}
……