dragable-js
Version:
一个简单易用的js拖动库
339 lines (292 loc) • 11.5 kB
JavaScript
/**
* 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');
};
})();