UNPKG

dragable-js

Version:

一个简单易用的js拖动库

339 lines (292 loc) 11.5 kB
/** * Dragable - 增强版拖动库 * 支持移动端和电脑端,增加停靠功能 * 自动添加到HTMLElement原型 * * 使用方法: * element.enableDrag(options); * element.disableDrag(); * * 选项: * { * dockable: true/false, // 是否启用停靠功能 * dockDistance: 20, // 停靠距离阈值(像素) * dockAnimationDuration: 300, // 停靠动画时长(毫秒) * dockAreas: ['top', 'right', 'bottom', 'left'] // 启用的停靠区域 * } */ (function() { // 存储拖动状态 const dragStateMap = new WeakMap(); // 默认选项 const defaultOptions = { dockable: true, dockDistance: 20, dockAnimationDuration: 300, dockAreas: ['top', 'right', 'bottom', 'left'] }; /** * 初始化拖动状态 * @param {HTMLElement} element * @param {Object} options */ function initDragState(element, options) { dragStateMap.set(element, { isDragging: false, startX: 0, startY: 0, startLeft: 0, startTop: 0, touchId: null, options: {...defaultOptions, ...options}, originalTransition: element.style.transition, originalPosition: window.getComputedStyle(element).position }); } /** * 检查是否需要停靠并执行停靠 * @param {HTMLElement} element * @param {number} left * @param {number} top */ function checkAndDock(element, left, top) { const state = dragStateMap.get(element); if (!state || !state.options.dockable) return { left, top, docked: false }; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const elementWidth = element.offsetWidth; const elementHeight = element.offsetHeight; const dockDistance = state.options.dockDistance; let newLeft = left; let newTop = top; let docked = false; let dockArea = null; // 检查顶部停靠 if (state.options.dockAreas.includes('top') && top <= dockDistance) { newTop = 0; docked = true; dockArea = 'top'; } // 检查底部停靠 else if (state.options.dockAreas.includes('bottom') && (top + elementHeight) >= (viewportHeight - dockDistance)) { newTop = viewportHeight - elementHeight; docked = true; dockArea = 'bottom'; } // 检查左侧停靠 if (state.options.dockAreas.includes('left') && left <= dockDistance) { newLeft = 0; docked = true; dockArea = dockArea ? `${dockArea}-left` : 'left'; } // 检查右侧停靠 else if (state.options.dockAreas.includes('right') && (left + elementWidth) >= (viewportWidth - dockDistance)) { newLeft = viewportWidth - elementWidth; docked = true; dockArea = dockArea ? `${dockArea}-right` : 'right'; } if (docked) { // 应用停靠动画 element.style.transition = `left ${state.options.dockAnimationDuration}ms ease-out, top ${state.options.dockAnimationDuration}ms ease-out`; // 触发自定义事件 const event = new CustomEvent('dock', { detail: { area: dockArea, left: newLeft, top: newTop } }); element.dispatchEvent(event); } return { left: newLeft, top: newTop, docked, dockArea }; } /** * 处理拖动开始 * @param {HTMLElement} element * @param {Event} e */ function handleDragStart(element, e) { const state = dragStateMap.get(element); if (!state) return; // 阻止默认行为以避免不必要的页面滚动或选择 e.preventDefault(); // 重置过渡效果 element.style.transition = state.originalTransition; // 获取初始位置 let clientX, clientY; if (e.type.includes('touch')) { const touch = e.touches[0] || e.changedTouches[0]; clientX = touch.clientX; clientY = touch.clientY; state.touchId = touch.identifier; } else { clientX = e.clientX; clientY = e.clientY; } // 获取元素当前样式 const style = window.getComputedStyle(element); const left = parseFloat(style.left) || 0; const top = parseFloat(style.top) || 0; // 更新状态 state.isDragging = true; state.startX = clientX; state.startY = clientY; state.startLeft = left; state.startTop = top; // 添加拖动类 element.classList.add('dragging'); // 触发自定义事件 const event = new CustomEvent('dragstart', { detail: { x: clientX, y: clientY } }); element.dispatchEvent(event); } /** * 处理拖动移动 * @param {HTMLElement} element * @param {Event} e */ function handleDragMove(element, e) { const state = dragStateMap.get(element); if (!state || !state.isDragging) return; e.preventDefault(); let clientX, clientY; if (e.type.includes('touch')) { // 查找匹配的触点 let touch; if (e.touches) { for (let i = 0; i < e.touches.length; i++) { if (e.touches[i].identifier === state.touchId) { touch = e.touches[i]; break; } } } if (!touch && e.changedTouches) { for (let i = 0; i < e.changedTouches.length; i++) { if (e.changedTouches[i].identifier === state.touchId) { touch = e.changedTouches[i]; break; } } } if (!touch) return; clientX = touch.clientX; clientY = touch.clientY; } else { clientX = e.clientX; clientY = e.clientY; } // 计算新位置 const deltaX = clientX - state.startX; const deltaY = clientY - state.startY; let newLeft = state.startLeft + deltaX; let newTop = state.startTop + deltaY; // 边界检查 const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const elementWidth = element.offsetWidth; const elementHeight = element.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, viewportWidth - elementWidth)); newTop = Math.max(0, Math.min(newTop, viewportHeight - elementHeight)); // 检查并应用停靠 const dockResult = checkAndDock(element, newLeft, newTop); // 应用新位置 element.style.left = `${dockResult.left}px`; element.style.top = `${dockResult.top}px`; // 触发自定义事件 const event = new CustomEvent('dragmove', { detail: { x: clientX, y: clientY, left: dockResult.left, top: dockResult.top, docked: dockResult.docked, dockArea: dockResult.dockArea } }); element.dispatchEvent(event); } /** * 处理拖动结束 * @param {HTMLElement} element * @param {Event} e */ function handleDragEnd(element, e) { const state = dragStateMap.get(element); if (!state || !state.isDragging) return; e.preventDefault(); state.isDragging = false; element.classList.remove('dragging'); // 恢复原始过渡效果 setTimeout(() => { element.style.transition = state.originalTransition; }, state.options.dockAnimationDuration); // 触发自定义事件 const event = new CustomEvent('dragend'); element.dispatchEvent(event); } /** * 启用拖动功能 * @param {Object} options */ HTMLElement.prototype.enableDrag = function(options) { // 确保元素可以定位 const computedStyle = window.getComputedStyle(this); if (computedStyle.position === 'static') { this.style.position = 'absolute'; } // 初始化状态 initDragState(this, options); // 添加事件监听器 this.addEventListener('mousedown', this._dragStartHandler = (e) => handleDragStart(this, e)); this.addEventListener('touchstart', this._dragStartHandlerTouch = (e) => handleDragStart(this, e), { passive: false }); document.addEventListener('mousemove', this._dragMoveHandler = (e) => handleDragMove(this, e)); document.addEventListener('touchmove', this._dragMoveHandlerTouch = (e) => handleDragMove(this, e), { passive: false }); document.addEventListener('mouseup', this._dragEndHandler = (e) => handleDragEnd(this, e)); document.addEventListener('touchend', this._dragEndHandlerTouch = (e) => handleDragEnd(this, e)); document.addEventListener('touchcancel', this._dragEndHandlerTouchCancel = (e) => handleDragEnd(this, e)); }; /** * 禁用拖动功能 */ HTMLElement.prototype.disableDrag = function() { // 移除事件监听器 if (this._dragStartHandler) { this.removeEventListener('mousedown', this._dragStartHandler); this._dragStartHandler = null; } if (this._dragStartHandlerTouch) { this.removeEventListener('touchstart', this._dragStartHandlerTouch); this._dragStartHandlerTouch = null; } if (this._dragMoveHandler) { document.removeEventListener('mousemove', this._dragMoveHandler); this._dragMoveHandler = null; } if (this._dragMoveHandlerTouch) { document.removeEventListener('touchmove', this._dragMoveHandlerTouch); this._dragMoveHandlerTouch = null; } if (this._dragEndHandler) { document.removeEventListener('mouseup', this._dragEndHandler); this._dragEndHandler = null; } if (this._dragEndHandlerTouch) { document.removeEventListener('touchend', this._dragEndHandlerTouch); this._dragEndHandlerTouch = null; } if (this._dragEndHandlerTouchCancel) { document.removeEventListener('touchcancel', this._dragEndHandlerTouchCancel); this._dragEndHandlerTouchCancel = null; } // 移除状态 const state = dragStateMap.get(this); if (state) { // 恢复原始样式 this.style.transition = state.originalTransition; if (state.originalPosition === 'static') { this.style.position = state.originalPosition; } } dragStateMap.delete(this); // 移除拖动类 this.classList.remove('dragging'); }; })();