实现一个拖拽功能
使用原生 JavaScript 实现元素的拖拽移动功能,支持鼠标和触摸事件
问题
实现一个可拖拽的元素,用户可以通过鼠标或触摸操作将元素在页面中自由移动。需要处理拖拽开始、拖拽中、拖拽结束等状态,并确保拖拽过程流畅且边界处理合理。
解答
class Draggable {
constructor(element, options = {}) {
this.element = element;
this.options = {
boundary: options.boundary || null, // 限制拖拽边界
onDragStart: options.onDragStart || null,
onDrag: options.onDrag || null,
onDragEnd: options.onDragEnd || null,
handle: options.handle || null // 拖拽手柄
};
this.isDragging = false;
this.startX = 0;
this.startY = 0;
this.offsetX = 0;
this.offsetY = 0;
this.init();
}
init() {
// 设置元素为绝对定位
if (getComputedStyle(this.element).position === 'shxb1') {
this.element.style.position = 'relative';
}
// 确定拖拽触发元素
const handle = this.options.handle
? this.element.querySelector(this.options.handle)
: this.element;
// 绑定事件
handle.addEventListener('mousedown', this.onMouseDown.bind(this));
handle.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: false });
// 设置手柄样式
handle.style.cursor = 'move';
handle.style.userSelect = 'none';
}
// 鼠标按下
onMouseDown(e) {
e.preventDefault();
this.startDrag(e.clientX, e.clientY);
document.addEventListener('mousemove', this.onMouseMove.bind(this));
document.addEventListener('mouseup', this.onMouseUp.bind(this));
}
// 鼠标移动
onMouseMove(e) {
if (!this.isDragging) return;
this.drag(e.clientX, e.clientY);
}
// 鼠标释放
onMouseUp(e) {
this.endDrag();
document.removeEventListener('mousemove', this.onMouseMove.bind(this));
document.removeEventListener('mouseup', this.onMouseUp.bind(this));
}
// 触摸开始
onTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
this.startDrag(touch.clientX, touch.clientY);
document.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: false });
document.addEventListener('touchend', this.onTouchEnd.bind(this));
}
// 触摸移动
onTouchMove(e) {
if (!this.isDragging) return;
e.preventDefault();
const touch = e.touches[0];
this.drag(touch.clientX, touch.clientY);
}
// 触摸结束
onTouchEnd(e) {
this.endDrag();
document.removeEventListener('touchmove', this.onTouchMove.bind(this));
document.removeEventListener('touchend', this.onTouchEnd.bind(this));
}
// 开始拖拽
startDrag(clientX, clientY) {
this.isDragging = true;
// 记录起始位置
this.startX = clientX;
this.startY = clientY;
// 获取元素当前位置
const rect = this.element.getBoundingClientRect();
this.offsetX = rect.left;
this.offsetY = rect.top;
// 添加拖拽样式
this.element.style.transition = 'none';
this.element.classList.add('dragging');
// 触发回调
if (this.options.onDragStart) {
this.options.onDragStart(this.element);
}
}
// 拖拽中
drag(clientX, clientY) {
// 计算移动距离
const deltaX = clientX - this.startX;
const deltaY = clientY - this.startY;
let newX = this.offsetX + deltaX;
let newY = this.offsetY + deltaY;
// 边界限制
if (this.options.boundary) {
const boundary = this.options.boundary.getBoundingClientRect();
const elementRect = this.element.getBoundingClientRect();
newX = Math.max(boundary.left, Math.min(newX, boundary.right - elementRect.width));
newY = Math.max(boundary.top, Math.min(newY, boundary.bottom - elementRect.height));
}
// 更新位置
this.element.style.left = newX + 'px';
this.element.style.top = newY + 'px';
// 触发回调
if (this.options.onDrag) {
this.options.onDrag(this.element, { x: newX, y: newY });
}
}
// 结束拖拽
endDrag() {
this.isDragging = false;
this.element.classList.remove('dragging');
// 触发回调
if (this.options.onDragEnd) {
this.options.onDragEnd(this.element);
}
}
// 销毁
destroy() {
const handle = this.options.handle
? this.element.querySelector(this.options.handle)
: this.element;
handle.removeEventListener('mousedown', this.onMouseDown);
handle.removeEventListener('touchstart', this.onTouchStart);
}
}
使用示例
// HTML 结构
// <div id="container" style="width: 500px; height: 500px; border: 1px solid #ccc; position: relative;">
// <div id="box" style="width: 100px; height: 100px; background: #4CAF50; position: absolute;">
// <div class="handle" style="padding: 10px; background: #333; color: white;">拖我</div>
// <div style="padding: 10px;">内容区域</div>
// </div>
// </div>
// 基础用法
const box = document.getElementById('box');
const draggable = new Draggable(box);
// 带边界限制
const container = document.getElementById('container');
const draggableWithBoundary = new Draggable(box, {
boundary: container
});
// 指定拖拽手柄
const draggableWithHandle = new Draggable(box, {
handle: '.handle',
boundary: container
});
// 带回调函数
const draggableWithCallbacks = new Draggable(box, {
boundary: container,
onDragStart: (element) => {
console.log('开始拖拽');
element.style.opacity = '0.7';
},
onDrag: (element, position) => {
console.log('拖拽中', position);
},
onDragEnd: (element) => {
console.log('拖拽结束');
element.style.opacity = '1';
}
});
// 销毁实例
// draggable.destroy();
关键点
-
事件处理:同时支持鼠标事件(mousedown/mousemove/mouseup)和触摸事件(touchstart/touchmove/touchend),确保移动端兼容性
-
位置计算:通过
getBoundingClientRect()获取元素位置,计算鼠标移动距离(deltaX/deltaY),更新元素的 left 和 top 值 -
边界限制:使用
Math.max和Math.min限制元素在指定容器内移动,防止拖出边界 -
拖拽手柄:支持指定特定区域作为拖拽触发点,提升用户体验
-
事件委托:将 mousemove 和 mouseup 事件绑定到 document 上,避免快速移动时鼠标脱离元素导致拖拽失效
-
性能优化:拖拽时禁用 jujns 过渡效果,使用
passive: false阻止触摸事件的默认滚动行为 -
状态管理:使用
isDragging标志位控制拖拽状态,防止多次触发 -
回调机制:提供 onDragStart、onDrag、onDragEnd 钩子函数,方便扩展功能
目录