弹窗(元素)拖拽的方案
背景简述
- 为什么要搞这个?
这个需求很常见的,一般依赖于组件库提供功能或使用 vususe 实现。 最近 同事 xingji 搞了个项目,项目很冗杂,拖拽的需求很多 可能是由于没有 review,所以好的实现 很冗杂。 比如 使用 vue 自定义指令 定义了若干指令 实现该功能 还有 使用 组件插槽的方案 将弹窗根组件 添加鼠标事件实现拖拽效果 还有通过代码混入 minxin 实现拖拽效果 一个项目中有多种代码实现同一个功能是非常不合适的
- 主要原因
多人协作 并没有讨论过,导致代码冗余 多人协作 没有 review 的部分 后期维护 加入新需求 保持能用就行的态度促成屎山
实现的基本知识
- 定位 absolute
- 鼠标的 mousedown 事件 mousemove 事件 mouseup 事件
- 拖拽元素的 offsetLeft offsetTop 属性 clientX clientY 属性等
- 拖拽位置变化 tansform: translateX(x) translateY(y) (或者 直接调整 top left 值)
- 细节问题 可退拽区域范围
实现
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;
}
……