Element Draggable

A lightweight draggable library.

Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @require https://update.greasyfork.org/scripts/587662/1894134/Element%20Draggable.js

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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;
})();