@skillnull/drage-js
Version:
拖拽元素到页面任意位置**支持平台:** 浏览器 · 微信小程序 · Node.js · React Native
688 lines (687 loc) • 24.2 kB
JavaScript
/**
* DrageJs - 跨平台拖拽库
* 支持:浏览器 / 微信小程序 / Node.js / React Native
*/
class DrageJs {
constructor() {
this.ref = null;
this.draggingFlag = false;
this.initX = undefined;
this.initY = undefined;
this.currentX = 0;
this.currentY = 0;
this.offsetX = 0;
this.offsetY = 0;
this.pageX = undefined;
this.pageY = undefined;
this.style = {
position: "fixed",
zIndex: '999999'
};
this.setStorage = 'local';
this.animationFrame = null;
// 平台检测
this._detectPlatform();
// 初始化平台适配器
this._initAdapter();
}
/**
* 检测运行平台
*/
_detectPlatform() {
// 微信小程序 / 微信环境
if (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function') {
this.platform = 'weixin';
}
// React Native
else if (typeof global !== 'undefined' && global.__DEV__) {
// RN 的 __DEV__ 全局变量
try {
const rn = require('react-native');
if (rn.Platform) {
this.platform = 'rn';
this.Platform = rn.Platform;
this.Dimensions = rn.Dimensions;
}
}
catch (e) {
this.platform = 'browser';
}
}
// Node.js (无 window)
else if (typeof window === 'undefined' || window === null) {
this.platform = 'node';
}
// 浏览器
else {
this.platform = 'browser';
}
}
/**
* 初始化平台适配器
*/
_initAdapter() {
switch (this.platform) {
case 'browser':
this._initBrowser();
break;
case 'weixin':
this._initWeixin();
break;
case 'node':
this._initNode();
break;
case 'rn':
this._initRN();
break;
}
}
/* ==================== 浏览器适配器 ==================== */
_initBrowser() {
this._getClientSize = () => ({
w: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
h: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
});
this._getElementRect = (el) => el.getBoundingClientRect();
this._setElementPosition = (el, x, y) => {
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
};
this._bindEvent = (el, type, handler, opts) => {
el.addEventListener(type, handler, opts);
};
this._unbindEvent = (el, type, handler) => {
el.removeEventListener(type, handler);
};
this._bindDocEvent = (type, handler, opts) => {
document.addEventListener(type, handler, opts);
};
this._unbindDocEvent = (type, handler) => {
document.removeEventListener(type, handler);
};
this._raf = (fn) => requestAnimationFrame(fn);
this._cancelRaf = (id) => cancelAnimationFrame(id);
this._storage = {
get: (key) => { try {
return localStorage.getItem(key);
}
catch (e) {
return null;
} },
set: (key, val) => { try {
localStorage.setItem(key, val);
}
catch (e) { } },
remove: (key) => { try {
localStorage.removeItem(key);
}
catch (e) { } },
getSession: (key) => { try {
return sessionStorage.getItem(key);
}
catch (e) {
return null;
} },
setSession: (key, val) => { try {
sessionStorage.setItem(key, val);
}
catch (e) { } },
removeSession: (key) => { try {
sessionStorage.removeItem(key);
}
catch (e) { } }
};
this._onResize = () => {
this.clientW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
this.clientH = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
};
window.addEventListener('resize', this._onResize);
const size = this._getClientSize();
this.clientW = size.w;
this.clientH = size.h;
}
/* ==================== 微信小程序适配器 ==================== */
_initWeixin() {
let pages = getCurrentPages();
let currentPage = pages[pages.length - 1];
let pageCtx = currentPage ? currentPage.$page : null;
this._getClientSize = () => {
const sys = wx.getSystemInfoSync();
return { w: sys.windowWidth, h: sys.windowHeight };
};
this._getElementRect = (selector) => new Promise((resolve) => {
const query = wx.createSelectorQuery().in(pageCtx);
query.select(selector).boundingClientRect((rect) => {
if (rect) {
resolve(rect);
}
else {
resolve({ top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0 });
}
}).exec();
});
this._setElementPosition = (ctx, x, y) => {
ctx.setData({
dragStyle: {
transform: `translate3d(${x}px, ${y}px, 0)`
}
});
};
this._bindEvent = (ctx, eventKey, handler) => {
ctx[eventKey] = handler;
};
this._unbindEvent = (ctx, eventKey) => {
ctx[eventKey] = null;
};
// 小程序没有 document,使用全局事件绑定
this._bindDocEvent = (type, handler) => {
this._globalHandlers = this._globalHandlers || {};
this._globalHandlers[type] = handler;
};
this._unbindDocEvent = (type) => {
if (this._globalHandlers) {
this._globalHandlers[type] = null;
}
};
this._raf = (fn) => {
// 小程序中没有 requestAnimationFrame,使用 setData 驱动
fn();
};
this._cancelRaf = (id) => { };
this._storage = {
get: (key) => { try {
return wx.getStorageSync(key);
}
catch (e) {
return null;
} },
set: (key, val) => { try {
wx.setStorageSync(key, val);
}
catch (e) { } },
remove: (key) => { try {
wx.removeStorageSync(key);
}
catch (e) { } },
getSession: (key) => { try {
return wx.getStorageSync(key);
}
catch (e) {
return null;
} },
setSession: (key, val) => { try {
wx.setStorageSync(key, val);
}
catch (e) { } },
removeSession: (key) => { try {
wx.removeStorageSync(key);
}
catch (e) { } }
};
this._onResize = () => {
wx.onWindowResize((res) => {
this.clientW = res.windowWidth;
this.clientH = res.windowHeight;
});
};
const size = this._getClientSize();
this.clientW = size.w;
this.clientH = size.h;
}
/* ==================== Node.js 适配器 ==================== */
_initNode() {
this._getClientSize = () => ({ w: 1920, h: 1080 });
this._getElementRect = (el) => ({
top: el._top || 0,
bottom: (el._top || 0) + (el._height || 0),
left: el._left || 0,
right: (el._left || 0) + (el._width || 0)
});
this._setElementPosition = (el, x, y) => {
el._x = x;
el._y = y;
el.style = el.style || {};
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
};
this._bindEvent = () => { };
this._unbindEvent = () => { };
this._bindDocEvent = () => { };
this._unbindDocEvent = () => { };
this._raf = (fn) => { fn(); };
this._cancelRaf = () => { };
this._storage = {
get: () => null,
set: () => { },
remove: () => { },
getSession: () => null,
setSession: () => { },
removeSession: () => { }
};
this._onResize = () => { };
const size = this._getClientSize();
this.clientW = size.w;
this.clientH = size.h;
}
/* ==================== React Native 适配器 ==================== */
_initRN() {
let dimSubscription = null;
this._getClientSize = () => {
try {
const dim = this.Dimensions.get('window');
return { w: dim.width, h: dim.height };
}
catch (e) {
return { w: 375, h: 667 };
}
};
this._getElementRect = (ref) => new Promise((resolve) => {
try {
if (ref && ref.measure) {
ref.measure((fx, fy, width, height, px, py) => {
resolve({ top: py, bottom: py + height, left: px, right: px + width });
});
}
else {
resolve({ top: 0, bottom: 0, left: 0, right: 0 });
}
}
catch (e) {
resolve({ top: 0, bottom: 0, left: 0, right: 0 });
}
});
// RN 中使用 left/top 代替 transform
this._setElementPosition = (ref, x, y) => {
try {
if (ref && ref.setNativeProps) {
ref.setNativeProps({ style: { left: x, top: y } });
}
}
catch (e) { }
};
this._bindEvent = (ref, handler) => {
try {
if (ref && ref.attachRef) {
ref.attachRef('drageRef', handler);
}
}
catch (e) { }
};
this._unbindEvent = () => { };
this._bindDocEvent = () => { };
this._unbindDocEvent = () => { };
this._raf = (fn) => { fn(); };
this._cancelRaf = () => { };
this._storage = {
get: (key) => {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage');
return AsyncStorage.getItem(key);
}
catch (e) {
return null;
}
},
set: (key, val) => {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage');
AsyncStorage.setItem(key, val);
}
catch (e) { }
},
remove: (key) => {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage');
AsyncStorage.removeItem(key);
}
catch (e) { }
},
getSession: (key) => {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage');
return AsyncStorage.getItem(key);
}
catch (e) {
return null;
}
},
setSession: (key, val) => {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage');
AsyncStorage.setItem(key, val);
}
catch (e) { }
},
removeSession: (key) => {
try {
const AsyncStorage = require('@react-native-async-storage/async-storage');
AsyncStorage.removeItem(key);
}
catch (e) { }
}
};
this._onResize = () => {
try {
dimSubscription = this.Dimensions.addEventListener('change', (size) => {
this.clientW = size.window.width;
this.clientH = size.window.height;
});
}
catch (e) { }
};
const size = this._getClientSize();
this.clientW = size.w;
this.clientH = size.h;
}
/* ==================== 存储适配 ==================== */
_getStorage(type) {
const s = this._storage;
if (type === 'local') {
return { get: s.get, set: s.set, remove: s.remove };
}
return { get: s.getSession, set: s.setSession, remove: s.removeSession };
}
storageHandle(operation) {
if (operation === 'get') {
const store = this._getStorage(this.setStorage);
let storage = store.get('DrageJS_INFO');
try {
storage = JSON.parse(storage);
if (!storage)
return;
this.currentX = storage.currentX || 0;
this.initX = storage.currentX || 0;
this.offsetX = storage.currentX || 0;
this.currentY = storage.currentY || 0;
this.initY = storage.currentY || 0;
this.offsetY = storage.currentY || 0;
this.setRef();
}
catch (err) {
// silent
}
}
if (operation === 'set') {
const store = this._getStorage(this.setStorage);
store.set('DrageJS_INFO', JSON.stringify({
currentX: this.currentX,
currentY: this.currentY
}));
}
if (operation === 'remove') {
const storeLocal = this._getStorage('local');
const storeSession = this._getStorage('session');
storeLocal.remove('DrageJS_INFO');
storeSession.remove('DrageJS_INFO');
}
}
/* ==================== 公共接口 ==================== */
/**
* 启用拖拽
* @param {Object} options
* @param {HTMLElement|String|Function} options.ref - 浏览器: HTMLElement / 小程序: 选择器字符串 / RN: ref 回调
* @param {Object} [options.style={}] - 自定义样式
* @param {String} [options.setStorage='local'] - 'local' | 'session' | 'none'
* @param {String} [options.platform] - 强制指定平台: 'browser' | 'weixin' | 'node' | 'rn'
*/
listen({ ref, style = {}, setStorage = 'local', platform }) {
// 允许运行时覆盖平台
if (platform && platform !== this.platform) {
this.platform = platform;
this._initAdapter();
}
this.ref = ref;
this.setStorage = setStorage;
if (style && Object.keys(style).length > 0) {
this.style = style;
}
if (setStorage && setStorage !== 'none') {
this.storageHandle('get');
}
// 根据平台绑定事件
switch (this.platform) {
case 'browser':
this._listenBrowser(ref);
break;
case 'weixin':
this._listenWeixin(ref);
break;
case 'node':
this._listenNode(ref);
break;
case 'rn':
this._listenRN(ref);
break;
}
}
/**
* 移除拖拽监听
* @param {HTMLElement|String|Function} [ref]
*/
removeListen(ref) {
this.draggingFlag = false;
switch (this.platform) {
case 'browser':
this._removeBrowser(ref);
break;
case 'weixin':
this._removeWeixin(ref);
break;
case 'node':
// Node 无需清理
break;
case 'rn':
this._removeRN(ref);
break;
}
if (this._onResize) {
if (typeof window !== 'undefined' && window.removeEventListener) {
window.removeEventListener('resize', this._onResize);
}
}
if (this.platform === 'rn' && this._dimSubscription) {
try {
this._dimSubscription.remove();
}
catch (e) { }
}
}
setRef() {
if (!this.ref)
return;
// 应用用户自定义样式
if (this.platform === 'browser' && typeof this.ref !== 'string') {
for (let item in this.style) {
this.ref.style[item] = this.style[item];
}
}
this._setElementPosition(this.ref, this.currentX, this.currentY);
}
/* ==================== 浏览器事件绑定 ==================== */
_listenBrowser(ref) {
if (!ref)
return;
ref.setAttribute('draggable', false);
this._onTouchStart = (e) => this.onStart(e);
this._onMouseDown = (e) => this.onStart(e);
this._onTouchEnd = (e) => this.onEnd(e);
this._onMouseUp = (e) => this.onEnd(e);
this._onTouchMove = (e) => this.onMove(e);
this._onMouseMove = (e) => this.onMove(e);
this._bindEvent(ref, 'touchstart', this._onTouchStart, { passive: false });
this._bindEvent(ref, 'mousedown', this._onMouseDown, { passive: false });
this._bindDocEvent('touchend', this._onTouchEnd);
this._bindDocEvent('mouseup', this._onMouseUp);
}
_removeBrowser(ref) {
const target = ref || this.ref || document;
if (!target)
return;
if (this._onTouchStart)
target.removeEventListener('touchstart', this._onTouchStart);
if (this._onMouseDown)
target.removeEventListener('mousedown', this._onMouseDown);
if (this._onTouchEnd)
document.removeEventListener('touchend', this._onTouchEnd);
if (this._onMouseUp)
document.removeEventListener('mouseup', this._onMouseUp);
if (this._onTouchMove)
document.removeEventListener('touchmove', this._onTouchMove);
if (this._onMouseMove)
document.removeEventListener('mousemove', this._onMouseMove);
}
/* ==================== 微信小程序事件绑定 ==================== */
_listenWeixin(selector) {
this._refSelector = selector;
// 小程序中通过 bind:touchstart 等绑定到页面上
// 这里提供一个方法让调用方在 WXML 中绑定
this._handlers = {
touchstart: (e) => this.onStart(e),
touchmove: (e) => this.onMove(e),
touchend: (e) => this.onEnd(e)
};
// 存储 handler 供外部在 WXML 中绑定
this._wxBindEvents = {
onTouchstart: (e) => this._handlers.touchstart(e),
onTouchmove: (e) => this._handlers.touchmove(e),
onTouchend: (e) => this._handlers.touchend(e)
};
}
_removeWeixin(ref) {
this._wxBindEvents = null;
this._handlers = null;
}
/* ==================== Node.js 事件绑定 ==================== */
_listenNode(ref) {
// Node 环境下无真实 DOM,仅做内存模拟
if (ref) {
ref._top = ref._top || 0;
ref._left = ref._left || 0;
ref._width = ref._width || 100;
ref._height = ref._height || 100;
}
}
/* ==================== React Native 事件绑定 ==================== */
_listenRN(ref) {
this._rnRef = ref;
}
_removeRN(ref) {
this._rnRef = null;
}
/* ==================== 拖拽核心逻辑 ==================== */
onStart(event) {
this.draggingFlag = true;
let clientX, clientY;
if (this.platform === 'browser') {
const touch = event.touches ? event.touches[0] : (event.changedTouches ? event.changedTouches[0] : event);
clientX = touch.clientX;
clientY = touch.clientY;
}
else if (this.platform === 'weixin') {
clientX = event.touches ? event.touches[0].clientX : 0;
clientY = event.touches ? event.touches[0].clientY : 0;
}
else {
clientX = event.clientX || 0;
clientY = event.clientY || 0;
}
this.initX = clientX - this.offsetX;
this.initY = clientY - this.offsetY;
// 绑定移动/结束事件
if (this.platform === 'browser') {
this._bindDocEvent('touchmove', this._onTouchMove, { passive: false });
this._bindDocEvent('mousemove', this._onMouseMove, { passive: false });
}
else if (this.platform === 'weixin') {
// 小程序事件已在 WXML 中绑定,不需要额外操作
}
}
onMove(event) {
if (!this.draggingFlag || !event)
return;
let clientX, clientY;
if (this.platform === 'browser') {
const touch = event.touches ? event.touches[0] : (event.changedTouches ? event.changedTouches[0] : event);
clientX = touch.clientX;
clientY = touch.clientY;
}
else if (this.platform === 'weixin') {
clientX = event.touches ? event.touches[0].clientX : 0;
clientY = event.touches ? event.touches[0].clientY : 0;
}
else {
clientX = event.clientX || 0;
clientY = event.clientY || 0;
}
this.currentX = clientX - this.initX;
this.currentY = clientY - this.initY;
const moveX = this.currentX - this.offsetX;
const moveY = this.currentY - this.offsetY;
// 获取元素位置信息
let rect;
if (this.platform === 'browser') {
rect = this._getElementRect(this.ref);
}
else if (this.platform === 'weixin') {
// 小程序异步获取,这里用缓存值
rect = this._cachedRect || { top: 0, bottom: 0, left: 0, right: 0 };
}
else {
rect = this._getElementRect(this.ref);
}
const { top, bottom, left, right } = rect;
// 边界处理 - 左/上
if (left <= 0) {
if (moveX <= 0) {
this.offsetX -= left;
this.currentX = this.offsetX;
}
}
else if (right >= this.clientW) {
if (moveX >= 0) {
this.offsetX -= (right - this.clientW);
this.currentX = this.offsetX;
}
}
else {
this.offsetX = this.currentX;
}
if (top <= 0) {
if (moveY <= 0) {
this.offsetY -= top;
this.currentY = this.offsetY;
}
}
else if (bottom >= this.clientH) {
if (moveY >= 0) {
this.offsetY -= (bottom - this.clientH);
this.currentY = this.offsetY;
}
}
else {
this.offsetY = this.currentY;
}
// 缓存矩形用于小程序
if (this.platform === 'weixin') {
this._cachedRect = rect;
}
this._raf(() => {
this.setRef();
});
}
onEnd(event) {
this.draggingFlag = false;
if (this.platform === 'browser') {
this._unbindDocEvent('touchmove', this._onTouchMove);
this._unbindDocEvent('mousemove', this._onMouseMove);
}
this.storageHandle('set');
}
}
/* ==================== 多环境导出 ==================== */
const Drage = new DrageJs();
// 浏览器 UMD 导出
if (typeof window !== 'undefined' && window !== null) {
;
window.Drage = Drage;
}
// ES Module 导出
export default Drage;