- // ==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.1
- // @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.allSettled([
- 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.allSettled([
- 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.allSettled([
- 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.allSettled([
- 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;
-
- })({});