A lightweight draggable library.
이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/587662/1894134/Element%20Draggable.js을(를) 사용하여 포함하는 라이브러리입니다.
// ==UserScript==
// @name Element Draggable
// @name:zh-CN Element Draggable
// @namespace https://greasyfork.org/users/1570630
// @version 2.0
// @author ryxel
// @description A lightweight draggable library.
// @description:zh-CN 一个轻量级的元素拖拽库。
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
var Draggable = (function() {
'use strict';
const getValue = typeof GM_getValue === 'function' ? GM_getValue : (key) => JSON.parse(localStorage.getItem(key));
const setValue = typeof GM_setValue === 'function' ? GM_setValue : (key, value) => localStorage.setItem(key, JSON.stringify(value));
class Draggable {
constructor(element, options = {}) {
if (!element) {
throw new Error('Draggable: Element reference is null or undefined.');
}
this.element = element;
this.handle = options.handle || element;
this.savePosition = options.savePosition || false;
this.id = options.id || null;
this.originalCursor = this.handle.style.cursor;
this.isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
this.isDragging = false;
this.rafId = null;
this.percentX = null;
this.percentY = null;
this.pointerX = 0;
this.pointerY = 0;
this.offsetX = 0;
this.offsetY = 0;
this.maxLeft = 0;
this.maxTop = 0;
this.midLeft = 0;
this.midTop = 0;
this.vw = 0;
this.vh = 0;
this.ew = 0;
this.eh = 0;
this.onResize = this.onResize.bind(this);
this.onStart = this.onStart.bind(this);
this.onMove = this.onMove.bind(this);
this.onEnd = this.onEnd.bind(this);
this.element.style.position = 'fixed';
this.handle.style.cursor = 'grab';
if (this.savePosition && this.id) {
this.loadStoredPosition();
}
window.addEventListener('resize', this.onResize);
this.handle.addEventListener('mousedown', this.onStart);
this.handle.addEventListener('touchstart', this.onStart, { passive: false });
}
updateBounds() {
const rect = this.element.getBoundingClientRect();
if (this.isMobile) {
this.vw = window.innerWidth;
this.vh = window.innerHeight;
} else {
this.vw = document.documentElement.clientWidth;
this.vh = document.documentElement.clientHeight;
}
this.ew = rect.width || this.ew;
this.eh = rect.height || this.eh;
this.maxLeft = Math.max(0, this.vw - this.ew);
this.maxTop = Math.max(0, this.vh - this.eh);
this.midLeft = this.maxLeft / 2;
this.midTop = this.maxTop / 2;
return rect;
}
setPosition(left, top) {
const clampedLeft = Math.max(0, Math.min(left, this.maxLeft));
const clampedTop = Math.max(0, Math.min(top, this.maxTop));
if (clampedLeft > this.midLeft) {
this.element.style.right = `${this.vw - clampedLeft - this.ew}px`;
this.element.style.left = 'auto';
} else {
this.element.style.left = `${clampedLeft}px`;
this.element.style.right = 'auto';
}
if (clampedTop > this.midTop) {
this.element.style.bottom = `${this.vh - clampedTop - this.eh}px`;
this.element.style.top = 'auto';
} else {
this.element.style.top = `${clampedTop}px`;
this.element.style.bottom = 'auto';
}
}
updatePointerCoords(e) {
const source = (e.touches && e.touches[0]) || e;
this.pointerX = source.clientX;
this.pointerY = source.clientY;
}
cancelAnimation() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
}
onResize() {
if (this.percentX !== null && this.percentY !== null) {
this.updateBounds();
this.setPosition(this.percentX * this.maxLeft, this.percentY * this.maxTop);
}
}
onStart(e) {
if (e.touches && e.touches.length > 1) return;
const tagName = e.target.tagName;
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'BUTTON') {
return;
}
e.preventDefault();
this.updatePointerCoords(e);
const rect = this.updateBounds();
this.offsetX = this.pointerX - rect.left;
this.offsetY = this.pointerY - rect.top;
this.isDragging = true;
this.handle.style.cursor = 'grabbing';
document.addEventListener('mousemove', this.onMove);
document.addEventListener('touchmove', this.onMove, { passive: false });
document.addEventListener('mouseup', this.onEnd);
document.addEventListener('touchend', this.onEnd);
}
onMove(e) {
if (!this.isDragging) return;
e.preventDefault();
this.updatePointerCoords(e);
if (this.rafId) return;
this.rafId = requestAnimationFrame(() => {
this.rafId = null;
this.setPosition(this.pointerX - this.offsetX, this.pointerY - this.offsetY);
});
}
onEnd(e) {
if (!this.isDragging) return;
if (e.touches && e.touches.length > 0) return;
this.isDragging = false;
this.handle.style.cursor = 'grab';
this.cancelAnimation();
document.removeEventListener('mousemove', this.onMove);
document.removeEventListener('touchmove', this.onMove);
document.removeEventListener('mouseup', this.onEnd);
document.removeEventListener('touchend', this.onEnd);
const rect = this.updateBounds();
this.percentX = this.maxLeft > 0 ? Math.max(0, Math.min(1, rect.left / this.maxLeft)) : 0;
this.percentY = this.maxTop > 0 ? Math.max(0, Math.min(1, rect.top / this.maxTop)) : 0;
if (this.savePosition && this.id) {
this.saveStoredPosition();
}
}
saveStoredPosition() {
try {
setValue(`draggable_percent_pos_${this.id}`, {
percentX: this.percentX,
percentY: this.percentY,
ew: this.ew, eh: this.eh
});
} catch (error) {
console.error('Failed to save position:', error);
}
}
loadStoredPosition() {
try {
const position = getValue(`draggable_percent_pos_${this.id}`);
if (position) {
this.ew = position.ew;
this.eh = position.eh;
this.updateBounds();
this.percentX = position.percentX;
this.percentY = position.percentY;
this.setPosition(this.percentX * this.maxLeft, this.percentY * this.maxTop);
}
} catch (error) {
console.error('Failed to load position:', error);
}
}
destroy() {
this.cancelAnimation();
window.removeEventListener('resize', this.onResize);
document.removeEventListener('mousemove', this.onMove);
document.removeEventListener('touchmove', this.onMove);
document.removeEventListener('mouseup', this.onEnd);
document.removeEventListener('touchend', this.onEnd);
if (this.handle) {
this.handle.removeEventListener('mousedown', this.onStart);
this.handle.removeEventListener('touchstart', this.onStart);
this.handle.style.cursor = this.originalCursor;
}
this.element = null;
this.handle = null;
}
}
return Draggable;
})();