type TDragStartCallback = (x: number, y: number) => void; type TDragMoveCallback = (x: number, y: number) => void; type TDragEndCallback = (x: number, y: number) => void; /** * 拖拽参数 */ interface IDraggableOptions { /** 用来触发拖拽的元素、句柄 */ handle: HTMLElement; /** 真正移动的元素 */ target: HTMLElement; /** 拖拽模式 */ mode?: 'transform' | 'position'; /** 拖拽的边界或容器元素 */ boundary?: IBoundary | HTMLElement; /** 拖拽开始回调 */ onStart?: TDragStartCallback; /** 拖拽移动中回调 */ onMove?: TDragMoveCallback; /** 拖拽结束回调 */ onEnd?: TDragEndCallback; } /** 拖拽的范围边界 */ interface IBoundary { /** 最小 X 坐标 */ minX?: number; /** 最大 X 坐标 */ maxX?: number; /** 最小 Y 坐标 */ minY?: number; /** 最大 Y 坐标 */ maxY?: number; } /** * 拖拽功能通用类 */ export class Draggable { private handle: HTMLElement; private target: HTMLElement; private mode: 'transform' | 'position'; private boundary?: IBoundary; private containerElement?: HTMLElement; private containerBounds?: IBoundary; private startX = 0; private startY = 0; private originX = 0; private originY = 0; private currentX = 0; private currentY = 0; private dragging = false; private onStart?: TDragStartCallback; private onMove?: TDragMoveCallback; private onEnd?: TDragEndCallback; private resizeObserver?: ResizeObserver; constructor(options: IDraggableOptions) { this.handle = options.handle; this.target = options.target; this.mode = options.mode ?? 'transform'; this.onStart = options.onStart; this.onMove = options.onMove; this.onEnd = options.onEnd; // 判断 boundary 类型 if (options.boundary instanceof HTMLElement) { this.containerElement = options.boundary; this.observeResize(); // 监听容器和目标变化 } else { this.boundary = options.boundary; } if (this.mode === 'position') { const computed = window.getComputedStyle(this.target); if (computed.position === 'static') { this.target.style.position = 'absolute'; } } this.handle.addEventListener('mousedown', this.onMouseDown); } /** 监听容器和目标大小变化 */ private observeResize() { this.resizeObserver = new ResizeObserver(() => { this.updateContainerBounds(); }); if (this.containerElement) { this.resizeObserver.observe(this.containerElement); } // 监听目标大小变化 this.resizeObserver.observe(this.target); this.updateContainerBounds(); } /** 更新边界 */ private updateContainerBounds() { if (!this.containerElement) return; const containerRect = this.containerElement.getBoundingClientRect(); const targetRect = this.target.getBoundingClientRect(); if (this.mode === 'transform') { this.containerBounds = { minX: containerRect.left + window.scrollX, maxX: containerRect.right + window.scrollX - targetRect.width, minY: containerRect.top + window.scrollY, maxY: containerRect.bottom + window.scrollY - targetRect.height, }; } else { this.containerBounds = { minX: 0, minY: 0, maxX: containerRect.width - targetRect.width, maxY: containerRect.height - targetRect.height, }; } } private onMouseDown = (e: MouseEvent) => { e.preventDefault(); this.handle.style.cursor = 'move'; this.dragging = true; const rect = this.target.getBoundingClientRect(); this.originX = rect.left + window.scrollX; this.originY = rect.top + window.scrollY; if (this.mode === 'position') { const style = window.getComputedStyle(this.target); this.originX = parseFloat(style.left) || 0; this.originY = parseFloat(style.top) || 0; } this.startX = e.clientX; this.startY = e.clientY; this.currentX = this.originX; this.currentY = this.originY; this.onStart?.(this.currentX, this.currentY); document.addEventListener('mousemove', this.onMouseMove); document.addEventListener('mouseup', this.onMouseUp); }; private onMouseMove = (e: MouseEvent) => { if (!this.dragging) return; let newX = this.originX + (e.clientX - this.startX); let newY = this.originY + (e.clientY - this.startY); const bounds = this.containerBounds || this.boundary; if (bounds) { if (bounds.minX !== undefined) newX = Math.max(newX, bounds.minX); if (bounds.maxX !== undefined) newX = Math.min(newX, bounds.maxX); if (bounds.minY !== undefined) newY = Math.max(newY, bounds.minY); if (bounds.maxY !== undefined) newY = Math.min(newY, bounds.maxY); } if (this.mode === 'transform') { this.target.style.transform = `translate(${newX}px, ${newY}px)`; } else { this.target.style.left = `${newX}px`; this.target.style.top = `${newY}px`; } this.currentX = newX; this.currentY = newY; this.onMove?.(this.currentX, this.currentY); }; private onMouseUp = () => { this.dragging = false; this.handle.style.cursor = 'default'; this.onEnd?.(this.currentX, this.currentY); document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onMouseUp); }; public destroy() { this.handle.removeEventListener('mousedown', this.onMouseDown); document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onMouseUp); this.resizeObserver?.disconnect(); } }