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-12-22 提交的版本,查看 最新版本

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