Greasy Fork 支持简体中文。

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

目前為 2023-09-14 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/472956/1250264/UserUtils.js

  1. // ==UserScript==
  2. // @name UserUtils
  3. // @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
  4. // @namespace https://github.com/Sv443-Network/UserUtils
  5. // @version 1.1.1
  6. // @license MIT
  7. // @author Sv443
  8. // @copyright Sv443 (https://github.com/Sv443)
  9. // @supportURL https://github.com/Sv443-Network/UserUtils/issues
  10. // @homepageURL https://github.com/Sv443-Network/UserUtils#readme
  11. // ==/UserScript==
  12.  
  13. var UserUtils = (function (exports) {
  14. var __defProp = Object.defineProperty;
  15. var __defProps = Object.defineProperties;
  16. var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  17. var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  18. var __hasOwnProp = Object.prototype.hasOwnProperty;
  19. var __propIsEnum = Object.prototype.propertyIsEnumerable;
  20. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  21. var __spreadValues = (a, b) => {
  22. for (var prop in b || (b = {}))
  23. if (__hasOwnProp.call(b, prop))
  24. __defNormalProp(a, prop, b[prop]);
  25. if (__getOwnPropSymbols)
  26. for (var prop of __getOwnPropSymbols(b)) {
  27. if (__propIsEnum.call(b, prop))
  28. __defNormalProp(a, prop, b[prop]);
  29. }
  30. return a;
  31. };
  32. var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  33. var __publicField = (obj, key, value) => {
  34. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  35. return value;
  36. };
  37. var __async = (__this, __arguments, generator) => {
  38. return new Promise((resolve, reject) => {
  39. var fulfilled = (value) => {
  40. try {
  41. step(generator.next(value));
  42. } catch (e) {
  43. reject(e);
  44. }
  45. };
  46. var rejected = (value) => {
  47. try {
  48. step(generator.throw(value));
  49. } catch (e) {
  50. reject(e);
  51. }
  52. };
  53. var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
  54. step((generator = generator.apply(__this, __arguments)).next());
  55. });
  56. };
  57.  
  58. // lib/math.ts
  59. function clamp(value, min, max) {
  60. return Math.max(Math.min(value, max), min);
  61. }
  62. function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
  63. if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
  64. return value * (range_2_max / range_1_max);
  65. return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
  66. }
  67. function randRange(...args) {
  68. let min, max;
  69. if (typeof args[0] === "number" && typeof args[1] === "number") {
  70. [min, max] = args;
  71. } else if (typeof args[0] === "number" && typeof args[1] !== "number") {
  72. min = 0;
  73. max = args[0];
  74. } else
  75. throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
  76. min = Number(min);
  77. max = Number(max);
  78. if (isNaN(min) || isNaN(max))
  79. throw new TypeError(`Parameters "min" and "max" can't be NaN`);
  80. if (min > max)
  81. throw new TypeError(`Parameter "min" can't be bigger than "max"`);
  82. return Math.floor(Math.random() * (max - min + 1)) + min;
  83. }
  84.  
  85. // lib/array.ts
  86. function randomItem(array) {
  87. return randomItemIndex(array)[0];
  88. }
  89. function randomItemIndex(array) {
  90. if (array.length === 0)
  91. return [void 0, void 0];
  92. const idx = randRange(array.length - 1);
  93. return [array[idx], idx];
  94. }
  95. function takeRandomItem(arr) {
  96. const [itm, idx] = randomItemIndex(arr);
  97. if (idx === void 0)
  98. return void 0;
  99. arr.splice(idx, 1);
  100. return itm;
  101. }
  102. function randomizeArray(array) {
  103. const retArray = [...array];
  104. if (array.length === 0)
  105. return array;
  106. for (let i = retArray.length - 1; i > 0; i--) {
  107. const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
  108. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  109. }
  110. return retArray;
  111. }
  112.  
  113. // lib/config.ts
  114. var ConfigManager = class {
  115. /**
  116. * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
  117. * 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.
  118. *
  119. * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
  120. * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
  121. *
  122. * @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`
  123. * @param options The options for this ConfigManager instance
  124. */
  125. constructor(options) {
  126. __publicField(this, "id");
  127. __publicField(this, "formatVersion");
  128. __publicField(this, "defaultConfig");
  129. __publicField(this, "cachedConfig");
  130. __publicField(this, "migrations");
  131. this.id = options.id;
  132. this.formatVersion = options.formatVersion;
  133. this.defaultConfig = options.defaultConfig;
  134. this.cachedConfig = options.defaultConfig;
  135. this.migrations = options.migrations;
  136. }
  137. /**
  138. * Loads the data saved in persistent storage into the in-memory cache and also returns it.
  139. * Automatically populates persistent storage with default data if it doesn't contain any data yet.
  140. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
  141. */
  142. loadData() {
  143. return __async(this, null, function* () {
  144. try {
  145. const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
  146. let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
  147. if (typeof gmData !== "string") {
  148. yield this.saveDefaultData();
  149. return this.defaultConfig;
  150. }
  151. if (isNaN(gmFmtVer))
  152. yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
  153. let parsed = JSON.parse(gmData);
  154. if (gmFmtVer < this.formatVersion && this.migrations)
  155. parsed = yield this.runMigrations(parsed, gmFmtVer);
  156. return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
  157. } catch (err) {
  158. yield this.saveDefaultData();
  159. return this.defaultConfig;
  160. }
  161. });
  162. }
  163. /** 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). */
  164. getData() {
  165. return this.deepCopy(this.cachedConfig);
  166. }
  167. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  168. setData(data) {
  169. this.cachedConfig = data;
  170. return new Promise((resolve) => __async(this, null, function* () {
  171. yield Promise.allSettled([
  172. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
  173. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  174. ]);
  175. resolve();
  176. }));
  177. }
  178. /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
  179. saveDefaultData() {
  180. return __async(this, null, function* () {
  181. this.cachedConfig = this.defaultConfig;
  182. return new Promise((resolve) => __async(this, null, function* () {
  183. yield Promise.allSettled([
  184. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
  185. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  186. ]);
  187. resolve();
  188. }));
  189. });
  190. }
  191. /**
  192. * Call this method to clear all persistently stored data associated with this ConfigManager instance.
  193. * The in-memory cache will be left untouched, so you may still access the data with `getData()`.
  194. * Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.
  195. *
  196. * ⚠️ This requires the additional directive `@grant GM.deleteValue`
  197. */
  198. deleteConfig() {
  199. return __async(this, null, function* () {
  200. yield Promise.allSettled([
  201. GM.deleteValue(`_uucfg-${this.id}`),
  202. GM.deleteValue(`_uucfgver-${this.id}`)
  203. ]);
  204. });
  205. }
  206. /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
  207. runMigrations(oldData, oldFmtVer) {
  208. return __async(this, null, function* () {
  209. if (!this.migrations)
  210. return oldData;
  211. let newData = oldData;
  212. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  213. let lastFmtVer = oldFmtVer;
  214. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  215. const ver = Number(fmtVer);
  216. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  217. try {
  218. const migRes = migrationFunc(newData);
  219. newData = migRes instanceof Promise ? yield migRes : migRes;
  220. lastFmtVer = oldFmtVer = ver;
  221. } catch (err) {
  222. console.error(`Error while running migration function for format version ${fmtVer}:`, err);
  223. }
  224. }
  225. }
  226. yield Promise.allSettled([
  227. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
  228. GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
  229. ]);
  230. return newData;
  231. });
  232. }
  233. /** Copies a JSON-compatible object and loses its internal references */
  234. deepCopy(obj) {
  235. return JSON.parse(JSON.stringify(obj));
  236. }
  237. };
  238.  
  239. // lib/dom.ts
  240. function getUnsafeWindow() {
  241. try {
  242. return unsafeWindow;
  243. } catch (e) {
  244. return window;
  245. }
  246. }
  247. function insertAfter(beforeElement, afterElement) {
  248. var _a;
  249. (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
  250. return afterElement;
  251. }
  252. function addParent(element, newParent) {
  253. const oldParent = element.parentNode;
  254. if (!oldParent)
  255. throw new Error("Element doesn't have a parent node");
  256. oldParent.replaceChild(newParent, element);
  257. newParent.appendChild(element);
  258. return newParent;
  259. }
  260. function addGlobalStyle(style) {
  261. const styleElem = document.createElement("style");
  262. styleElem.innerHTML = style;
  263. document.head.appendChild(styleElem);
  264. }
  265. function preloadImages(srcUrls, rejects = false) {
  266. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  267. const image = new Image();
  268. image.src = src;
  269. image.addEventListener("load", () => res(image));
  270. image.addEventListener("error", (evt) => rejects && rej(evt));
  271. }));
  272. return Promise.allSettled(promises);
  273. }
  274. function openInNewTab(href) {
  275. const openElem = document.createElement("a");
  276. Object.assign(openElem, {
  277. className: "userutils-open-in-new-tab",
  278. target: "_blank",
  279. rel: "noopener noreferrer",
  280. href
  281. });
  282. openElem.style.display = "none";
  283. document.body.appendChild(openElem);
  284. openElem.click();
  285. setTimeout(openElem.remove, 50);
  286. }
  287. function interceptEvent(eventObject, eventName, predicate) {
  288. if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
  289. Error.stackTraceLimit = 1e3;
  290. }
  291. (function(original) {
  292. eventObject.__proto__.addEventListener = function(...args) {
  293. var _a, _b;
  294. const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
  295. args[1] = function(...a) {
  296. if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
  297. return;
  298. else
  299. return origListener.apply(this, a);
  300. };
  301. original.apply(this, args);
  302. };
  303. })(eventObject.__proto__.addEventListener);
  304. }
  305. function interceptWindowEvent(eventName, predicate) {
  306. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  307. }
  308. function amplifyMedia(mediaElement, multiplier = 1) {
  309. const context = new (window.AudioContext || window.webkitAudioContext)();
  310. const result = {
  311. mediaElement,
  312. amplify: (multiplier2) => {
  313. result.gain.gain.value = multiplier2;
  314. },
  315. getAmpLevel: () => result.gain.gain.value,
  316. context,
  317. source: context.createMediaElementSource(mediaElement),
  318. gain: context.createGain()
  319. };
  320. result.source.connect(result.gain);
  321. result.gain.connect(context.destination);
  322. result.amplify(multiplier);
  323. return result;
  324. }
  325. function isScrollable(element) {
  326. const { overflowX, overflowY } = getComputedStyle(element);
  327. return {
  328. vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
  329. horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
  330. };
  331. }
  332.  
  333. // lib/misc.ts
  334. function autoPlural(word, num) {
  335. if (Array.isArray(num) || num instanceof NodeList)
  336. num = num.length;
  337. return `${word}${num === 1 ? "" : "s"}`;
  338. }
  339. function pauseFor(time) {
  340. return new Promise((res) => {
  341. setTimeout(() => res(), time);
  342. });
  343. }
  344. function debounce(func, timeout = 300) {
  345. let timer;
  346. return function(...args) {
  347. clearTimeout(timer);
  348. timer = setTimeout(() => func.apply(this, args), timeout);
  349. };
  350. }
  351. function fetchAdvanced(_0) {
  352. return __async(this, arguments, function* (url, options = {}) {
  353. const { timeout = 1e4 } = options;
  354. const controller = new AbortController();
  355. const id = setTimeout(() => controller.abort(), timeout);
  356. const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
  357. signal: controller.signal
  358. }));
  359. clearTimeout(id);
  360. return res;
  361. });
  362. }
  363.  
  364. // lib/onSelector.ts
  365. var selectorMap = /* @__PURE__ */ new Map();
  366. function onSelector(selector, options) {
  367. let selectorMapItems = [];
  368. if (selectorMap.has(selector))
  369. selectorMapItems = selectorMap.get(selector);
  370. selectorMapItems.push(options);
  371. selectorMap.set(selector, selectorMapItems);
  372. checkSelectorExists(selector, selectorMapItems);
  373. }
  374. function removeOnSelector(selector) {
  375. return selectorMap.delete(selector);
  376. }
  377. function checkSelectorExists(selector, options) {
  378. const deleteIndices = [];
  379. options.forEach((option, i) => {
  380. try {
  381. const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
  382. if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
  383. option.listener(elements);
  384. if (!option.continuous)
  385. deleteIndices.push(i);
  386. }
  387. } catch (err) {
  388. console.error(`Couldn't call listener for selector '${selector}'`, err);
  389. }
  390. });
  391. if (deleteIndices.length > 0) {
  392. const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
  393. if (newOptsArray.length === 0)
  394. selectorMap.delete(selector);
  395. else {
  396. selectorMap.set(selector, newOptsArray);
  397. }
  398. }
  399. }
  400. function initOnSelector(options = {}) {
  401. const observer = new MutationObserver(() => {
  402. for (const [selector, options2] of selectorMap.entries())
  403. checkSelectorExists(selector, options2);
  404. });
  405. observer.observe(document.body, __spreadValues({
  406. subtree: true,
  407. childList: true
  408. }, options));
  409. }
  410. function getSelectorMap() {
  411. return selectorMap;
  412. }
  413.  
  414. exports.ConfigManager = ConfigManager;
  415. exports.addGlobalStyle = addGlobalStyle;
  416. exports.addParent = addParent;
  417. exports.amplifyMedia = amplifyMedia;
  418. exports.autoPlural = autoPlural;
  419. exports.clamp = clamp;
  420. exports.debounce = debounce;
  421. exports.fetchAdvanced = fetchAdvanced;
  422. exports.getSelectorMap = getSelectorMap;
  423. exports.getUnsafeWindow = getUnsafeWindow;
  424. exports.initOnSelector = initOnSelector;
  425. exports.insertAfter = insertAfter;
  426. exports.interceptEvent = interceptEvent;
  427. exports.interceptWindowEvent = interceptWindowEvent;
  428. exports.isScrollable = isScrollable;
  429. exports.mapRange = mapRange;
  430. exports.onSelector = onSelector;
  431. exports.openInNewTab = openInNewTab;
  432. exports.pauseFor = pauseFor;
  433. exports.preloadImages = preloadImages;
  434. exports.randRange = randRange;
  435. exports.randomItem = randomItem;
  436. exports.randomItemIndex = randomItemIndex;
  437. exports.randomizeArray = randomizeArray;
  438. exports.removeOnSelector = removeOnSelector;
  439. exports.takeRandomItem = takeRandomItem;
  440.  
  441. return exports;
  442.  
  443. })({});