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-11-21 提交的版本,檢視 最新版本

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

  1. // ==UserScript==
  2. // @namespace https://github.com/Sv443-Network/UserUtils
  3. // @exclude *
  4. // @author Sv443
  5. // @supportURL https://github.com/Sv443-Network/UserUtils/issues
  6. // @homepageURL https://github.com/Sv443-Network/UserUtils#readme
  7. // @supportURL https://github.com/Sv443-Network/UserUtils/issues
  8.  
  9. // ==UserLibrary==
  10. // @name UserUtils
  11. // @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
  12. // @version 4.0.0
  13. // @license MIT
  14. // @copyright Sv443 (https://github.com/Sv443)
  15.  
  16. // ==/UserScript==
  17. // ==/UserLibrary==
  18.  
  19. // ==OpenUserJS==
  20. // @author Sv443
  21. // ==/OpenUserJS==
  22.  
  23. var UserUtils = (function (exports) {
  24. var __defProp = Object.defineProperty;
  25. var __defProps = Object.defineProperties;
  26. var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  27. var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  28. var __hasOwnProp = Object.prototype.hasOwnProperty;
  29. var __propIsEnum = Object.prototype.propertyIsEnumerable;
  30. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  31. var __spreadValues = (a, b) => {
  32. for (var prop in b || (b = {}))
  33. if (__hasOwnProp.call(b, prop))
  34. __defNormalProp(a, prop, b[prop]);
  35. if (__getOwnPropSymbols)
  36. for (var prop of __getOwnPropSymbols(b)) {
  37. if (__propIsEnum.call(b, prop))
  38. __defNormalProp(a, prop, b[prop]);
  39. }
  40. return a;
  41. };
  42. var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  43. var __publicField = (obj, key, value) => {
  44. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  45. return value;
  46. };
  47. var __async = (__this, __arguments, generator) => {
  48. return new Promise((resolve, reject) => {
  49. var fulfilled = (value) => {
  50. try {
  51. step(generator.next(value));
  52. } catch (e) {
  53. reject(e);
  54. }
  55. };
  56. var rejected = (value) => {
  57. try {
  58. step(generator.throw(value));
  59. } catch (e) {
  60. reject(e);
  61. }
  62. };
  63. var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
  64. step((generator = generator.apply(__this, __arguments)).next());
  65. });
  66. };
  67.  
  68. // lib/math.ts
  69. function clamp(value, min, max) {
  70. return Math.max(Math.min(value, max), min);
  71. }
  72. function mapRange(value, range1min, range1max, range2min, range2max) {
  73. if (Number(range1min) === 0 && Number(range2min) === 0)
  74. return value * (range2max / range1max);
  75. return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
  76. }
  77. function randRange(...args) {
  78. let min, max;
  79. if (typeof args[0] === "number" && typeof args[1] === "number")
  80. [min, max] = args;
  81. else if (typeof args[0] === "number" && typeof args[1] !== "number") {
  82. min = 0;
  83. [max] = args;
  84. } else
  85. throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
  86. min = Number(min);
  87. max = Number(max);
  88. if (isNaN(min) || isNaN(max))
  89. return NaN;
  90. if (min > max)
  91. throw new TypeError(`Parameter "min" can't be bigger than "max"`);
  92. return Math.floor(Math.random() * (max - min + 1)) + min;
  93. }
  94. function randomId(length = 16, radix = 16) {
  95. const arr = new Uint8Array(length);
  96. crypto.getRandomValues(arr);
  97. return Array.from(
  98. arr,
  99. (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
  100. ).join("");
  101. }
  102.  
  103. // lib/array.ts
  104. function randomItem(array) {
  105. return randomItemIndex(array)[0];
  106. }
  107. function randomItemIndex(array) {
  108. if (array.length === 0)
  109. return [void 0, void 0];
  110. const idx = randRange(array.length - 1);
  111. return [array[idx], idx];
  112. }
  113. function takeRandomItem(arr) {
  114. const [itm, idx] = randomItemIndex(arr);
  115. if (idx === void 0)
  116. return void 0;
  117. arr.splice(idx, 1);
  118. return itm;
  119. }
  120. function randomizeArray(array) {
  121. const retArray = [...array];
  122. if (array.length === 0)
  123. return array;
  124. for (let i = retArray.length - 1; i > 0; i--) {
  125. const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
  126. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  127. }
  128. return retArray;
  129. }
  130.  
  131. // lib/ConfigManager.ts
  132. var ConfigManager = class {
  133. /**
  134. * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
  135. * 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.
  136. *
  137. * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
  138. * ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
  139. *
  140. * @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`
  141. * @param options The options for this ConfigManager instance
  142. */
  143. constructor(options) {
  144. __publicField(this, "id");
  145. __publicField(this, "formatVersion");
  146. __publicField(this, "defaultConfig");
  147. __publicField(this, "cachedConfig");
  148. __publicField(this, "migrations");
  149. this.id = options.id;
  150. this.formatVersion = options.formatVersion;
  151. this.defaultConfig = options.defaultConfig;
  152. this.cachedConfig = options.defaultConfig;
  153. this.migrations = options.migrations;
  154. }
  155. /**
  156. * Loads the data saved in persistent storage into the in-memory cache and also returns it.
  157. * Automatically populates persistent storage with default data if it doesn't contain any data yet.
  158. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
  159. */
  160. loadData() {
  161. return __async(this, null, function* () {
  162. try {
  163. const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
  164. let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
  165. if (typeof gmData !== "string") {
  166. yield this.saveDefaultData();
  167. return this.defaultConfig;
  168. }
  169. if (isNaN(gmFmtVer))
  170. yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
  171. let parsed = JSON.parse(gmData);
  172. if (gmFmtVer < this.formatVersion && this.migrations)
  173. parsed = yield this.runMigrations(parsed, gmFmtVer);
  174. return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
  175. } catch (err) {
  176. yield this.saveDefaultData();
  177. return this.defaultConfig;
  178. }
  179. });
  180. }
  181. /**
  182. * Returns a copy of the data from the in-memory cache.
  183. * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
  184. */
  185. getData() {
  186. return this.deepCopy(this.cachedConfig);
  187. }
  188. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  189. setData(data) {
  190. this.cachedConfig = data;
  191. return new Promise((resolve) => __async(this, null, function* () {
  192. yield Promise.all([
  193. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
  194. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  195. ]);
  196. resolve();
  197. }));
  198. }
  199. /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
  200. saveDefaultData() {
  201. return __async(this, null, function* () {
  202. this.cachedConfig = this.defaultConfig;
  203. return new Promise((resolve) => __async(this, null, function* () {
  204. yield Promise.all([
  205. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
  206. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  207. ]);
  208. resolve();
  209. }));
  210. });
  211. }
  212. /**
  213. * Call this method to clear all persistently stored data associated with this ConfigManager instance.
  214. * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
  215. * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
  216. *
  217. * ⚠️ This requires the additional directive `@grant GM.deleteValue`
  218. */
  219. deleteConfig() {
  220. return __async(this, null, function* () {
  221. yield Promise.all([
  222. GM.deleteValue(`_uucfg-${this.id}`),
  223. GM.deleteValue(`_uucfgver-${this.id}`)
  224. ]);
  225. });
  226. }
  227. /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
  228. runMigrations(oldData, oldFmtVer) {
  229. return __async(this, null, function* () {
  230. if (!this.migrations)
  231. return oldData;
  232. let newData = oldData;
  233. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  234. let lastFmtVer = oldFmtVer;
  235. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  236. const ver = Number(fmtVer);
  237. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  238. try {
  239. const migRes = migrationFunc(newData);
  240. newData = migRes instanceof Promise ? yield migRes : migRes;
  241. lastFmtVer = oldFmtVer = ver;
  242. } catch (err) {
  243. console.error(`Error while running migration function for format version ${fmtVer}:`, err);
  244. }
  245. }
  246. }
  247. yield Promise.all([
  248. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
  249. GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
  250. ]);
  251. return newData;
  252. });
  253. }
  254. /** Copies a JSON-compatible object and loses its internal references */
  255. deepCopy(obj) {
  256. return JSON.parse(JSON.stringify(obj));
  257. }
  258. };
  259.  
  260. // lib/dom.ts
  261. function getUnsafeWindow() {
  262. try {
  263. return unsafeWindow;
  264. } catch (e) {
  265. return window;
  266. }
  267. }
  268. function insertAfter(beforeElement, afterElement) {
  269. var _a;
  270. (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
  271. return afterElement;
  272. }
  273. function addParent(element, newParent) {
  274. const oldParent = element.parentNode;
  275. if (!oldParent)
  276. throw new Error("Element doesn't have a parent node");
  277. oldParent.replaceChild(newParent, element);
  278. newParent.appendChild(element);
  279. return newParent;
  280. }
  281. function addGlobalStyle(style) {
  282. const styleElem = document.createElement("style");
  283. styleElem.innerHTML = style;
  284. document.head.appendChild(styleElem);
  285. }
  286. function preloadImages(srcUrls, rejects = false) {
  287. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  288. const image = new Image();
  289. image.src = src;
  290. image.addEventListener("load", () => res(image));
  291. image.addEventListener("error", (evt) => rejects && rej(evt));
  292. }));
  293. return Promise.allSettled(promises);
  294. }
  295. function openInNewTab(href) {
  296. const openElem = document.createElement("a");
  297. Object.assign(openElem, {
  298. className: "userutils-open-in-new-tab",
  299. target: "_blank",
  300. rel: "noopener noreferrer",
  301. href
  302. });
  303. openElem.style.display = "none";
  304. document.body.appendChild(openElem);
  305. openElem.click();
  306. setTimeout(openElem.remove, 50);
  307. }
  308. function interceptEvent(eventObject, eventName, predicate = () => true) {
  309. if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
  310. Error.stackTraceLimit = 1e3;
  311. }
  312. (function(original) {
  313. eventObject.__proto__.addEventListener = function(...args) {
  314. var _a, _b;
  315. const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
  316. args[1] = function(...a) {
  317. if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
  318. return;
  319. else
  320. return origListener.apply(this, a);
  321. };
  322. original.apply(this, args);
  323. };
  324. })(eventObject.__proto__.addEventListener);
  325. }
  326. function interceptWindowEvent(eventName, predicate = () => true) {
  327. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  328. }
  329. function isScrollable(element) {
  330. const { overflowX, overflowY } = getComputedStyle(element);
  331. return {
  332. vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
  333. horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
  334. };
  335. }
  336.  
  337. // lib/misc.ts
  338. function autoPlural(word, num) {
  339. if (Array.isArray(num) || num instanceof NodeList)
  340. num = num.length;
  341. return `${word}${num === 1 ? "" : "s"}`;
  342. }
  343. function pauseFor(time) {
  344. return new Promise((res) => {
  345. setTimeout(() => res(), time);
  346. });
  347. }
  348. function debounce(func, timeout = 300) {
  349. let timer;
  350. return function(...args) {
  351. clearTimeout(timer);
  352. timer = setTimeout(() => func.apply(this, args), timeout);
  353. };
  354. }
  355. function fetchAdvanced(_0) {
  356. return __async(this, arguments, function* (url, options = {}) {
  357. const { timeout = 1e4 } = options;
  358. const controller = new AbortController();
  359. const id = setTimeout(() => controller.abort(), timeout);
  360. const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
  361. signal: controller.signal
  362. }));
  363. clearTimeout(id);
  364. return res;
  365. });
  366. }
  367. function insertValues(input, ...values) {
  368. return input.replace(/%\d/gm, (match) => {
  369. var _a, _b;
  370. const argIndex = Number(match.substring(1)) - 1;
  371. return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
  372. });
  373. }
  374. function compress(input, compressionFormat, outputType = "base64") {
  375. return __async(this, null, function* () {
  376. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  377. const comp = new CompressionStream(compressionFormat);
  378. const writer = comp.writable.getWriter();
  379. writer.write(byteArray);
  380. writer.close();
  381. const buf = yield new Response(comp.readable).arrayBuffer();
  382. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  383. });
  384. }
  385. function decompress(input, compressionFormat, outputType = "string") {
  386. return __async(this, null, function* () {
  387. const byteArray = typeof input === "string" ? str2ab(input) : input;
  388. const decomp = new DecompressionStream(compressionFormat);
  389. const writer = decomp.writable.getWriter();
  390. writer.write(byteArray);
  391. writer.close();
  392. const buf = yield new Response(decomp.readable).arrayBuffer();
  393. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  394. });
  395. }
  396. function ab2str(buf) {
  397. return getUnsafeWindow().btoa(
  398. new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
  399. );
  400. }
  401. function str2ab(str) {
  402. return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
  403. }
  404.  
  405. // lib/SelectorObserver.ts
  406. var SelectorObserver = class {
  407. constructor(baseElement, options = {}) {
  408. __publicField(this, "enabled", false);
  409. __publicField(this, "baseElement");
  410. __publicField(this, "observer");
  411. __publicField(this, "observerOptions");
  412. __publicField(this, "listenerMap");
  413. this.baseElement = baseElement;
  414. this.listenerMap = /* @__PURE__ */ new Map();
  415. this.observer = new MutationObserver(() => this.checkAllSelectors());
  416. this.observerOptions = __spreadValues({
  417. childList: true,
  418. subtree: true
  419. }, options);
  420. }
  421. checkAllSelectors() {
  422. for (const [selector, listeners] of this.listenerMap.entries())
  423. this.checkSelector(selector, listeners);
  424. }
  425. checkSelector(selector, listeners) {
  426. var _a;
  427. if (!this.enabled)
  428. return;
  429. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  430. if (!baseElement)
  431. return;
  432. const all = listeners.some((listener) => listener.all);
  433. const one = listeners.some((listener) => !listener.all);
  434. const allElements = all ? baseElement.querySelectorAll(selector) : null;
  435. const oneElement = one ? baseElement.querySelector(selector) : null;
  436. for (const options of listeners) {
  437. if (options.all) {
  438. if (allElements && allElements.length > 0) {
  439. options.listener(allElements);
  440. if (!options.continuous)
  441. this.removeListener(selector, options);
  442. }
  443. } else {
  444. if (oneElement) {
  445. options.listener(oneElement);
  446. if (!options.continuous)
  447. this.removeListener(selector, options);
  448. }
  449. }
  450. if (((_a = this.listenerMap.get(selector)) == null ? void 0 : _a.length) === 0)
  451. this.listenerMap.delete(selector);
  452. }
  453. }
  454. debounce(func, time) {
  455. let timeout;
  456. return function(...args) {
  457. clearTimeout(timeout);
  458. timeout = setTimeout(() => func.apply(this, args), time);
  459. };
  460. }
  461. /**
  462. * Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
  463. * @param selector The selector to observe
  464. * @param options Options for the selector observation
  465. * @param options.listener Gets called whenever the selector was found in the DOM
  466. * @param [options.all] Whether to use `querySelectorAll()` instead - default is false
  467. * @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
  468. * @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
  469. */
  470. addListener(selector, options) {
  471. options = __spreadValues({ all: false, continuous: false, debounce: 0 }, options);
  472. if (options.debounce && options.debounce > 0 || this.observerOptions.defaultDebounce && this.observerOptions.defaultDebounce > 0) {
  473. options.listener = this.debounce(
  474. options.listener,
  475. options.debounce || this.observerOptions.defaultDebounce
  476. );
  477. }
  478. if (this.listenerMap.has(selector))
  479. this.listenerMap.get(selector).push(options);
  480. else
  481. this.listenerMap.set(selector, [options]);
  482. this.checkSelector(selector, [options]);
  483. }
  484. /** Disables the observation of the child elements */
  485. disable() {
  486. if (!this.enabled)
  487. return;
  488. this.enabled = false;
  489. this.observer.disconnect();
  490. }
  491. /**
  492. * Enables or reenables the observation of the child elements.
  493. * @param immediatelyCheckSelectors Whether to immediately check if all previously registered selectors exist (default is true)
  494. * @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found)
  495. */
  496. enable(immediatelyCheckSelectors = true) {
  497. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  498. if (this.enabled || !baseElement)
  499. return false;
  500. this.enabled = true;
  501. this.observer.observe(baseElement, this.observerOptions);
  502. if (immediatelyCheckSelectors)
  503. this.checkAllSelectors();
  504. return true;
  505. }
  506. /** Returns whether the observation of the child elements is currently enabled */
  507. isEnabled() {
  508. return this.enabled;
  509. }
  510. /** Removes all listeners that have been registered with {@linkcode addListener()} */
  511. clearListeners() {
  512. this.listenerMap.clear();
  513. }
  514. /**
  515. * Removes all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()}
  516. * @returns Returns true when all listeners for the associated selector were found and removed, false otherwise
  517. */
  518. removeAllListeners(selector) {
  519. return this.listenerMap.delete(selector);
  520. }
  521. /**
  522. * Removes a single listener for the given {@linkcode selector} and {@linkcode options} that has been registered with {@linkcode addListener()}
  523. * @returns Returns true when the listener was found and removed, false otherwise
  524. */
  525. removeListener(selector, options) {
  526. const listeners = this.listenerMap.get(selector);
  527. if (!listeners)
  528. return false;
  529. const index = listeners.indexOf(options);
  530. if (index > -1) {
  531. listeners.splice(index, 1);
  532. return true;
  533. }
  534. return false;
  535. }
  536. /** Returns all listeners that have been registered with {@linkcode addListener()} */
  537. getAllListeners() {
  538. return this.listenerMap;
  539. }
  540. /** Returns all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} */
  541. getListeners(selector) {
  542. return this.listenerMap.get(selector);
  543. }
  544. };
  545.  
  546. // lib/translation.ts
  547. var trans = {};
  548. var curLang;
  549. function tr(key, ...args) {
  550. var _a;
  551. if (!curLang)
  552. return key;
  553. const trText = (_a = trans[curLang]) == null ? void 0 : _a[key];
  554. if (!trText)
  555. return key;
  556. if (args.length > 0 && trText.match(/%\d/)) {
  557. return insertValues(trText, ...args);
  558. }
  559. return trText;
  560. }
  561. tr.addLanguage = (language, translations) => {
  562. trans[language] = translations;
  563. };
  564. tr.setLanguage = (language) => {
  565. curLang = language;
  566. };
  567. tr.getLanguage = () => {
  568. return curLang;
  569. };
  570.  
  571. exports.ConfigManager = ConfigManager;
  572. exports.SelectorObserver = SelectorObserver;
  573. exports.addGlobalStyle = addGlobalStyle;
  574. exports.addParent = addParent;
  575. exports.autoPlural = autoPlural;
  576. exports.clamp = clamp;
  577. exports.compress = compress;
  578. exports.debounce = debounce;
  579. exports.decompress = decompress;
  580. exports.fetchAdvanced = fetchAdvanced;
  581. exports.getUnsafeWindow = getUnsafeWindow;
  582. exports.insertAfter = insertAfter;
  583. exports.insertValues = insertValues;
  584. exports.interceptEvent = interceptEvent;
  585. exports.interceptWindowEvent = interceptWindowEvent;
  586. exports.isScrollable = isScrollable;
  587. exports.mapRange = mapRange;
  588. exports.openInNewTab = openInNewTab;
  589. exports.pauseFor = pauseFor;
  590. exports.preloadImages = preloadImages;
  591. exports.randRange = randRange;
  592. exports.randomId = randomId;
  593. exports.randomItem = randomItem;
  594. exports.randomItemIndex = randomItemIndex;
  595. exports.randomizeArray = randomizeArray;
  596. exports.takeRandomItem = takeRandomItem;
  597. exports.tr = tr;
  598.  
  599. return exports;
  600.  
  601. })({});