UserUtils

Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more

目前為 2024-11-08 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/472956/1480265/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
  7.  
  8. // ==UserLibrary==
  9. // @name UserUtils
  10. // @description Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more
  11. // @version 8.3.0
  12. // @license MIT
  13. // @copyright Sv443 (https://github.com/Sv443)
  14.  
  15. // ==/UserScript==
  16. // ==/UserLibrary==
  17.  
  18. // ==OpenUserJS==
  19. // @author Sv443
  20. // ==/OpenUserJS==
  21.  
  22. var UserUtils = (function (exports) {
  23. var __defProp = Object.defineProperty;
  24. var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  25. var __hasOwnProp = Object.prototype.hasOwnProperty;
  26. var __propIsEnum = Object.prototype.propertyIsEnumerable;
  27. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  28. var __spreadValues = (a, b) => {
  29. for (var prop in b || (b = {}))
  30. if (__hasOwnProp.call(b, prop))
  31. __defNormalProp(a, prop, b[prop]);
  32. if (__getOwnPropSymbols)
  33. for (var prop of __getOwnPropSymbols(b)) {
  34. if (__propIsEnum.call(b, prop))
  35. __defNormalProp(a, prop, b[prop]);
  36. }
  37. return a;
  38. };
  39. var __objRest = (source, exclude) => {
  40. var target = {};
  41. for (var prop in source)
  42. if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
  43. target[prop] = source[prop];
  44. if (source != null && __getOwnPropSymbols)
  45. for (var prop of __getOwnPropSymbols(source)) {
  46. if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
  47. target[prop] = source[prop];
  48. }
  49. return target;
  50. };
  51. var __publicField = (obj, key, value) => {
  52. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  53. return value;
  54. };
  55. var __async = (__this, __arguments, generator) => {
  56. return new Promise((resolve, reject) => {
  57. var fulfilled = (value) => {
  58. try {
  59. step(generator.next(value));
  60. } catch (e) {
  61. reject(e);
  62. }
  63. };
  64. var rejected = (value) => {
  65. try {
  66. step(generator.throw(value));
  67. } catch (e) {
  68. reject(e);
  69. }
  70. };
  71. var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
  72. step((generator = generator.apply(__this, __arguments)).next());
  73. });
  74. };
  75.  
  76. // lib/math.ts
  77. function clamp(value, min, max) {
  78. return Math.max(Math.min(value, max), min);
  79. }
  80. function mapRange(value, range1min, range1max, range2min, range2max) {
  81. if (typeof range2min === "undefined" || range2max === void 0) {
  82. range2max = range1max;
  83. range2min = 0;
  84. range1max = range1min;
  85. range1min = 0;
  86. }
  87. if (Number(range1min) === 0 && Number(range2min) === 0)
  88. return value * (range2max / range1max);
  89. return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
  90. }
  91. function randRange(...args) {
  92. let min, max, enhancedEntropy = false;
  93. if (typeof args[0] === "number" && typeof args[1] === "number")
  94. [min, max] = args;
  95. else if (typeof args[0] === "number" && typeof args[1] !== "number") {
  96. min = 0;
  97. [max] = args;
  98. } else
  99. throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
  100. if (typeof args[2] === "boolean")
  101. enhancedEntropy = args[2];
  102. else if (typeof args[1] === "boolean")
  103. enhancedEntropy = args[1];
  104. min = Number(min);
  105. max = Number(max);
  106. if (isNaN(min) || isNaN(max))
  107. return NaN;
  108. if (min > max)
  109. throw new TypeError(`Parameter "min" can't be bigger than "max"`);
  110. if (enhancedEntropy) {
  111. const uintArr = new Uint8Array(1);
  112. crypto.getRandomValues(uintArr);
  113. return Number(Array.from(
  114. uintArr,
  115. (v) => Math.round(mapRange(v, 0, 255, min, max)).toString(10).substring(0, 1)
  116. ).join(""));
  117. } else
  118. return Math.floor(Math.random() * (max - min + 1)) + min;
  119. }
  120.  
  121. // lib/array.ts
  122. function randomItem(array) {
  123. return randomItemIndex(array)[0];
  124. }
  125. function randomItemIndex(array) {
  126. if (array.length === 0)
  127. return [void 0, void 0];
  128. const idx = randRange(array.length - 1);
  129. return [array[idx], idx];
  130. }
  131. function takeRandomItem(arr) {
  132. const [itm, idx] = randomItemIndex(arr);
  133. if (idx === void 0)
  134. return void 0;
  135. arr.splice(idx, 1);
  136. return itm;
  137. }
  138. function randomizeArray(array) {
  139. const retArray = [...array];
  140. if (array.length === 0)
  141. return retArray;
  142. for (let i = retArray.length - 1; i > 0; i--) {
  143. const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
  144. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  145. }
  146. return retArray;
  147. }
  148.  
  149. // lib/colors.ts
  150. function hexToRgb(hex) {
  151. hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
  152. const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : void 0;
  153. if (!isNaN(Number(a)))
  154. hex = hex.slice(0, -(hex.length / 4));
  155. if (hex.length === 3 || hex.length === 4)
  156. hex = hex.split("").map((c) => c + c).join("");
  157. const bigint = parseInt(hex, 16);
  158. const r = bigint >> 16 & 255;
  159. const g = bigint >> 8 & 255;
  160. const b = bigint & 255;
  161. return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : void 0];
  162. }
  163. function rgbToHex(red, green, blue, alpha, withHash = true, upperCase = false) {
  164. const toHexVal = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[upperCase ? "toUpperCase" : "toLowerCase"]();
  165. return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`;
  166. }
  167. function lightenColor(color, percent, upperCase = false) {
  168. return darkenColor(color, percent * -1, upperCase);
  169. }
  170. function darkenColor(color, percent, upperCase = false) {
  171. var _a;
  172. color = color.trim();
  173. const darkenRgb = (r2, g2, b2, percent2) => {
  174. r2 = Math.max(0, Math.min(255, r2 - r2 * percent2 / 100));
  175. g2 = Math.max(0, Math.min(255, g2 - g2 * percent2 / 100));
  176. b2 = Math.max(0, Math.min(255, b2 - b2 * percent2 / 100));
  177. return [r2, g2, b2];
  178. };
  179. let r, g, b, a;
  180. const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
  181. if (isHexCol)
  182. [r, g, b, a] = hexToRgb(color);
  183. else if (color.startsWith("rgb")) {
  184. const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? void 0 : _a.map(Number);
  185. if (!rgbValues)
  186. throw new Error("Invalid RGB/RGBA color format");
  187. [r, g, b, a] = rgbValues;
  188. } else
  189. throw new Error("Unsupported color format");
  190. [r, g, b] = darkenRgb(r, g, b, percent);
  191. if (isHexCol)
  192. return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
  193. else if (color.startsWith("rgba"))
  194. return `rgba(${r}, ${g}, ${b}, ${a != null ? a : NaN})`;
  195. else if (color.startsWith("rgb"))
  196. return `rgb(${r}, ${g}, ${b})`;
  197. else
  198. throw new Error("Unsupported color format");
  199. }
  200.  
  201. // lib/dom.ts
  202. function getUnsafeWindow() {
  203. try {
  204. return unsafeWindow;
  205. } catch (e) {
  206. return window;
  207. }
  208. }
  209. function addParent(element, newParent) {
  210. const oldParent = element.parentNode;
  211. if (!oldParent)
  212. throw new Error("Element doesn't have a parent node");
  213. oldParent.replaceChild(newParent, element);
  214. newParent.appendChild(element);
  215. return newParent;
  216. }
  217. function addGlobalStyle(style) {
  218. const styleElem = document.createElement("style");
  219. setInnerHtmlUnsafe(styleElem, style);
  220. document.head.appendChild(styleElem);
  221. return styleElem;
  222. }
  223. function preloadImages(srcUrls, rejects = false) {
  224. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  225. const image = new Image();
  226. image.src = src;
  227. image.addEventListener("load", () => res(image));
  228. image.addEventListener("error", (evt) => rejects && rej(evt));
  229. }));
  230. return Promise.allSettled(promises);
  231. }
  232. function openInNewTab(href, background) {
  233. try {
  234. GM.openInTab(href, background);
  235. } catch (e) {
  236. const openElem = document.createElement("a");
  237. Object.assign(openElem, {
  238. className: "userutils-open-in-new-tab",
  239. target: "_blank",
  240. rel: "noopener noreferrer",
  241. href
  242. });
  243. openElem.style.display = "none";
  244. document.body.appendChild(openElem);
  245. openElem.click();
  246. setTimeout(openElem.remove, 50);
  247. }
  248. }
  249. function interceptEvent(eventObject, eventName, predicate = () => true) {
  250. Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100);
  251. if (isNaN(Error.stackTraceLimit))
  252. Error.stackTraceLimit = 100;
  253. (function(original) {
  254. eventObject.__proto__.addEventListener = function(...args) {
  255. var _a, _b;
  256. const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
  257. args[1] = function(...a) {
  258. if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
  259. return;
  260. else
  261. return origListener.apply(this, a);
  262. };
  263. original.apply(this, args);
  264. };
  265. })(eventObject.__proto__.addEventListener);
  266. }
  267. function interceptWindowEvent(eventName, predicate = () => true) {
  268. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  269. }
  270. function isScrollable(element) {
  271. const { overflowX, overflowY } = getComputedStyle(element);
  272. return {
  273. vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
  274. horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
  275. };
  276. }
  277. function observeElementProp(element, property, callback) {
  278. const elementPrototype = Object.getPrototypeOf(element);
  279. if (elementPrototype.hasOwnProperty(property)) {
  280. const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
  281. Object.defineProperty(element, property, {
  282. get: function() {
  283. var _a;
  284. return (_a = descriptor == null ? void 0 : descriptor.get) == null ? void 0 : _a.apply(this, arguments);
  285. },
  286. set: function() {
  287. var _a;
  288. const oldValue = this[property];
  289. (_a = descriptor == null ? void 0 : descriptor.set) == null ? void 0 : _a.apply(this, arguments);
  290. const newValue = this[property];
  291. if (typeof callback === "function") {
  292. callback.bind(this, oldValue, newValue);
  293. }
  294. return newValue;
  295. }
  296. });
  297. }
  298. }
  299. function getSiblingsFrame(refElement, siblingAmount, refElementAlignment = "center-top", includeRef = true) {
  300. var _a, _b;
  301. const siblings = [...(_b = (_a = refElement.parentNode) == null ? void 0 : _a.childNodes) != null ? _b : []];
  302. const elemSiblIdx = siblings.indexOf(refElement);
  303. if (elemSiblIdx === -1)
  304. throw new Error("Element doesn't have a parent node");
  305. if (refElementAlignment === "top")
  306. return [...siblings.slice(elemSiblIdx + Number(!includeRef), elemSiblIdx + siblingAmount + Number(!includeRef))];
  307. else if (refElementAlignment.startsWith("center-")) {
  308. const halfAmount = (refElementAlignment === "center-bottom" ? Math.ceil : Math.floor)(siblingAmount / 2);
  309. const startIdx = Math.max(0, elemSiblIdx - halfAmount);
  310. const topOffset = Number(refElementAlignment === "center-top" && siblingAmount % 2 === 0 && includeRef);
  311. const btmOffset = Number(refElementAlignment === "center-bottom" && siblingAmount % 2 !== 0 && includeRef);
  312. const startIdxWithOffset = startIdx + topOffset + btmOffset;
  313. return [
  314. ...siblings.filter((_, idx) => includeRef || idx !== elemSiblIdx).slice(startIdxWithOffset, startIdxWithOffset + siblingAmount)
  315. ];
  316. } else if (refElementAlignment === "bottom")
  317. return [...siblings.slice(elemSiblIdx - siblingAmount + Number(includeRef), elemSiblIdx + Number(includeRef))];
  318. return [];
  319. }
  320. var ttPolicy;
  321. function setInnerHtmlUnsafe(element, html) {
  322. var _a, _b, _c;
  323. if (!ttPolicy && typeof ((_a = window == null ? void 0 : window.trustedTypes) == null ? void 0 : _a.createPolicy) === "function") {
  324. ttPolicy = window.trustedTypes.createPolicy("_uu_set_innerhtml_unsafe", {
  325. createHTML: (unsafeHtml) => unsafeHtml
  326. });
  327. }
  328. element.innerHTML = (_c = (_b = ttPolicy == null ? void 0 : ttPolicy.createHTML) == null ? void 0 : _b.call(ttPolicy, html)) != null ? _c : html;
  329. return element;
  330. }
  331.  
  332. // lib/crypto.ts
  333. function compress(input, compressionFormat, outputType = "string") {
  334. return __async(this, null, function* () {
  335. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  336. const comp = new CompressionStream(compressionFormat);
  337. const writer = comp.writable.getWriter();
  338. writer.write(byteArray);
  339. writer.close();
  340. const buf = yield new Response(comp.readable).arrayBuffer();
  341. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  342. });
  343. }
  344. function decompress(input, compressionFormat, outputType = "string") {
  345. return __async(this, null, function* () {
  346. const byteArray = typeof input === "string" ? str2ab(input) : input;
  347. const decomp = new DecompressionStream(compressionFormat);
  348. const writer = decomp.writable.getWriter();
  349. writer.write(byteArray);
  350. writer.close();
  351. const buf = yield new Response(decomp.readable).arrayBuffer();
  352. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  353. });
  354. }
  355. function ab2str(buf) {
  356. return getUnsafeWindow().btoa(
  357. new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
  358. );
  359. }
  360. function str2ab(str) {
  361. return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
  362. }
  363. function computeHash(input, algorithm = "SHA-256") {
  364. return __async(this, null, function* () {
  365. let data;
  366. if (typeof input === "string") {
  367. const encoder = new TextEncoder();
  368. data = encoder.encode(input);
  369. } else
  370. data = input;
  371. const hashBuffer = yield crypto.subtle.digest(algorithm, data);
  372. const hashArray = Array.from(new Uint8Array(hashBuffer));
  373. const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
  374. return hashHex;
  375. });
  376. }
  377. function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
  378. let arr = [];
  379. const caseArr = randomCase ? [0, 1] : [0];
  380. if (enhancedEntropy) {
  381. const uintArr = new Uint8Array(length);
  382. crypto.getRandomValues(uintArr);
  383. arr = Array.from(
  384. uintArr,
  385. (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
  386. );
  387. } else {
  388. arr = Array.from(
  389. { length },
  390. () => Math.floor(Math.random() * radix).toString(radix)
  391. );
  392. }
  393. if (!arr.some((v) => /[a-zA-Z]/.test(v)))
  394. return arr.join("");
  395. return arr.map((v) => caseArr[randRange(0, caseArr.length - 1, enhancedEntropy)] === 1 ? v.toUpperCase() : v).join("");
  396. }
  397.  
  398. // lib/DataStore.ts
  399. var DataStore = class {
  400. /**
  401. * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
  402. * Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
  403. *
  404. * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
  405. * ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
  406. *
  407. * @template TData The type of the data that is saved in persistent storage for the currently set format version (will be automatically inferred from `defaultData` if not provided) - **This has to be a JSON-compatible object!** (no undefined, circular references, etc.)
  408. * @param options The options for this DataStore instance
  409. */
  410. constructor(options) {
  411. __publicField(this, "id");
  412. __publicField(this, "formatVersion");
  413. __publicField(this, "defaultData");
  414. __publicField(this, "encodeData");
  415. __publicField(this, "decodeData");
  416. __publicField(this, "storageMethod");
  417. __publicField(this, "cachedData");
  418. __publicField(this, "migrations");
  419. __publicField(this, "migrateIds", []);
  420. var _a;
  421. this.id = options.id;
  422. this.formatVersion = options.formatVersion;
  423. this.defaultData = options.defaultData;
  424. this.cachedData = options.defaultData;
  425. this.migrations = options.migrations;
  426. if (options.migrateIds)
  427. this.migrateIds = Array.isArray(options.migrateIds) ? options.migrateIds : [options.migrateIds];
  428. this.storageMethod = (_a = options.storageMethod) != null ? _a : "GM";
  429. this.encodeData = options.encodeData;
  430. this.decodeData = options.decodeData;
  431. }
  432. //#region public
  433. /**
  434. * Loads the data saved in persistent storage into the in-memory cache and also returns it.
  435. * Automatically populates persistent storage with default data if it doesn't contain any data yet.
  436. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
  437. */
  438. loadData() {
  439. return __async(this, null, function* () {
  440. try {
  441. if (this.migrateIds.length > 0) {
  442. yield this.migrateId(this.migrateIds);
  443. this.migrateIds = [];
  444. }
  445. const gmData = yield this.getValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultData));
  446. let gmFmtVer = Number(yield this.getValue(`_uucfgver-${this.id}`, NaN));
  447. if (typeof gmData !== "string") {
  448. yield this.saveDefaultData();
  449. return __spreadValues({}, this.defaultData);
  450. }
  451. const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${this.id}`, false));
  452. let saveData = false;
  453. if (isNaN(gmFmtVer)) {
  454. yield this.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
  455. saveData = true;
  456. }
  457. let parsed = yield this.deserializeData(gmData, isEncoded);
  458. if (gmFmtVer < this.formatVersion && this.migrations)
  459. parsed = yield this.runMigrations(parsed, gmFmtVer);
  460. if (saveData)
  461. yield this.setData(parsed);
  462. this.cachedData = __spreadValues({}, parsed);
  463. return this.cachedData;
  464. } catch (err) {
  465. console.warn("Error while parsing JSON data, resetting it to the default value.", err);
  466. yield this.saveDefaultData();
  467. return this.defaultData;
  468. }
  469. });
  470. }
  471. /**
  472. * Returns a copy of the data from the in-memory cache.
  473. * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
  474. * @param deepCopy Whether to return a deep copy of the data (default: `false`) - only necessary if your data object is nested and may have a bigger performance impact if enabled
  475. */
  476. getData(deepCopy = false) {
  477. return deepCopy ? this.deepCopy(this.cachedData) : __spreadValues({}, this.cachedData);
  478. }
  479. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  480. setData(data) {
  481. this.cachedData = data;
  482. const useEncoding = this.encodingEnabled();
  483. return new Promise((resolve) => __async(this, null, function* () {
  484. yield Promise.all([
  485. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(data, useEncoding)),
  486. this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
  487. this.setValue(`_uucfgenc-${this.id}`, useEncoding)
  488. ]);
  489. resolve();
  490. }));
  491. }
  492. /** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
  493. saveDefaultData() {
  494. return __async(this, null, function* () {
  495. this.cachedData = this.defaultData;
  496. const useEncoding = this.encodingEnabled();
  497. return new Promise((resolve) => __async(this, null, function* () {
  498. yield Promise.all([
  499. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(this.defaultData, useEncoding)),
  500. this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
  501. this.setValue(`_uucfgenc-${this.id}`, useEncoding)
  502. ]);
  503. resolve();
  504. }));
  505. });
  506. }
  507. /**
  508. * Call this method to clear all persistently stored data associated with this DataStore instance.
  509. * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
  510. * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
  511. *
  512. * ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"`
  513. */
  514. deleteData() {
  515. return __async(this, null, function* () {
  516. yield Promise.all([
  517. this.deleteValue(`_uucfg-${this.id}`),
  518. this.deleteValue(`_uucfgver-${this.id}`),
  519. this.deleteValue(`_uucfgenc-${this.id}`)
  520. ]);
  521. });
  522. }
  523. /** Returns whether encoding and decoding are enabled for this DataStore instance */
  524. encodingEnabled() {
  525. return Boolean(this.encodeData && this.decodeData);
  526. }
  527. //#region migrations
  528. /**
  529. * Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
  530. * This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
  531. * Though calling this method manually is not necessary, it can be useful if you want to run migrations for special occasions like a user importing potentially outdated data that has been previously exported.
  532. *
  533. * If one of the migrations fails, the data will be reset to the default value if `resetOnError` is set to `true` (default). Otherwise, an error will be thrown and no data will be saved.
  534. */
  535. runMigrations(oldData, oldFmtVer, resetOnError = true) {
  536. return __async(this, null, function* () {
  537. if (!this.migrations)
  538. return oldData;
  539. let newData = oldData;
  540. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  541. let lastFmtVer = oldFmtVer;
  542. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  543. const ver = Number(fmtVer);
  544. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  545. try {
  546. const migRes = migrationFunc(newData);
  547. newData = migRes instanceof Promise ? yield migRes : migRes;
  548. lastFmtVer = oldFmtVer = ver;
  549. } catch (err) {
  550. if (!resetOnError)
  551. throw new Error(`Error while running migration function for format version '${fmtVer}'`);
  552. console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
  553. yield this.saveDefaultData();
  554. return this.getData();
  555. }
  556. }
  557. }
  558. yield Promise.all([
  559. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(newData)),
  560. this.setValue(`_uucfgver-${this.id}`, lastFmtVer),
  561. this.setValue(`_uucfgenc-${this.id}`, this.encodingEnabled())
  562. ]);
  563. return this.cachedData = __spreadValues({}, newData);
  564. });
  565. }
  566. /**
  567. * Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.
  568. * If no data exist for the old ID(s), nothing will be done, but some time may still pass trying to fetch the non-existent data.
  569. */
  570. migrateId(oldIds) {
  571. return __async(this, null, function* () {
  572. const ids = Array.isArray(oldIds) ? oldIds : [oldIds];
  573. yield Promise.all(ids.map((id) => __async(this, null, function* () {
  574. const data = yield this.getValue(`_uucfg-${id}`, JSON.stringify(this.defaultData));
  575. const fmtVer = Number(yield this.getValue(`_uucfgver-${id}`, NaN));
  576. const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${id}`, false));
  577. if (data === void 0 || isNaN(fmtVer))
  578. return;
  579. const parsed = yield this.deserializeData(data, isEncoded);
  580. yield Promise.allSettled([
  581. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(parsed)),
  582. this.setValue(`_uucfgver-${this.id}`, fmtVer),
  583. this.setValue(`_uucfgenc-${this.id}`, isEncoded),
  584. this.deleteValue(`_uucfg-${id}`),
  585. this.deleteValue(`_uucfgver-${id}`),
  586. this.deleteValue(`_uucfgenc-${id}`)
  587. ]);
  588. })));
  589. });
  590. }
  591. //#region serialization
  592. /** Serializes the data using the optional this.encodeData() and returns it as a string */
  593. serializeData(data, useEncoding = true) {
  594. return __async(this, null, function* () {
  595. const stringData = JSON.stringify(data);
  596. if (!this.encodingEnabled() || !useEncoding)
  597. return stringData;
  598. const encRes = this.encodeData(stringData);
  599. if (encRes instanceof Promise)
  600. return yield encRes;
  601. return encRes;
  602. });
  603. }
  604. /** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */
  605. deserializeData(data, useEncoding = true) {
  606. return __async(this, null, function* () {
  607. let decRes = this.encodingEnabled() && useEncoding ? this.decodeData(data) : void 0;
  608. if (decRes instanceof Promise)
  609. decRes = yield decRes;
  610. return JSON.parse(decRes != null ? decRes : data);
  611. });
  612. }
  613. //#region misc
  614. /** Copies a JSON-compatible object and loses all its internal references in the process */
  615. deepCopy(obj) {
  616. return JSON.parse(JSON.stringify(obj));
  617. }
  618. //#region storage
  619. /** Gets a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
  620. getValue(name, defaultValue) {
  621. return __async(this, null, function* () {
  622. var _a, _b;
  623. switch (this.storageMethod) {
  624. case "localStorage":
  625. return (_a = localStorage.getItem(name)) != null ? _a : defaultValue;
  626. case "sessionStorage":
  627. return (_b = sessionStorage.getItem(name)) != null ? _b : defaultValue;
  628. default:
  629. return GM.getValue(name, defaultValue);
  630. }
  631. });
  632. }
  633. /**
  634. * Sets a value in persistent storage - can be overwritten in a subclass if you want to use something other than GM storage.
  635. * The default storage engines will stringify all passed values like numbers or booleans, so be aware of that.
  636. */
  637. setValue(name, value) {
  638. return __async(this, null, function* () {
  639. switch (this.storageMethod) {
  640. case "localStorage":
  641. return localStorage.setItem(name, String(value));
  642. case "sessionStorage":
  643. return sessionStorage.setItem(name, String(value));
  644. default:
  645. return GM.setValue(name, String(value));
  646. }
  647. });
  648. }
  649. /** Deletes a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
  650. deleteValue(name) {
  651. return __async(this, null, function* () {
  652. switch (this.storageMethod) {
  653. case "localStorage":
  654. return localStorage.removeItem(name);
  655. case "sessionStorage":
  656. return sessionStorage.removeItem(name);
  657. default:
  658. return GM.deleteValue(name);
  659. }
  660. });
  661. }
  662. };
  663.  
  664. // lib/DataStoreSerializer.ts
  665. var DataStoreSerializer = class {
  666. constructor(stores, options = {}) {
  667. __publicField(this, "stores");
  668. __publicField(this, "options");
  669. if (!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
  670. throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
  671. this.stores = stores;
  672. this.options = __spreadValues({
  673. addChecksum: true,
  674. ensureIntegrity: true
  675. }, options);
  676. }
  677. /** Calculates the checksum of a string */
  678. calcChecksum(input) {
  679. return __async(this, null, function* () {
  680. return computeHash(input, "SHA-256");
  681. });
  682. }
  683. /** Serializes a DataStore instance */
  684. serializeStore(storeInst) {
  685. return __async(this, null, function* () {
  686. const data = storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
  687. const checksum = this.options.addChecksum ? yield this.calcChecksum(data) : void 0;
  688. return {
  689. id: storeInst.id,
  690. data,
  691. formatVersion: storeInst.formatVersion,
  692. encoded: storeInst.encodingEnabled(),
  693. checksum
  694. };
  695. });
  696. }
  697. /** Serializes the data stores into a string */
  698. serialize() {
  699. return __async(this, null, function* () {
  700. const serData = [];
  701. for (const store of this.stores)
  702. serData.push(yield this.serializeStore(store));
  703. return JSON.stringify(serData);
  704. });
  705. }
  706. /**
  707. * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
  708. * Also triggers the migration process if the data format has changed.
  709. */
  710. deserialize(serializedData) {
  711. return __async(this, null, function* () {
  712. const deserStores = JSON.parse(serializedData);
  713. for (const storeData of deserStores) {
  714. const storeInst = this.stores.find((s) => s.id === storeData.id);
  715. if (!storeInst)
  716. throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
  717. if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
  718. const checksum = yield this.calcChecksum(storeData.data);
  719. if (checksum !== storeData.checksum)
  720. throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!
  721. Expected: ${storeData.checksum}
  722. Has: ${checksum}`);
  723. }
  724. const decodedData = storeData.encoded && storeInst.encodingEnabled() ? yield storeInst.decodeData(storeData.data) : storeData.data;
  725. if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
  726. yield storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
  727. else
  728. yield storeInst.setData(JSON.parse(decodedData));
  729. }
  730. });
  731. }
  732. /**
  733. * Loads the persistent data of the DataStore instances into the in-memory cache.
  734. * Also triggers the migration process if the data format has changed.
  735. * @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }`
  736. */
  737. loadStoresData() {
  738. return __async(this, null, function* () {
  739. return Promise.allSettled(this.stores.map(
  740. (store) => __async(this, null, function* () {
  741. return {
  742. id: store.id,
  743. data: yield store.loadData()
  744. };
  745. })
  746. ));
  747. });
  748. }
  749. /** Resets the persistent data of the DataStore instances to their default values. */
  750. resetStoresData() {
  751. return __async(this, null, function* () {
  752. return Promise.allSettled(this.stores.map((store) => store.saveDefaultData()));
  753. });
  754. }
  755. /**
  756. * Deletes the persistent data of the DataStore instances.
  757. * Leaves the in-memory data untouched.
  758. */
  759. deleteStoresData() {
  760. return __async(this, null, function* () {
  761. return Promise.allSettled(this.stores.map((store) => store.deleteData()));
  762. });
  763. }
  764. };
  765.  
  766. // node_modules/nanoevents/index.js
  767. var createNanoEvents = () => ({
  768. emit(event, ...args) {
  769. for (let i = 0, callbacks = this.events[event] || [], length = callbacks.length; i < length; i++) {
  770. callbacks[i](...args);
  771. }
  772. },
  773. events: {},
  774. on(event, cb) {
  775. var _a;
  776. ((_a = this.events)[event] || (_a[event] = [])).push(cb);
  777. return () => {
  778. var _a2;
  779. this.events[event] = (_a2 = this.events[event]) == null ? void 0 : _a2.filter((i) => cb !== i);
  780. };
  781. }
  782. });
  783.  
  784. // lib/NanoEmitter.ts
  785. var NanoEmitter = class {
  786. constructor(options = {}) {
  787. __publicField(this, "events", createNanoEvents());
  788. __publicField(this, "eventUnsubscribes", []);
  789. __publicField(this, "emitterOptions");
  790. this.emitterOptions = __spreadValues({
  791. publicEmit: false
  792. }, options);
  793. }
  794. /** Subscribes to an event - returns a function that unsubscribes the event listener */
  795. on(event, cb) {
  796. let unsub;
  797. const unsubProxy = () => {
  798. if (!unsub)
  799. return;
  800. unsub();
  801. this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => u !== unsub);
  802. };
  803. unsub = this.events.on(event, cb);
  804. this.eventUnsubscribes.push(unsub);
  805. return unsubProxy;
  806. }
  807. /** Subscribes to an event and calls the callback or resolves the Promise only once */
  808. once(event, cb) {
  809. return new Promise((resolve) => {
  810. let unsub;
  811. const onceProxy = (...args) => {
  812. unsub();
  813. cb == null ? void 0 : cb(...args);
  814. resolve(args);
  815. };
  816. unsub = this.on(event, onceProxy);
  817. });
  818. }
  819. /** Emits an event on this instance - Needs `publicEmit` to be set to true in the constructor! */
  820. emit(event, ...args) {
  821. if (this.emitterOptions.publicEmit) {
  822. this.events.emit(event, ...args);
  823. return true;
  824. }
  825. return false;
  826. }
  827. /** Unsubscribes all event listeners */
  828. unsubscribeAll() {
  829. for (const unsub of this.eventUnsubscribes)
  830. unsub();
  831. this.eventUnsubscribes = [];
  832. }
  833. };
  834.  
  835. // lib/Dialog.ts
  836. var defaultDialogCss = `.uu-no-select {
  837. user-select: none;
  838. }
  839.  
  840. .uu-dialog-bg {
  841. --uu-dialog-bg: #333333;
  842. --uu-dialog-bg-highlight: #252525;
  843. --uu-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
  844. --uu-dialog-separator-color: #797979;
  845. --uu-dialog-border-radius: 10px;
  846. }
  847.  
  848. .uu-dialog-bg {
  849. display: block;
  850. position: fixed;
  851. width: 100%;
  852. height: 100%;
  853. top: 0;
  854. left: 0;
  855. z-index: 5;
  856. background-color: rgba(0, 0, 0, 0.6);
  857. }
  858.  
  859. .uu-dialog {
  860. --uu-calc-dialog-height: calc(min(100vh - 40px, var(--uu-dialog-height-max)));
  861. position: absolute;
  862. display: flex;
  863. flex-direction: column;
  864. width: calc(min(100% - 60px, var(--uu-dialog-width-max)));
  865. border-radius: var(--uu-dialog-border-radius);
  866. height: auto;
  867. max-height: var(--uu-calc-dialog-height);
  868. left: 50%;
  869. top: 50%;
  870. transform: translate(-50%, -50%);
  871. z-index: 6;
  872. color: #fff;
  873. background-color: var(--uu-dialog-bg);
  874. }
  875.  
  876. .uu-dialog.align-top {
  877. top: 0;
  878. transform: translate(-50%, 40px);
  879. }
  880.  
  881. .uu-dialog.align-bottom {
  882. top: 100%;
  883. transform: translate(-50%, -100%);
  884. }
  885.  
  886. .uu-dialog-body {
  887. font-size: 1.5rem;
  888. padding: 20px;
  889. }
  890.  
  891. .uu-dialog-body.small {
  892. padding: 15px;
  893. }
  894.  
  895. #uu-dialog-opts {
  896. display: flex;
  897. flex-direction: column;
  898. position: relative;
  899. padding: 30px 0px;
  900. overflow-y: auto;
  901. }
  902.  
  903. .uu-dialog-header {
  904. display: flex;
  905. justify-content: space-between;
  906. align-items: center;
  907. margin-bottom: 6px;
  908. padding: 15px 20px 15px 20px;
  909. background-color: var(--uu-dialog-bg);
  910. border: 2px solid var(--uu-dialog-separator-color);
  911. border-style: none none solid none !important;
  912. border-radius: var(--uu-dialog-border-radius) var(--uu-dialog-border-radius) 0px 0px;
  913. }
  914.  
  915. .uu-dialog-header.small {
  916. padding: 10px 15px;
  917. border-style: none none solid none !important;
  918. }
  919.  
  920. .uu-dialog-header-pad {
  921. content: " ";
  922. min-height: 32px;
  923. }
  924.  
  925. .uu-dialog-header-pad.small {
  926. min-height: 24px;
  927. }
  928.  
  929. .uu-dialog-titlecont {
  930. display: flex;
  931. align-items: center;
  932. }
  933.  
  934. .uu-dialog-titlecont-no-title {
  935. display: flex;
  936. justify-content: flex-end;
  937. align-items: center;
  938. }
  939.  
  940. .uu-dialog-title {
  941. position: relative;
  942. display: inline-block;
  943. font-size: 22px;
  944. }
  945.  
  946. .uu-dialog-close {
  947. cursor: pointer;
  948. }
  949.  
  950. .uu-dialog-header-img,
  951. .uu-dialog-close
  952. {
  953. width: 32px;
  954. height: 32px;
  955. }
  956.  
  957. .uu-dialog-header-img.small,
  958. .uu-dialog-close.small
  959. {
  960. width: 24px;
  961. height: 24px;
  962. }
  963.  
  964. .uu-dialog-footer {
  965. font-size: 17px;
  966. text-decoration: underline;
  967. }
  968.  
  969. .uu-dialog-footer.hidden {
  970. display: none;
  971. }
  972.  
  973. .uu-dialog-footer-cont {
  974. margin-top: 6px;
  975. padding: 15px 20px;
  976. background: var(--uu-dialog-bg);
  977. background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--uu-dialog-bg) 30%, var(--uu-dialog-bg) 100%);
  978. border: 2px solid var(--uu-dialog-separator-color);
  979. border-style: solid none none none !important;
  980. border-radius: 0px 0px var(--uu-dialog-border-radius) var(--uu-dialog-border-radius);
  981. }
  982.  
  983. .uu-dialog-footer-buttons-cont button:not(:last-of-type) {
  984. margin-right: 15px;
  985. }`;
  986. exports.currentDialogId = null;
  987. var openDialogs = [];
  988. var defaultStrings = {
  989. closeDialogTooltip: "Click to close the dialog"
  990. };
  991. var Dialog = class _Dialog extends NanoEmitter {
  992. constructor(options) {
  993. super();
  994. /** Options passed to the dialog in the constructor */
  995. __publicField(this, "options");
  996. /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
  997. __publicField(this, "id");
  998. /** Strings used in the dialog (used for translations) */
  999. __publicField(this, "strings");
  1000. __publicField(this, "dialogOpen", false);
  1001. __publicField(this, "dialogMounted", false);
  1002. const _a = options, { strings } = _a, opts = __objRest(_a, ["strings"]);
  1003. this.strings = __spreadValues(__spreadValues({}, defaultStrings), strings != null ? strings : {});
  1004. this.options = __spreadValues({
  1005. closeOnBgClick: true,
  1006. closeOnEscPress: true,
  1007. destroyOnClose: false,
  1008. unmountOnClose: true,
  1009. removeListenersOnDestroy: true,
  1010. small: false,
  1011. verticalAlign: "center"
  1012. }, opts);
  1013. this.id = opts.id;
  1014. }
  1015. //#region public
  1016. /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
  1017. mount() {
  1018. return __async(this, null, function* () {
  1019. var _a;
  1020. if (this.dialogMounted)
  1021. return;
  1022. this.dialogMounted = true;
  1023. if (!document.querySelector("style.uu-dialog-css"))
  1024. addGlobalStyle((_a = this.options.dialogCss) != null ? _a : defaultDialogCss).classList.add("uu-dialog-css");
  1025. const bgElem = document.createElement("div");
  1026. bgElem.id = `uu-${this.id}-dialog-bg`;
  1027. bgElem.classList.add("uu-dialog-bg");
  1028. if (this.options.closeOnBgClick)
  1029. bgElem.ariaLabel = bgElem.title = this.getString("closeDialogTooltip");
  1030. bgElem.style.setProperty("--uu-dialog-width-max", `${this.options.width}px`);
  1031. bgElem.style.setProperty("--uu-dialog-height-max", `${this.options.height}px`);
  1032. bgElem.style.visibility = "hidden";
  1033. bgElem.style.display = "none";
  1034. bgElem.inert = true;
  1035. bgElem.appendChild(yield this.getDialogContent());
  1036. document.body.appendChild(bgElem);
  1037. this.attachListeners(bgElem);
  1038. this.events.emit("render");
  1039. return bgElem;
  1040. });
  1041. }
  1042. /** Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call */
  1043. unmount() {
  1044. var _a;
  1045. this.close();
  1046. this.dialogMounted = false;
  1047. const clearSelectors = [
  1048. `#uu-${this.id}-dialog-bg`,
  1049. `#uu-style-dialog-${this.id}`
  1050. ];
  1051. for (const sel of clearSelectors)
  1052. (_a = document.querySelector(sel)) == null ? void 0 : _a.remove();
  1053. this.events.emit("clear");
  1054. }
  1055. /** Clears the DOM of the dialog and then renders it again */
  1056. remount() {
  1057. return __async(this, null, function* () {
  1058. this.unmount();
  1059. yield this.mount();
  1060. });
  1061. }
  1062. /**
  1063. * Opens the dialog - also mounts it if it hasn't been mounted yet
  1064. * Prevents default action and immediate propagation of the passed event
  1065. */
  1066. open(e) {
  1067. return __async(this, null, function* () {
  1068. var _a;
  1069. e == null ? void 0 : e.preventDefault();
  1070. e == null ? void 0 : e.stopImmediatePropagation();
  1071. if (this.isOpen())
  1072. return;
  1073. this.dialogOpen = true;
  1074. if (openDialogs.includes(this.id))
  1075. throw new Error(`A dialog with the same ID of '${this.id}' already exists and is open!`);
  1076. if (!this.isMounted())
  1077. yield this.mount();
  1078. const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
  1079. if (!dialogBg)
  1080. return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
  1081. dialogBg.style.visibility = "visible";
  1082. dialogBg.style.display = "block";
  1083. dialogBg.inert = false;
  1084. exports.currentDialogId = this.id;
  1085. openDialogs.unshift(this.id);
  1086. for (const dialogId of openDialogs)
  1087. if (dialogId !== this.id)
  1088. (_a = document.querySelector(`#uu-${dialogId}-dialog-bg`)) == null ? void 0 : _a.setAttribute("inert", "true");
  1089. document.body.classList.remove("uu-no-select");
  1090. document.body.setAttribute("inert", "true");
  1091. this.events.emit("open");
  1092. return dialogBg;
  1093. });
  1094. }
  1095. /** Closes the dialog - prevents default action and immediate propagation of the passed event */
  1096. close(e) {
  1097. var _a, _b;
  1098. e == null ? void 0 : e.preventDefault();
  1099. e == null ? void 0 : e.stopImmediatePropagation();
  1100. if (!this.isOpen())
  1101. return;
  1102. this.dialogOpen = false;
  1103. const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
  1104. if (!dialogBg)
  1105. return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
  1106. dialogBg.style.visibility = "hidden";
  1107. dialogBg.style.display = "none";
  1108. dialogBg.inert = true;
  1109. openDialogs.splice(openDialogs.indexOf(this.id), 1);
  1110. exports.currentDialogId = (_a = openDialogs[0]) != null ? _a : null;
  1111. if (exports.currentDialogId)
  1112. (_b = document.querySelector(`#uu-${exports.currentDialogId}-dialog-bg`)) == null ? void 0 : _b.removeAttribute("inert");
  1113. if (openDialogs.length === 0) {
  1114. document.body.classList.add("uu-no-select");
  1115. document.body.removeAttribute("inert");
  1116. }
  1117. this.events.emit("close");
  1118. if (this.options.destroyOnClose)
  1119. this.destroy();
  1120. else if (this.options.unmountOnClose)
  1121. this.unmount();
  1122. }
  1123. /** Returns true if the dialog is currently open */
  1124. isOpen() {
  1125. return this.dialogOpen;
  1126. }
  1127. /** Returns true if the dialog is currently mounted */
  1128. isMounted() {
  1129. return this.dialogMounted;
  1130. }
  1131. /** Clears the DOM of the dialog and removes all event listeners */
  1132. destroy() {
  1133. this.unmount();
  1134. this.events.emit("destroy");
  1135. this.options.removeListenersOnDestroy && this.unsubscribeAll();
  1136. }
  1137. //#region static
  1138. /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
  1139. static getCurrentDialogId() {
  1140. return exports.currentDialogId;
  1141. }
  1142. /** Returns the IDs of all currently open dialogs, top-most first */
  1143. static getOpenDialogs() {
  1144. return openDialogs;
  1145. }
  1146. //#region protected
  1147. getString(key) {
  1148. var _a;
  1149. return (_a = this.strings[key]) != null ? _a : defaultStrings[key];
  1150. }
  1151. /** Called once to attach all generic event listeners */
  1152. attachListeners(bgElem) {
  1153. if (this.options.closeOnBgClick) {
  1154. bgElem.addEventListener("click", (e) => {
  1155. var _a;
  1156. if (this.isOpen() && ((_a = e.target) == null ? void 0 : _a.id) === `uu-${this.id}-dialog-bg`)
  1157. this.close(e);
  1158. });
  1159. }
  1160. if (this.options.closeOnEscPress) {
  1161. document.body.addEventListener("keydown", (e) => {
  1162. if (e.key === "Escape" && this.isOpen() && _Dialog.getCurrentDialogId() === this.id)
  1163. this.close(e);
  1164. });
  1165. }
  1166. }
  1167. //#region protected
  1168. /**
  1169. * Adds generic, accessible interaction listeners to the passed element.
  1170. * All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
  1171. * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
  1172. */
  1173. onInteraction(elem, listener, listenerOptions) {
  1174. const _a = listenerOptions != null ? listenerOptions : {}, { preventDefault = true, stopPropagation = true } = _a, listenerOpts = __objRest(_a, ["preventDefault", "stopPropagation"]);
  1175. const interactionKeys = ["Enter", " ", "Space"];
  1176. const proxListener = (e) => {
  1177. if (e instanceof KeyboardEvent) {
  1178. if (interactionKeys.includes(e.key)) {
  1179. preventDefault && e.preventDefault();
  1180. stopPropagation && e.stopPropagation();
  1181. } else
  1182. return;
  1183. } else if (e instanceof MouseEvent) {
  1184. preventDefault && e.preventDefault();
  1185. stopPropagation && e.stopPropagation();
  1186. }
  1187. (listenerOpts == null ? void 0 : listenerOpts.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOpts);
  1188. (listenerOpts == null ? void 0 : listenerOpts.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOpts);
  1189. listener(e);
  1190. };
  1191. elem.addEventListener("click", proxListener, listenerOpts);
  1192. elem.addEventListener("keydown", proxListener, listenerOpts);
  1193. }
  1194. /** Returns the dialog content element and all its children */
  1195. getDialogContent() {
  1196. return __async(this, null, function* () {
  1197. var _a, _b, _c, _d;
  1198. const header = (_b = (_a = this.options).renderHeader) == null ? void 0 : _b.call(_a);
  1199. const footer = (_d = (_c = this.options).renderFooter) == null ? void 0 : _d.call(_c);
  1200. const dialogWrapperEl = document.createElement("div");
  1201. dialogWrapperEl.id = `uu-${this.id}-dialog`;
  1202. dialogWrapperEl.classList.add("uu-dialog");
  1203. dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
  1204. dialogWrapperEl.role = "dialog";
  1205. dialogWrapperEl.setAttribute("aria-labelledby", `uu-${this.id}-dialog-title`);
  1206. dialogWrapperEl.setAttribute("aria-describedby", `uu-${this.id}-dialog-body`);
  1207. if (this.options.verticalAlign !== "center")
  1208. dialogWrapperEl.classList.add(`align-${this.options.verticalAlign}`);
  1209. const headerWrapperEl = document.createElement("div");
  1210. headerWrapperEl.classList.add("uu-dialog-header");
  1211. this.options.small && headerWrapperEl.classList.add("small");
  1212. if (header) {
  1213. const headerTitleWrapperEl = document.createElement("div");
  1214. headerTitleWrapperEl.id = `uu-${this.id}-dialog-title`;
  1215. headerTitleWrapperEl.classList.add("uu-dialog-title-wrapper");
  1216. headerTitleWrapperEl.role = "heading";
  1217. headerTitleWrapperEl.ariaLevel = "1";
  1218. headerTitleWrapperEl.appendChild(header instanceof Promise ? yield header : header);
  1219. headerWrapperEl.appendChild(headerTitleWrapperEl);
  1220. } else {
  1221. const padEl = document.createElement("div");
  1222. padEl.classList.add("uu-dialog-header-pad", this.options.small ? "small" : "");
  1223. headerWrapperEl.appendChild(padEl);
  1224. }
  1225. if (this.options.renderCloseBtn) {
  1226. const closeBtnEl = yield this.options.renderCloseBtn();
  1227. closeBtnEl.classList.add("uu-dialog-close");
  1228. this.options.small && closeBtnEl.classList.add("small");
  1229. closeBtnEl.tabIndex = 0;
  1230. if (closeBtnEl.hasAttribute("alt"))
  1231. closeBtnEl.setAttribute("alt", this.getString("closeDialogTooltip"));
  1232. closeBtnEl.title = closeBtnEl.ariaLabel = this.getString("closeDialogTooltip");
  1233. this.onInteraction(closeBtnEl, () => this.close());
  1234. headerWrapperEl.appendChild(closeBtnEl);
  1235. }
  1236. dialogWrapperEl.appendChild(headerWrapperEl);
  1237. const dialogBodyElem = document.createElement("div");
  1238. dialogBodyElem.id = `uu-${this.id}-dialog-body`;
  1239. dialogBodyElem.classList.add("uu-dialog-body");
  1240. this.options.small && dialogBodyElem.classList.add("small");
  1241. const body = this.options.renderBody();
  1242. dialogBodyElem.appendChild(body instanceof Promise ? yield body : body);
  1243. dialogWrapperEl.appendChild(dialogBodyElem);
  1244. if (footer) {
  1245. const footerWrapper = document.createElement("div");
  1246. footerWrapper.classList.add("uu-dialog-footer-cont");
  1247. dialogWrapperEl.appendChild(footerWrapper);
  1248. footerWrapper.appendChild(footer instanceof Promise ? yield footer : footer);
  1249. }
  1250. return dialogWrapperEl;
  1251. });
  1252. }
  1253. };
  1254.  
  1255. // lib/misc.ts
  1256. function autoPlural(word, num) {
  1257. if (Array.isArray(num) || num instanceof NodeList)
  1258. num = num.length;
  1259. return `${word}${num === 1 ? "" : "s"}`;
  1260. }
  1261. function insertValues(input, ...values) {
  1262. return input.replace(/%\d/gm, (match) => {
  1263. var _a, _b;
  1264. const argIndex = Number(match.substring(1)) - 1;
  1265. return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
  1266. });
  1267. }
  1268. function pauseFor(time) {
  1269. return new Promise((res) => {
  1270. setTimeout(() => res(), time);
  1271. });
  1272. }
  1273. function debounce(func, timeout = 300, edge = "falling") {
  1274. let id;
  1275. return function(...args) {
  1276. if (edge === "rising") {
  1277. if (!id) {
  1278. func.apply(this, args);
  1279. id = setTimeout(() => id = void 0, timeout);
  1280. }
  1281. } else {
  1282. clearTimeout(id);
  1283. id = setTimeout(() => func.apply(this, args), timeout);
  1284. }
  1285. };
  1286. }
  1287. function fetchAdvanced(_0) {
  1288. return __async(this, arguments, function* (input, options = {}) {
  1289. var _a;
  1290. const { timeout = 1e4 } = options;
  1291. const { signal, abort } = new AbortController();
  1292. (_a = options.signal) == null ? void 0 : _a.addEventListener("abort", abort);
  1293. let signalOpts = {}, id = void 0;
  1294. if (timeout >= 0) {
  1295. id = setTimeout(() => abort(), timeout);
  1296. signalOpts = { signal };
  1297. }
  1298. try {
  1299. const res = yield fetch(input, __spreadValues(__spreadValues({}, options), signalOpts));
  1300. id && clearTimeout(id);
  1301. return res;
  1302. } catch (err) {
  1303. id && clearTimeout(id);
  1304. throw err;
  1305. }
  1306. });
  1307. }
  1308.  
  1309. // lib/SelectorObserver.ts
  1310. var domLoaded = false;
  1311. document.addEventListener("DOMContentLoaded", () => domLoaded = true);
  1312. var SelectorObserver = class {
  1313. constructor(baseElement, options = {}) {
  1314. __publicField(this, "enabled", false);
  1315. __publicField(this, "baseElement");
  1316. __publicField(this, "observer");
  1317. __publicField(this, "observerOptions");
  1318. __publicField(this, "customOptions");
  1319. __publicField(this, "listenerMap");
  1320. this.baseElement = baseElement;
  1321. this.listenerMap = /* @__PURE__ */ new Map();
  1322. const _a = options, {
  1323. defaultDebounce,
  1324. defaultDebounceEdge,
  1325. disableOnNoListeners,
  1326. enableOnAddListener
  1327. } = _a, observerOptions = __objRest(_a, [
  1328. "defaultDebounce",
  1329. "defaultDebounceEdge",
  1330. "disableOnNoListeners",
  1331. "enableOnAddListener"
  1332. ]);
  1333. this.observerOptions = __spreadValues({
  1334. childList: true,
  1335. subtree: true
  1336. }, observerOptions);
  1337. this.customOptions = {
  1338. defaultDebounce: defaultDebounce != null ? defaultDebounce : 0,
  1339. defaultDebounceEdge: defaultDebounceEdge != null ? defaultDebounceEdge : "rising",
  1340. disableOnNoListeners: disableOnNoListeners != null ? disableOnNoListeners : false,
  1341. enableOnAddListener: enableOnAddListener != null ? enableOnAddListener : true
  1342. };
  1343. if (typeof this.customOptions.checkInterval !== "number") {
  1344. this.observer = new MutationObserver(() => this.checkAllSelectors());
  1345. } else {
  1346. this.checkAllSelectors();
  1347. setInterval(() => this.checkAllSelectors(), this.customOptions.checkInterval);
  1348. }
  1349. }
  1350. /** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
  1351. checkAllSelectors() {
  1352. if (!this.enabled || !domLoaded)
  1353. return;
  1354. for (const [selector, listeners] of this.listenerMap.entries())
  1355. this.checkSelector(selector, listeners);
  1356. }
  1357. /** Checks if the element(s) with the given {@linkcode selector} exist in the DOM and calls the respective {@linkcode listeners} accordingly */
  1358. checkSelector(selector, listeners) {
  1359. var _a;
  1360. if (!this.enabled)
  1361. return;
  1362. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  1363. if (!baseElement)
  1364. return;
  1365. const all = listeners.some((listener) => listener.all);
  1366. const one = listeners.some((listener) => !listener.all);
  1367. const allElements = all ? baseElement.querySelectorAll(selector) : null;
  1368. const oneElement = one ? baseElement.querySelector(selector) : null;
  1369. for (const options of listeners) {
  1370. if (options.all) {
  1371. if (allElements && allElements.length > 0) {
  1372. options.listener(allElements);
  1373. if (!options.continuous)
  1374. this.removeListener(selector, options);
  1375. }
  1376. } else {
  1377. if (oneElement) {
  1378. options.listener(oneElement);
  1379. if (!options.continuous)
  1380. this.removeListener(selector, options);
  1381. }
  1382. }
  1383. if (((_a = this.listenerMap.get(selector)) == null ? void 0 : _a.length) === 0)
  1384. this.listenerMap.delete(selector);
  1385. if (this.listenerMap.size === 0 && this.customOptions.disableOnNoListeners)
  1386. this.disable();
  1387. }
  1388. }
  1389. /**
  1390. * Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
  1391. * @param selector The selector to observe
  1392. * @param options Options for the selector observation
  1393. * @param options.listener Gets called whenever the selector was found in the DOM
  1394. * @param [options.all] Whether to use `querySelectorAll()` instead - default is false
  1395. * @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
  1396. * @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
  1397. * @returns Returns a function that can be called to remove this listener more easily
  1398. */
  1399. addListener(selector, options) {
  1400. options = __spreadValues({
  1401. all: false,
  1402. continuous: false,
  1403. debounce: 0
  1404. }, options);
  1405. if (options.debounce && options.debounce > 0 || this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0) {
  1406. options.listener = debounce(
  1407. options.listener,
  1408. options.debounce || this.customOptions.defaultDebounce,
  1409. options.debounceEdge || this.customOptions.defaultDebounceEdge
  1410. );
  1411. }
  1412. if (this.listenerMap.has(selector))
  1413. this.listenerMap.get(selector).push(options);
  1414. else
  1415. this.listenerMap.set(selector, [options]);
  1416. if (this.enabled === false && this.customOptions.enableOnAddListener)
  1417. this.enable();
  1418. this.checkSelector(selector, [options]);
  1419. return () => this.removeListener(selector, options);
  1420. }
  1421. /** Disables the observation of the child elements */
  1422. disable() {
  1423. var _a;
  1424. if (!this.enabled)
  1425. return;
  1426. this.enabled = false;
  1427. (_a = this.observer) == null ? void 0 : _a.disconnect();
  1428. }
  1429. /**
  1430. * Enables or reenables the observation of the child elements.
  1431. * @param immediatelyCheckSelectors Whether to immediately check if all previously registered selectors exist (default is true)
  1432. * @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found)
  1433. */
  1434. enable(immediatelyCheckSelectors = true) {
  1435. var _a;
  1436. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  1437. if (this.enabled || !baseElement)
  1438. return false;
  1439. this.enabled = true;
  1440. (_a = this.observer) == null ? void 0 : _a.observe(baseElement, this.observerOptions);
  1441. if (immediatelyCheckSelectors)
  1442. this.checkAllSelectors();
  1443. return true;
  1444. }
  1445. /** Returns whether the observation of the child elements is currently enabled */
  1446. isEnabled() {
  1447. return this.enabled;
  1448. }
  1449. /** Removes all listeners that have been registered with {@linkcode addListener()} */
  1450. clearListeners() {
  1451. this.listenerMap.clear();
  1452. }
  1453. /**
  1454. * Removes all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()}
  1455. * @returns Returns true when all listeners for the associated selector were found and removed, false otherwise
  1456. */
  1457. removeAllListeners(selector) {
  1458. return this.listenerMap.delete(selector);
  1459. }
  1460. /**
  1461. * Removes a single listener for the given {@linkcode selector} and {@linkcode options} that has been registered with {@linkcode addListener()}
  1462. * @returns Returns true when the listener was found and removed, false otherwise
  1463. */
  1464. removeListener(selector, options) {
  1465. const listeners = this.listenerMap.get(selector);
  1466. if (!listeners)
  1467. return false;
  1468. const index = listeners.indexOf(options);
  1469. if (index > -1) {
  1470. listeners.splice(index, 1);
  1471. return true;
  1472. }
  1473. return false;
  1474. }
  1475. /** Returns all listeners that have been registered with {@linkcode addListener()} */
  1476. getAllListeners() {
  1477. return this.listenerMap;
  1478. }
  1479. /** Returns all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} */
  1480. getListeners(selector) {
  1481. return this.listenerMap.get(selector);
  1482. }
  1483. };
  1484.  
  1485. // lib/translation.ts
  1486. var trans = {};
  1487. var curLang;
  1488. var trLang = (language, key, ...args) => {
  1489. var _a;
  1490. if (!language)
  1491. return key;
  1492. const trText = (_a = trans[language]) == null ? void 0 : _a[key];
  1493. if (!trText)
  1494. return key;
  1495. if (args.length > 0 && trText.match(/%\d/)) {
  1496. return insertValues(trText, ...args);
  1497. }
  1498. return trText;
  1499. };
  1500. function tr(key, ...args) {
  1501. return trLang(curLang, key, ...args);
  1502. }
  1503. tr.forLang = trLang;
  1504. tr.addLanguage = (language, translations) => {
  1505. trans[language] = translations;
  1506. };
  1507. tr.setLanguage = (language) => {
  1508. curLang = language;
  1509. };
  1510. tr.getLanguage = () => {
  1511. return curLang;
  1512. };
  1513. tr.getTranslations = (language) => {
  1514. return trans[language != null ? language : curLang];
  1515. };
  1516.  
  1517. exports.DataStore = DataStore;
  1518. exports.DataStoreSerializer = DataStoreSerializer;
  1519. exports.Dialog = Dialog;
  1520. exports.NanoEmitter = NanoEmitter;
  1521. exports.SelectorObserver = SelectorObserver;
  1522. exports.addGlobalStyle = addGlobalStyle;
  1523. exports.addParent = addParent;
  1524. exports.autoPlural = autoPlural;
  1525. exports.clamp = clamp;
  1526. exports.compress = compress;
  1527. exports.computeHash = computeHash;
  1528. exports.darkenColor = darkenColor;
  1529. exports.debounce = debounce;
  1530. exports.decompress = decompress;
  1531. exports.defaultDialogCss = defaultDialogCss;
  1532. exports.defaultStrings = defaultStrings;
  1533. exports.fetchAdvanced = fetchAdvanced;
  1534. exports.getSiblingsFrame = getSiblingsFrame;
  1535. exports.getUnsafeWindow = getUnsafeWindow;
  1536. exports.hexToRgb = hexToRgb;
  1537. exports.insertValues = insertValues;
  1538. exports.interceptEvent = interceptEvent;
  1539. exports.interceptWindowEvent = interceptWindowEvent;
  1540. exports.isScrollable = isScrollable;
  1541. exports.lightenColor = lightenColor;
  1542. exports.mapRange = mapRange;
  1543. exports.observeElementProp = observeElementProp;
  1544. exports.openDialogs = openDialogs;
  1545. exports.openInNewTab = openInNewTab;
  1546. exports.pauseFor = pauseFor;
  1547. exports.preloadImages = preloadImages;
  1548. exports.randRange = randRange;
  1549. exports.randomId = randomId;
  1550. exports.randomItem = randomItem;
  1551. exports.randomItemIndex = randomItemIndex;
  1552. exports.randomizeArray = randomizeArray;
  1553. exports.rgbToHex = rgbToHex;
  1554. exports.setInnerHtmlUnsafe = setInnerHtmlUnsafe;
  1555. exports.takeRandomItem = takeRandomItem;
  1556. exports.tr = tr;
  1557.  
  1558. return exports;
  1559.  
  1560. })({});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址