UserUtils

Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more

As of 16.09.2023. See ბოლო ვერსია.

ეს სკრიპტი არ უნდა იყოს პირდაპირ დაინსტალირებული. ეს ბიბლიოთეკაა, სხვა სკრიპტებისთვის უნდა ჩართეთ მეტა-დირექტივაში // @require https://update.greasyforks.org/scripts/472956/1251427/UserUtils.js.

// ==UserScript==
// @name         UserUtils
// @description  Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more
// @namespace    https://github.com/Sv443-Network/UserUtils
// @version      1.1.2
// @license      MIT
// @author       Sv443
// @copyright    Sv443 (https://github.com/Sv443)
// @supportURL   https://github.com/Sv443-Network/UserUtils/issues
// @homepageURL  https://github.com/Sv443-Network/UserUtils#readme
// ==/UserScript==

var UserUtils = (function (exports) {
  var __defProp = Object.defineProperty;
  var __defProps = Object.defineProperties;
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __spreadValues = (a, b) => {
    for (var prop in b || (b = {}))
      if (__hasOwnProp.call(b, prop))
        __defNormalProp(a, prop, b[prop]);
    if (__getOwnPropSymbols)
      for (var prop of __getOwnPropSymbols(b)) {
        if (__propIsEnum.call(b, prop))
          __defNormalProp(a, prop, b[prop]);
      }
    return a;
  };
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  var __publicField = (obj, key, value) => {
    __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
    return value;
  };
  var __async = (__this, __arguments, generator) => {
    return new Promise((resolve, reject) => {
      var fulfilled = (value) => {
        try {
          step(generator.next(value));
        } catch (e) {
          reject(e);
        }
      };
      var rejected = (value) => {
        try {
          step(generator.throw(value));
        } catch (e) {
          reject(e);
        }
      };
      var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
      step((generator = generator.apply(__this, __arguments)).next());
    });
  };

  // lib/math.ts
  function clamp(value, min, max) {
    return Math.max(Math.min(value, max), min);
  }
  function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
    if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
      return value * (range_2_max / range_1_max);
    return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
  }
  function randRange(...args) {
    let min, max;
    if (typeof args[0] === "number" && typeof args[1] === "number") {
      [min, max] = args;
    } else if (typeof args[0] === "number" && typeof args[1] !== "number") {
      min = 0;
      max = args[0];
    } else
      throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
    min = Number(min);
    max = Number(max);
    if (isNaN(min) || isNaN(max))
      throw new TypeError(`Parameters "min" and "max" can't be NaN`);
    if (min > max)
      throw new TypeError(`Parameter "min" can't be bigger than "max"`);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  // lib/array.ts
  function randomItem(array) {
    return randomItemIndex(array)[0];
  }
  function randomItemIndex(array) {
    if (array.length === 0)
      return [void 0, void 0];
    const idx = randRange(array.length - 1);
    return [array[idx], idx];
  }
  function takeRandomItem(arr) {
    const [itm, idx] = randomItemIndex(arr);
    if (idx === void 0)
      return void 0;
    arr.splice(idx, 1);
    return itm;
  }
  function randomizeArray(array) {
    const retArray = [...array];
    if (array.length === 0)
      return array;
    for (let i = retArray.length - 1; i > 0; i--) {
      const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
      [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
    }
    return retArray;
  }

  // lib/config.ts
  var ConfigManager = class {
    /**
     * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.  
     * Supports migrating data from older versions of the configuration to newer ones and populating the cache with default data if no persistent data is found.  
     *   
     * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`  
     * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
     * 
     * @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `config.defaultConfig`) - this should also be the type of the data format associated with the current `options.formatVersion`
     * @param options The options for this ConfigManager instance
    */
    constructor(options) {
      __publicField(this, "id");
      __publicField(this, "formatVersion");
      __publicField(this, "defaultConfig");
      __publicField(this, "cachedConfig");
      __publicField(this, "migrations");
      this.id = options.id;
      this.formatVersion = options.formatVersion;
      this.defaultConfig = options.defaultConfig;
      this.cachedConfig = options.defaultConfig;
      this.migrations = options.migrations;
    }
    /**
     * Loads the data saved in persistent storage into the in-memory cache and also returns it.  
     * Automatically populates persistent storage with default data if it doesn't contain any data yet.  
     * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
     */
    loadData() {
      return __async(this, null, function* () {
        try {
          const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
          let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
          if (typeof gmData !== "string") {
            yield this.saveDefaultData();
            return this.defaultConfig;
          }
          if (isNaN(gmFmtVer))
            yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
          let parsed = JSON.parse(gmData);
          if (gmFmtVer < this.formatVersion && this.migrations)
            parsed = yield this.runMigrations(parsed, gmFmtVer);
          return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
        } catch (err) {
          yield this.saveDefaultData();
          return this.defaultConfig;
        }
      });
    }
    /** Returns a copy of the data from the in-memory cache. Use `loadData()` to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage). */
    getData() {
      return this.deepCopy(this.cachedConfig);
    }
    /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
    setData(data) {
      this.cachedConfig = data;
      return new Promise((resolve) => __async(this, null, function* () {
        yield Promise.all([
          GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
          GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
        ]);
        resolve();
      }));
    }
    /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
    saveDefaultData() {
      return __async(this, null, function* () {
        this.cachedConfig = this.defaultConfig;
        return new Promise((resolve) => __async(this, null, function* () {
          yield Promise.all([
            GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
            GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
          ]);
          resolve();
        }));
      });
    }
    /**
     * Call this method to clear all persistently stored data associated with this ConfigManager instance.  
     * The in-memory cache will be left untouched, so you may still access the data with `getData()`.  
     * Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.  
     *   
     * ⚠️ This requires the additional directive `@grant GM.deleteValue`
     */
    deleteConfig() {
      return __async(this, null, function* () {
        yield Promise.all([
          GM.deleteValue(`_uucfg-${this.id}`),
          GM.deleteValue(`_uucfgver-${this.id}`)
        ]);
      });
    }
    /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
    runMigrations(oldData, oldFmtVer) {
      return __async(this, null, function* () {
        if (!this.migrations)
          return oldData;
        let newData = oldData;
        const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
        let lastFmtVer = oldFmtVer;
        for (const [fmtVer, migrationFunc] of sortedMigrations) {
          const ver = Number(fmtVer);
          if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
            try {
              const migRes = migrationFunc(newData);
              newData = migRes instanceof Promise ? yield migRes : migRes;
              lastFmtVer = oldFmtVer = ver;
            } catch (err) {
              console.error(`Error while running migration function for format version ${fmtVer}:`, err);
            }
          }
        }
        yield Promise.all([
          GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
          GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
        ]);
        return newData;
      });
    }
    /** Copies a JSON-compatible object and loses its internal references */
    deepCopy(obj) {
      return JSON.parse(JSON.stringify(obj));
    }
  };

  // lib/dom.ts
  function getUnsafeWindow() {
    try {
      return unsafeWindow;
    } catch (e) {
      return window;
    }
  }
  function insertAfter(beforeElement, afterElement) {
    var _a;
    (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
    return afterElement;
  }
  function addParent(element, newParent) {
    const oldParent = element.parentNode;
    if (!oldParent)
      throw new Error("Element doesn't have a parent node");
    oldParent.replaceChild(newParent, element);
    newParent.appendChild(element);
    return newParent;
  }
  function addGlobalStyle(style) {
    const styleElem = document.createElement("style");
    styleElem.innerHTML = style;
    document.head.appendChild(styleElem);
  }
  function preloadImages(srcUrls, rejects = false) {
    const promises = srcUrls.map((src) => new Promise((res, rej) => {
      const image = new Image();
      image.src = src;
      image.addEventListener("load", () => res(image));
      image.addEventListener("error", (evt) => rejects && rej(evt));
    }));
    return Promise.allSettled(promises);
  }
  function openInNewTab(href) {
    const openElem = document.createElement("a");
    Object.assign(openElem, {
      className: "userutils-open-in-new-tab",
      target: "_blank",
      rel: "noopener noreferrer",
      href
    });
    openElem.style.display = "none";
    document.body.appendChild(openElem);
    openElem.click();
    setTimeout(openElem.remove, 50);
  }
  function interceptEvent(eventObject, eventName, predicate) {
    if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
      Error.stackTraceLimit = 1e3;
    }
    (function(original) {
      eventObject.__proto__.addEventListener = function(...args) {
        var _a, _b;
        const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
        args[1] = function(...a) {
          if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
            return;
          else
            return origListener.apply(this, a);
        };
        original.apply(this, args);
      };
    })(eventObject.__proto__.addEventListener);
  }
  function interceptWindowEvent(eventName, predicate) {
    return interceptEvent(getUnsafeWindow(), eventName, predicate);
  }
  function amplifyMedia(mediaElement, multiplier = 1) {
    const context = new (window.AudioContext || window.webkitAudioContext)();
    const result = {
      mediaElement,
      amplify: (multiplier2) => {
        result.gain.gain.value = multiplier2;
      },
      getAmpLevel: () => result.gain.gain.value,
      context,
      source: context.createMediaElementSource(mediaElement),
      gain: context.createGain()
    };
    result.source.connect(result.gain);
    result.gain.connect(context.destination);
    result.amplify(multiplier);
    return result;
  }
  function isScrollable(element) {
    const { overflowX, overflowY } = getComputedStyle(element);
    return {
      vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
      horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
    };
  }

  // lib/misc.ts
  function autoPlural(word, num) {
    if (Array.isArray(num) || num instanceof NodeList)
      num = num.length;
    return `${word}${num === 1 ? "" : "s"}`;
  }
  function pauseFor(time) {
    return new Promise((res) => {
      setTimeout(() => res(), time);
    });
  }
  function debounce(func, timeout = 300) {
    let timer;
    return function(...args) {
      clearTimeout(timer);
      timer = setTimeout(() => func.apply(this, args), timeout);
    };
  }
  function fetchAdvanced(_0) {
    return __async(this, arguments, function* (url, options = {}) {
      const { timeout = 1e4 } = options;
      const controller = new AbortController();
      const id = setTimeout(() => controller.abort(), timeout);
      const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
        signal: controller.signal
      }));
      clearTimeout(id);
      return res;
    });
  }

  // lib/onSelector.ts
  var selectorMap = /* @__PURE__ */ new Map();
  function onSelector(selector, options) {
    let selectorMapItems = [];
    if (selectorMap.has(selector))
      selectorMapItems = selectorMap.get(selector);
    selectorMapItems.push(options);
    selectorMap.set(selector, selectorMapItems);
    checkSelectorExists(selector, selectorMapItems);
  }
  function removeOnSelector(selector) {
    return selectorMap.delete(selector);
  }
  function checkSelectorExists(selector, options) {
    const deleteIndices = [];
    options.forEach((option, i) => {
      try {
        const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
        if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
          option.listener(elements);
          if (!option.continuous)
            deleteIndices.push(i);
        }
      } catch (err) {
        console.error(`Couldn't call listener for selector '${selector}'`, err);
      }
    });
    if (deleteIndices.length > 0) {
      const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
      if (newOptsArray.length === 0)
        selectorMap.delete(selector);
      else {
        selectorMap.set(selector, newOptsArray);
      }
    }
  }
  function initOnSelector(options = {}) {
    const observer = new MutationObserver(() => {
      for (const [selector, options2] of selectorMap.entries())
        checkSelectorExists(selector, options2);
    });
    observer.observe(document.body, __spreadValues({
      subtree: true,
      childList: true
    }, options));
  }
  function getSelectorMap() {
    return selectorMap;
  }

  exports.ConfigManager = ConfigManager;
  exports.addGlobalStyle = addGlobalStyle;
  exports.addParent = addParent;
  exports.amplifyMedia = amplifyMedia;
  exports.autoPlural = autoPlural;
  exports.clamp = clamp;
  exports.debounce = debounce;
  exports.fetchAdvanced = fetchAdvanced;
  exports.getSelectorMap = getSelectorMap;
  exports.getUnsafeWindow = getUnsafeWindow;
  exports.initOnSelector = initOnSelector;
  exports.insertAfter = insertAfter;
  exports.interceptEvent = interceptEvent;
  exports.interceptWindowEvent = interceptWindowEvent;
  exports.isScrollable = isScrollable;
  exports.mapRange = mapRange;
  exports.onSelector = onSelector;
  exports.openInNewTab = openInNewTab;
  exports.pauseFor = pauseFor;
  exports.preloadImages = preloadImages;
  exports.randRange = randRange;
  exports.randomItem = randomItem;
  exports.randomItemIndex = randomItemIndex;
  exports.randomizeArray = randomizeArray;
  exports.removeOnSelector = removeOnSelector;
  exports.takeRandomItem = takeRandomItem;

  return exports;

})({});
长期地址
遇到问题?请前往 GitHub 提 Issues。