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;
}
……
关注公众号