UserUtils

General purpose DOM/GreaseMonkey library: register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/472956/1563415/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 General purpose DOM/GreaseMonkey library that allows you to register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more
  11. // @version 9.4.1
  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 __defProps = Object.defineProperties;
  25. var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  26. var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  27. var __hasOwnProp = Object.prototype.hasOwnProperty;
  28. var __propIsEnum = Object.prototype.propertyIsEnumerable;
  29. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  30. var __spreadValues = (a, b) => {
  31. for (var prop in b || (b = {}))
  32. if (__hasOwnProp.call(b, prop))
  33. __defNormalProp(a, prop, b[prop]);
  34. if (__getOwnPropSymbols)
  35. for (var prop of __getOwnPropSymbols(b)) {
  36. if (__propIsEnum.call(b, prop))
  37. __defNormalProp(a, prop, b[prop]);
  38. }
  39. return a;
  40. };
  41. var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  42. var __objRest = (source, exclude) => {
  43. var target = {};
  44. for (var prop in source)
  45. if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
  46. target[prop] = source[prop];
  47. if (source != null && __getOwnPropSymbols)
  48. for (var prop of __getOwnPropSymbols(source)) {
  49. if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
  50. target[prop] = source[prop];
  51. }
  52. return target;
  53. };
  54. var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  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. if (typeof max !== "number") {
  79. max = min;
  80. min = 0;
  81. }
  82. return Math.max(Math.min(value, max), min);
  83. }
  84. function mapRange(value, range1min, range1max, range2min, range2max) {
  85. if (typeof range2min === "undefined" || typeof range2max === "undefined") {
  86. range2max = range1max;
  87. range1max = range1min;
  88. range2min = range1min = 0;
  89. }
  90. if (Number(range1min) === 0 && Number(range2min) === 0)
  91. return value * (range2max / range1max);
  92. return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
  93. }
  94. function randRange(...args) {
  95. let min, max, enhancedEntropy = false;
  96. if (typeof args[0] === "number" && typeof args[1] === "number")
  97. [min, max] = args;
  98. else if (typeof args[0] === "number" && typeof args[1] !== "number") {
  99. min = 0;
  100. [max] = args;
  101. } else
  102. 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`);
  103. if (typeof args[2] === "boolean")
  104. enhancedEntropy = args[2];
  105. else if (typeof args[1] === "boolean")
  106. enhancedEntropy = args[1];
  107. min = Number(min);
  108. max = Number(max);
  109. if (isNaN(min) || isNaN(max))
  110. return NaN;
  111. if (min > max)
  112. throw new TypeError(`Parameter "min" can't be bigger than "max"`);
  113. if (enhancedEntropy) {
  114. const uintArr = new Uint8Array(1);
  115. crypto.getRandomValues(uintArr);
  116. return Number(Array.from(
  117. uintArr,
  118. (v) => Math.round(mapRange(v, 0, 255, min, max)).toString(10)
  119. ).join(""));
  120. } else
  121. return Math.floor(Math.random() * (max - min + 1)) + min;
  122. }
  123. function digitCount(num, withDecimals = true) {
  124. num = Number(!["string", "number"].includes(typeof num) ? String(num) : num);
  125. if (typeof num === "number" && isNaN(num))
  126. return NaN;
  127. const [intPart, decPart] = num.toString().split(".");
  128. const intDigits = intPart === "0" ? 1 : Math.floor(Math.log10(Math.abs(Number(intPart))) + 1);
  129. const decDigits = withDecimals && decPart ? decPart.length : 0;
  130. return intDigits + decDigits;
  131. }
  132. function roundFixed(num, fractionDigits) {
  133. const scale = 10 ** fractionDigits;
  134. return Math.round(num * scale) / scale;
  135. }
  136. function bitSetHas(bitSet, checkVal) {
  137. return (bitSet & checkVal) === checkVal;
  138. }
  139.  
  140. // lib/array.ts
  141. function randomItem(array) {
  142. return randomItemIndex(array)[0];
  143. }
  144. function randomItemIndex(array) {
  145. if (array.length === 0)
  146. return [undefined, undefined];
  147. const idx = randRange(array.length - 1);
  148. return [array[idx], idx];
  149. }
  150. function takeRandomItem(arr) {
  151. const [itm, idx] = randomItemIndex(arr);
  152. if (idx === undefined)
  153. return undefined;
  154. arr.splice(idx, 1);
  155. return itm;
  156. }
  157. function randomizeArray(array) {
  158. const retArray = [...array];
  159. if (array.length === 0)
  160. return retArray;
  161. for (let i = retArray.length - 1; i > 0; i--) {
  162. const j = Math.floor(Math.random() * (i + 1));
  163. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  164. }
  165. return retArray;
  166. }
  167.  
  168. // lib/colors.ts
  169. function hexToRgb(hex) {
  170. hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
  171. const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : undefined;
  172. if (!isNaN(Number(a)))
  173. hex = hex.slice(0, -(hex.length / 4));
  174. if (hex.length === 3 || hex.length === 4)
  175. hex = hex.split("").map((c) => c + c).join("");
  176. const bigint = parseInt(hex, 16);
  177. const r = bigint >> 16 & 255;
  178. const g = bigint >> 8 & 255;
  179. const b = bigint & 255;
  180. return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : undefined];
  181. }
  182. function rgbToHex(red, green, blue, alpha, withHash = true, upperCase = false) {
  183. const toHexVal = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[upperCase ? "toUpperCase" : "toLowerCase"]();
  184. return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`;
  185. }
  186. function lightenColor(color, percent, upperCase = false) {
  187. return darkenColor(color, percent * -1, upperCase);
  188. }
  189. function darkenColor(color, percent, upperCase = false) {
  190. var _a;
  191. color = color.trim();
  192. const darkenRgb = (r2, g2, b2, percent2) => {
  193. r2 = Math.max(0, Math.min(255, r2 - r2 * percent2 / 100));
  194. g2 = Math.max(0, Math.min(255, g2 - g2 * percent2 / 100));
  195. b2 = Math.max(0, Math.min(255, b2 - b2 * percent2 / 100));
  196. return [r2, g2, b2];
  197. };
  198. let r, g, b, a;
  199. const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
  200. if (isHexCol)
  201. [r, g, b, a] = hexToRgb(color);
  202. else if (color.startsWith("rgb")) {
  203. const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? undefined : _a.map(Number);
  204. if (!rgbValues)
  205. throw new TypeError("Invalid RGB/RGBA color format");
  206. [r, g, b, a] = rgbValues;
  207. } else
  208. throw new TypeError("Unsupported color format");
  209. [r, g, b] = darkenRgb(r, g, b, percent);
  210. if (isHexCol)
  211. return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
  212. else if (color.startsWith("rgba"))
  213. return `rgba(${r}, ${g}, ${b}, ${a != null ? a : NaN})`;
  214. else if (color.startsWith("rgb"))
  215. return `rgb(${r}, ${g}, ${b})`;
  216. else
  217. throw new TypeError("Unsupported color format");
  218. }
  219.  
  220. // lib/errors.ts
  221. var UUError = class extends Error {
  222. constructor(message, options) {
  223. super(message, options);
  224. __publicField(this, "date");
  225. this.date = /* @__PURE__ */ new Date();
  226. }
  227. };
  228. var ChecksumMismatchError = class extends UUError {
  229. constructor(message, options) {
  230. super(message, options);
  231. this.name = "ChecksumMismatchError";
  232. }
  233. };
  234. var MigrationError = class extends UUError {
  235. constructor(message, options) {
  236. super(message, options);
  237. this.name = "MigrationError";
  238. }
  239. };
  240. var PlatformError = class extends UUError {
  241. constructor(message, options) {
  242. super(message, options);
  243. this.name = "PlatformError";
  244. }
  245. };
  246.  
  247. // lib/dom.ts
  248. var domReady = false;
  249. document.addEventListener("DOMContentLoaded", () => domReady = true);
  250. function getUnsafeWindow() {
  251. try {
  252. return unsafeWindow;
  253. } catch (e) {
  254. return window;
  255. }
  256. }
  257. function addParent(element, newParent) {
  258. const oldParent = element.parentNode;
  259. if (!oldParent)
  260. throw new Error("Element doesn't have a parent node");
  261. oldParent.replaceChild(newParent, element);
  262. newParent.appendChild(element);
  263. return newParent;
  264. }
  265. function addGlobalStyle(style) {
  266. const styleElem = document.createElement("style");
  267. setInnerHtmlUnsafe(styleElem, style);
  268. document.head.appendChild(styleElem);
  269. return styleElem;
  270. }
  271. function preloadImages(srcUrls, rejects = false) {
  272. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  273. const image = new Image();
  274. image.addEventListener("load", () => res(image));
  275. image.addEventListener("error", (evt) => rejects && rej(evt));
  276. image.src = src;
  277. }));
  278. return Promise.allSettled(promises);
  279. }
  280. function openInNewTab(href, background, additionalProps) {
  281. try {
  282. if (typeof window.GM === "object")
  283. GM.openInTab(href, background);
  284. } catch (e) {
  285. const openElem = document.createElement("a");
  286. Object.assign(openElem, __spreadValues({
  287. className: "userutils-open-in-new-tab",
  288. target: "_blank",
  289. rel: "noopener noreferrer",
  290. tabIndex: -1,
  291. ariaHidden: "true",
  292. href
  293. }, additionalProps));
  294. Object.assign(openElem.style, {
  295. display: "none",
  296. pointerEvents: "none"
  297. });
  298. document.body.appendChild(openElem);
  299. openElem.click();
  300. setTimeout(() => {
  301. try {
  302. openElem.remove();
  303. } catch (e2) {
  304. }
  305. }, 0);
  306. }
  307. }
  308. function interceptEvent(eventObject, eventName, predicate = () => true) {
  309. var _a;
  310. if (typeof window.GM === "object" && ((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey" && (eventObject === window || eventObject === getUnsafeWindow()))
  311. throw new PlatformError("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
  312. Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100);
  313. if (isNaN(Error.stackTraceLimit))
  314. Error.stackTraceLimit = 100;
  315. (function(original) {
  316. eventObject.__proto__.addEventListener = function(...args) {
  317. var _a2, _b;
  318. const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a2 = args[1]) == null ? undefined : _a2.handleEvent) != null ? _b : () => undefined;
  319. args[1] = function(...a) {
  320. if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
  321. return;
  322. else
  323. return origListener.apply(this, a);
  324. };
  325. original.apply(this, args);
  326. };
  327. })(eventObject.__proto__.addEventListener);
  328. }
  329. function interceptWindowEvent(eventName, predicate = () => true) {
  330. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  331. }
  332. function isScrollable(element) {
  333. const { overflowX, overflowY } = getComputedStyle(element);
  334. return {
  335. vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
  336. horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
  337. };
  338. }
  339. function observeElementProp(element, property, callback) {
  340. const elementPrototype = Object.getPrototypeOf(element);
  341. if (elementPrototype.hasOwnProperty(property)) {
  342. const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
  343. Object.defineProperty(element, property, {
  344. get: function() {
  345. var _a;
  346. return (_a = descriptor == null ? undefined : descriptor.get) == null ? undefined : _a.apply(this, arguments);
  347. },
  348. set: function() {
  349. var _a;
  350. const oldValue = this[property];
  351. (_a = descriptor == null ? undefined : descriptor.set) == null ? undefined : _a.apply(this, arguments);
  352. const newValue = this[property];
  353. if (typeof callback === "function") {
  354. callback.bind(this, oldValue, newValue);
  355. }
  356. return newValue;
  357. }
  358. });
  359. }
  360. }
  361. function getSiblingsFrame(refElement, siblingAmount, refElementAlignment = "center-top", includeRef = true) {
  362. var _a, _b;
  363. const siblings = [...(_b = (_a = refElement.parentNode) == null ? undefined : _a.childNodes) != null ? _b : []];
  364. const elemSiblIdx = siblings.indexOf(refElement);
  365. if (elemSiblIdx === -1)
  366. throw new Error("Element doesn't have a parent node");
  367. if (refElementAlignment === "top")
  368. return [...siblings.slice(elemSiblIdx + Number(!includeRef), elemSiblIdx + siblingAmount + Number(!includeRef))];
  369. else if (refElementAlignment.startsWith("center-")) {
  370. const halfAmount = (refElementAlignment === "center-bottom" ? Math.ceil : Math.floor)(siblingAmount / 2);
  371. const startIdx = Math.max(0, elemSiblIdx - halfAmount);
  372. const topOffset = Number(refElementAlignment === "center-top" && siblingAmount % 2 === 0 && includeRef);
  373. const btmOffset = Number(refElementAlignment === "center-bottom" && siblingAmount % 2 !== 0 && includeRef);
  374. const startIdxWithOffset = startIdx + topOffset + btmOffset;
  375. return [
  376. ...siblings.filter((_, idx) => includeRef || idx !== elemSiblIdx).slice(startIdxWithOffset, startIdxWithOffset + siblingAmount)
  377. ];
  378. } else if (refElementAlignment === "bottom")
  379. return [...siblings.slice(elemSiblIdx - siblingAmount + Number(includeRef), elemSiblIdx + Number(includeRef))];
  380. return [];
  381. }
  382. var ttPolicy;
  383. function setInnerHtmlUnsafe(element, html) {
  384. var _a, _b, _c;
  385. if (!ttPolicy && typeof ((_a = window == null ? undefined : window.trustedTypes) == null ? undefined : _a.createPolicy) === "function") {
  386. ttPolicy = window.trustedTypes.createPolicy("_uu_set_innerhtml_unsafe", {
  387. createHTML: (unsafeHtml) => unsafeHtml
  388. });
  389. }
  390. element.innerHTML = (_c = (_b = ttPolicy == null ? undefined : ttPolicy.createHTML) == null ? undefined : _b.call(ttPolicy, html)) != null ? _c : html;
  391. return element;
  392. }
  393. function probeElementStyle(probeStyle, element, hideOffscreen = true, parentElement = document.body) {
  394. const el = element ? typeof element === "function" ? element() : element : document.createElement("span");
  395. if (hideOffscreen) {
  396. el.style.position = "absolute";
  397. el.style.left = "-9999px";
  398. el.style.top = "-9999px";
  399. el.style.zIndex = "-9999";
  400. }
  401. el.classList.add("_uu_probe_element");
  402. parentElement.appendChild(el);
  403. const style = window.getComputedStyle(el);
  404. const result = probeStyle(style, el);
  405. setTimeout(() => el.remove(), 1);
  406. return result;
  407. }
  408. function isDomLoaded() {
  409. return domReady;
  410. }
  411. function onDomLoad(cb) {
  412. return new Promise((res) => {
  413. if (domReady) {
  414. cb == null ? undefined : cb();
  415. res();
  416. } else
  417. document.addEventListener("DOMContentLoaded", () => {
  418. cb == null ? undefined : cb();
  419. res();
  420. });
  421. });
  422. }
  423.  
  424. // lib/crypto.ts
  425. function compress(input, compressionFormat, outputType = "string") {
  426. return __async(this, null, function* () {
  427. const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
  428. const comp = new CompressionStream(compressionFormat);
  429. const writer = comp.writable.getWriter();
  430. writer.write(byteArray);
  431. writer.close();
  432. const buf = yield new Response(comp.readable).arrayBuffer();
  433. return outputType === "arrayBuffer" ? buf : ab2str(buf);
  434. });
  435. }
  436. function decompress(input, compressionFormat, outputType = "string") {
  437. return __async(this, null, function* () {
  438. const byteArray = typeof input === "string" ? str2ab(input) : input;
  439. const decomp = new DecompressionStream(compressionFormat);
  440. const writer = decomp.writable.getWriter();
  441. writer.write(byteArray);
  442. writer.close();
  443. const buf = yield new Response(decomp.readable).arrayBuffer();
  444. return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
  445. });
  446. }
  447. function ab2str(buf) {
  448. return getUnsafeWindow().btoa(
  449. new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
  450. );
  451. }
  452. function str2ab(str) {
  453. return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
  454. }
  455. function computeHash(input, algorithm = "SHA-256") {
  456. return __async(this, null, function* () {
  457. let data;
  458. if (typeof input === "string") {
  459. const encoder = new TextEncoder();
  460. data = encoder.encode(input);
  461. } else
  462. data = input;
  463. const hashBuffer = yield crypto.subtle.digest(algorithm, data);
  464. const hashArray = Array.from(new Uint8Array(hashBuffer));
  465. const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
  466. return hashHex;
  467. });
  468. }
  469. function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
  470. if (length < 1)
  471. throw new RangeError("The length argument must be at least 1");
  472. if (radix < 2 || radix > 36)
  473. throw new RangeError("The radix argument must be between 2 and 36");
  474. let arr = [];
  475. const caseArr = randomCase ? [0, 1] : [0];
  476. if (enhancedEntropy) {
  477. const uintArr = new Uint8Array(length);
  478. crypto.getRandomValues(uintArr);
  479. arr = Array.from(
  480. uintArr,
  481. (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
  482. );
  483. } else {
  484. arr = Array.from(
  485. { length },
  486. () => Math.floor(Math.random() * radix).toString(radix)
  487. );
  488. }
  489. if (!arr.some((v) => /[a-zA-Z]/.test(v)))
  490. return arr.join("");
  491. return arr.map((v) => caseArr[randRange(0, caseArr.length - 1, enhancedEntropy)] === 1 ? v.toUpperCase() : v).join("");
  492. }
  493.  
  494. // lib/DataStore.ts
  495. var DataStore = class {
  496. /**
  497. * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
  498. * Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
  499. *
  500. * - ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
  501. * - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
  502. *
  503. * @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.)
  504. * @param options The options for this DataStore instance
  505. */
  506. constructor(options) {
  507. __publicField(this, "id");
  508. __publicField(this, "formatVersion");
  509. __publicField(this, "defaultData");
  510. __publicField(this, "encodeData");
  511. __publicField(this, "decodeData");
  512. __publicField(this, "storageMethod");
  513. __publicField(this, "cachedData");
  514. __publicField(this, "migrations");
  515. __publicField(this, "migrateIds", []);
  516. var _a;
  517. this.id = options.id;
  518. this.formatVersion = options.formatVersion;
  519. this.defaultData = options.defaultData;
  520. this.cachedData = options.defaultData;
  521. this.migrations = options.migrations;
  522. if (options.migrateIds)
  523. this.migrateIds = Array.isArray(options.migrateIds) ? options.migrateIds : [options.migrateIds];
  524. this.storageMethod = (_a = options.storageMethod) != null ? _a : "GM";
  525. this.encodeData = options.encodeData;
  526. this.decodeData = options.decodeData;
  527. }
  528. //#region public
  529. /**
  530. * Loads the data saved in persistent storage into the in-memory cache and also returns it.
  531. * Automatically populates persistent storage with default data if it doesn't contain any data yet.
  532. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
  533. */
  534. loadData() {
  535. return __async(this, null, function* () {
  536. try {
  537. if (this.migrateIds.length > 0) {
  538. yield this.migrateId(this.migrateIds);
  539. this.migrateIds = [];
  540. }
  541. const gmData = yield this.getValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultData));
  542. let gmFmtVer = Number(yield this.getValue(`_uucfgver-${this.id}`, NaN));
  543. if (typeof gmData !== "string") {
  544. yield this.saveDefaultData();
  545. return __spreadValues({}, this.defaultData);
  546. }
  547. const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${this.id}`, false));
  548. let saveData = false;
  549. if (isNaN(gmFmtVer)) {
  550. yield this.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
  551. saveData = true;
  552. }
  553. let parsed = yield this.deserializeData(gmData, isEncoded);
  554. if (gmFmtVer < this.formatVersion && this.migrations)
  555. parsed = yield this.runMigrations(parsed, gmFmtVer);
  556. if (saveData)
  557. yield this.setData(parsed);
  558. this.cachedData = __spreadValues({}, parsed);
  559. return this.cachedData;
  560. } catch (err) {
  561. console.warn("Error while parsing JSON data, resetting it to the default value.", err);
  562. yield this.saveDefaultData();
  563. return this.defaultData;
  564. }
  565. });
  566. }
  567. /**
  568. * Returns a copy of the data from the in-memory cache.
  569. * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
  570. * @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
  571. */
  572. getData(deepCopy = false) {
  573. return deepCopy ? this.deepCopy(this.cachedData) : __spreadValues({}, this.cachedData);
  574. }
  575. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  576. setData(data) {
  577. this.cachedData = data;
  578. const useEncoding = this.encodingEnabled();
  579. return new Promise((resolve) => __async(this, null, function* () {
  580. yield Promise.all([
  581. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(data, useEncoding)),
  582. this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
  583. this.setValue(`_uucfgenc-${this.id}`, useEncoding)
  584. ]);
  585. resolve();
  586. }));
  587. }
  588. /** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
  589. saveDefaultData() {
  590. return __async(this, null, function* () {
  591. this.cachedData = this.defaultData;
  592. const useEncoding = this.encodingEnabled();
  593. return new Promise((resolve) => __async(this, null, function* () {
  594. yield Promise.all([
  595. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(this.defaultData, useEncoding)),
  596. this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
  597. this.setValue(`_uucfgenc-${this.id}`, useEncoding)
  598. ]);
  599. resolve();
  600. }));
  601. });
  602. }
  603. /**
  604. * Call this method to clear all persistently stored data associated with this DataStore instance.
  605. * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
  606. * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
  607. *
  608. * - ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"`
  609. */
  610. deleteData() {
  611. return __async(this, null, function* () {
  612. yield Promise.all([
  613. this.deleteValue(`_uucfg-${this.id}`),
  614. this.deleteValue(`_uucfgver-${this.id}`),
  615. this.deleteValue(`_uucfgenc-${this.id}`)
  616. ]);
  617. });
  618. }
  619. /** Returns whether encoding and decoding are enabled for this DataStore instance */
  620. encodingEnabled() {
  621. return Boolean(this.encodeData && this.decodeData);
  622. }
  623. //#region migrations
  624. /**
  625. * Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
  626. * This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
  627. * 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.
  628. *
  629. * 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.
  630. */
  631. runMigrations(oldData, oldFmtVer, resetOnError = true) {
  632. return __async(this, null, function* () {
  633. if (!this.migrations)
  634. return oldData;
  635. let newData = oldData;
  636. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  637. let lastFmtVer = oldFmtVer;
  638. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  639. const ver = Number(fmtVer);
  640. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  641. try {
  642. const migRes = migrationFunc(newData);
  643. newData = migRes instanceof Promise ? yield migRes : migRes;
  644. lastFmtVer = oldFmtVer = ver;
  645. } catch (err) {
  646. if (!resetOnError)
  647. throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
  648. yield this.saveDefaultData();
  649. return this.getData();
  650. }
  651. }
  652. }
  653. yield Promise.all([
  654. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(newData)),
  655. this.setValue(`_uucfgver-${this.id}`, lastFmtVer),
  656. this.setValue(`_uucfgenc-${this.id}`, this.encodingEnabled())
  657. ]);
  658. return this.cachedData = __spreadValues({}, newData);
  659. });
  660. }
  661. /**
  662. * Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.
  663. * 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.
  664. */
  665. migrateId(oldIds) {
  666. return __async(this, null, function* () {
  667. const ids = Array.isArray(oldIds) ? oldIds : [oldIds];
  668. yield Promise.all(ids.map((id) => __async(this, null, function* () {
  669. const data = yield this.getValue(`_uucfg-${id}`, JSON.stringify(this.defaultData));
  670. const fmtVer = Number(yield this.getValue(`_uucfgver-${id}`, NaN));
  671. const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${id}`, false));
  672. if (data === undefined || isNaN(fmtVer))
  673. return;
  674. const parsed = yield this.deserializeData(data, isEncoded);
  675. yield Promise.allSettled([
  676. this.setValue(`_uucfg-${this.id}`, yield this.serializeData(parsed)),
  677. this.setValue(`_uucfgver-${this.id}`, fmtVer),
  678. this.setValue(`_uucfgenc-${this.id}`, isEncoded),
  679. this.deleteValue(`_uucfg-${id}`),
  680. this.deleteValue(`_uucfgver-${id}`),
  681. this.deleteValue(`_uucfgenc-${id}`)
  682. ]);
  683. })));
  684. });
  685. }
  686. //#region serialization
  687. /** Serializes the data using the optional this.encodeData() and returns it as a string */
  688. serializeData(data, useEncoding = true) {
  689. return __async(this, null, function* () {
  690. const stringData = JSON.stringify(data);
  691. if (!this.encodingEnabled() || !useEncoding)
  692. return stringData;
  693. const encRes = this.encodeData(stringData);
  694. if (encRes instanceof Promise)
  695. return yield encRes;
  696. return encRes;
  697. });
  698. }
  699. /** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */
  700. deserializeData(data, useEncoding = true) {
  701. return __async(this, null, function* () {
  702. let decRes = this.encodingEnabled() && useEncoding ? this.decodeData(data) : undefined;
  703. if (decRes instanceof Promise)
  704. decRes = yield decRes;
  705. return JSON.parse(decRes != null ? decRes : data);
  706. });
  707. }
  708. /** Copies a JSON-compatible object and loses all its internal references in the process */
  709. deepCopy(obj) {
  710. return JSON.parse(JSON.stringify(obj));
  711. }
  712. //#region storage
  713. /** Gets a value from persistent storage - can be overwritten in a subclass if you want to use something other than the default storage methods */
  714. getValue(name, defaultValue) {
  715. return __async(this, null, function* () {
  716. var _a, _b;
  717. switch (this.storageMethod) {
  718. case "localStorage":
  719. return (_a = localStorage.getItem(name)) != null ? _a : defaultValue;
  720. case "sessionStorage":
  721. return (_b = sessionStorage.getItem(name)) != null ? _b : defaultValue;
  722. default:
  723. return GM.getValue(name, defaultValue);
  724. }
  725. });
  726. }
  727. /**
  728. * Sets a value in persistent storage - can be overwritten in a subclass if you want to use something other than the default storage methods.
  729. * The default storage engines will stringify all passed values like numbers or booleans, so be aware of that.
  730. */
  731. setValue(name, value) {
  732. return __async(this, null, function* () {
  733. switch (this.storageMethod) {
  734. case "localStorage":
  735. return localStorage.setItem(name, String(value));
  736. case "sessionStorage":
  737. return sessionStorage.setItem(name, String(value));
  738. default:
  739. return GM.setValue(name, String(value));
  740. }
  741. });
  742. }
  743. /** Deletes a value from persistent storage - can be overwritten in a subclass if you want to use something other than the default storage methods */
  744. deleteValue(name) {
  745. return __async(this, null, function* () {
  746. switch (this.storageMethod) {
  747. case "localStorage":
  748. return localStorage.removeItem(name);
  749. case "sessionStorage":
  750. return sessionStorage.removeItem(name);
  751. default:
  752. return GM.deleteValue(name);
  753. }
  754. });
  755. }
  756. };
  757.  
  758. // lib/DataStoreSerializer.ts
  759. var DataStoreSerializer = class _DataStoreSerializer {
  760. constructor(stores, options = {}) {
  761. __publicField(this, "stores");
  762. __publicField(this, "options");
  763. if (!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
  764. throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
  765. this.stores = stores;
  766. this.options = __spreadValues({
  767. addChecksum: true,
  768. ensureIntegrity: true
  769. }, options);
  770. }
  771. /** Calculates the checksum of a string */
  772. calcChecksum(input) {
  773. return __async(this, null, function* () {
  774. return computeHash(input, "SHA-256");
  775. });
  776. }
  777. /**
  778. * Serializes only a subset of the data stores into a string.
  779. * @param stores An array of store IDs or functions that take a store ID and return a boolean
  780. * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
  781. * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
  782. */
  783. serializePartial(stores, useEncoding = true, stringified = true) {
  784. return __async(this, null, function* () {
  785. const serData = [];
  786. for (const storeInst of this.stores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) {
  787. const data = useEncoding && storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
  788. serData.push({
  789. id: storeInst.id,
  790. data,
  791. formatVersion: storeInst.formatVersion,
  792. encoded: useEncoding && storeInst.encodingEnabled(),
  793. checksum: this.options.addChecksum ? yield this.calcChecksum(data) : undefined
  794. });
  795. }
  796. return stringified ? JSON.stringify(serData) : serData;
  797. });
  798. }
  799. /**
  800. * Serializes the data stores into a string.
  801. * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
  802. * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
  803. */
  804. serialize(useEncoding = true, stringified = true) {
  805. return __async(this, null, function* () {
  806. return this.serializePartial(this.stores.map((s) => s.id), useEncoding, stringified);
  807. });
  808. }
  809. /**
  810. * Deserializes the data exported via {@linkcode serialize()} and imports only a subset into the DataStore instances.
  811. * Also triggers the migration process if the data format has changed.
  812. */
  813. deserializePartial(stores, data) {
  814. return __async(this, null, function* () {
  815. const deserStores = typeof data === "string" ? JSON.parse(data) : data;
  816. if (!Array.isArray(deserStores) || !deserStores.every(_DataStoreSerializer.isSerializedDataStoreObj))
  817. throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects.");
  818. for (const storeData of deserStores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) {
  819. const storeInst = this.stores.find((s) => s.id === storeData.id);
  820. if (!storeInst)
  821. throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
  822. if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
  823. const checksum = yield this.calcChecksum(storeData.data);
  824. if (checksum !== storeData.checksum)
  825. throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"!
  826. Expected: ${storeData.checksum}
  827. Has: ${checksum}`);
  828. }
  829. const decodedData = storeData.encoded && storeInst.encodingEnabled() ? yield storeInst.decodeData(storeData.data) : storeData.data;
  830. if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
  831. yield storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
  832. else
  833. yield storeInst.setData(JSON.parse(decodedData));
  834. }
  835. });
  836. }
  837. /**
  838. * Deserializes the data exported via {@linkcode serialize()} and imports the data into all matching DataStore instances.
  839. * Also triggers the migration process if the data format has changed.
  840. */
  841. deserialize(data) {
  842. return __async(this, null, function* () {
  843. return this.deserializePartial(this.stores.map((s) => s.id), data);
  844. });
  845. }
  846. /**
  847. * Loads the persistent data of the DataStore instances into the in-memory cache.
  848. * Also triggers the migration process if the data format has changed.
  849. * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be loaded
  850. * @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }`
  851. */
  852. loadStoresData(stores) {
  853. return __async(this, null, function* () {
  854. return Promise.allSettled(
  855. this.getStoresFiltered(stores).map((store) => __async(this, null, function* () {
  856. return {
  857. id: store.id,
  858. data: yield store.loadData()
  859. };
  860. }))
  861. );
  862. });
  863. }
  864. /**
  865. * Resets the persistent and in-memory data of the DataStore instances to their default values.
  866. * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be affected
  867. */
  868. resetStoresData(stores) {
  869. return __async(this, null, function* () {
  870. return Promise.allSettled(
  871. this.getStoresFiltered(stores).map((store) => store.saveDefaultData())
  872. );
  873. });
  874. }
  875. /**
  876. * Deletes the persistent data of the DataStore instances.
  877. * Leaves the in-memory data untouched.
  878. * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be affected
  879. */
  880. deleteStoresData(stores) {
  881. return __async(this, null, function* () {
  882. return Promise.allSettled(
  883. this.getStoresFiltered(stores).map((store) => store.deleteData())
  884. );
  885. });
  886. }
  887. /** Checks if a given value is an array of SerializedDataStore objects */
  888. static isSerializedDataStoreObjArray(obj) {
  889. return Array.isArray(obj) && obj.every((o) => typeof o === "object" && o !== null && "id" in o && "data" in o && "formatVersion" in o && "encoded" in o);
  890. }
  891. /** Checks if a given value is a SerializedDataStore object */
  892. static isSerializedDataStoreObj(obj) {
  893. return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj;
  894. }
  895. /** Returns the DataStore instances whose IDs match the provided array or function */
  896. getStoresFiltered(stores) {
  897. return this.stores.filter((s) => typeof stores === "undefined" ? true : Array.isArray(stores) ? stores.includes(s.id) : stores(s.id));
  898. }
  899. };
  900.  
  901. // node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js
  902. var createNanoEvents = () => ({
  903. emit(event, ...args) {
  904. for (let callbacks = this.events[event] || [], i = 0, length = callbacks.length; i < length; i++) {
  905. callbacks[i](...args);
  906. }
  907. },
  908. events: {},
  909. on(event, cb) {
  910. var _a;
  911. ((_a = this.events)[event] || (_a[event] = [])).push(cb);
  912. return () => {
  913. var _a2;
  914. this.events[event] = (_a2 = this.events[event]) == null ? undefined : _a2.filter((i) => cb !== i);
  915. };
  916. }
  917. });
  918.  
  919. // lib/NanoEmitter.ts
  920. var NanoEmitter = class {
  921. /** Creates a new instance of NanoEmitter - a lightweight event emitter with helper methods and a strongly typed event map */
  922. constructor(options = {}) {
  923. __publicField(this, "events", createNanoEvents());
  924. __publicField(this, "eventUnsubscribes", []);
  925. __publicField(this, "emitterOptions");
  926. this.emitterOptions = __spreadValues({
  927. publicEmit: false
  928. }, options);
  929. }
  930. /**
  931. * Subscribes to an event and calls the callback when it's emitted.
  932. * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_")
  933. * @returns Returns a function that can be called to unsubscribe the event listener
  934. * @example ```ts
  935. * const emitter = new NanoEmitter<{
  936. * foo: (bar: string) => void;
  937. * }>({
  938. * publicEmit: true,
  939. * });
  940. *
  941. * let i = 0;
  942. * const unsub = emitter.on("foo", (bar) => {
  943. * // unsubscribe after 10 events:
  944. * if(++i === 10) unsub();
  945. * console.log(bar);
  946. * });
  947. *
  948. * emitter.emit("foo", "bar");
  949. * ```
  950. */
  951. on(event, cb) {
  952. let unsub;
  953. const unsubProxy = () => {
  954. if (!unsub)
  955. return;
  956. unsub();
  957. this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => u !== unsub);
  958. };
  959. unsub = this.events.on(event, cb);
  960. this.eventUnsubscribes.push(unsub);
  961. return unsubProxy;
  962. }
  963. /**
  964. * Subscribes to an event and calls the callback or resolves the Promise only once when it's emitted.
  965. * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_")
  966. * @param cb The callback to call when the event is emitted - if provided or not, the returned Promise will resolve with the event arguments
  967. * @returns Returns a Promise that resolves with the event arguments when the event is emitted
  968. * @example ```ts
  969. * const emitter = new NanoEmitter<{
  970. * foo: (bar: string) => void;
  971. * }>();
  972. *
  973. * // Promise syntax:
  974. * const [bar] = await emitter.once("foo");
  975. * console.log(bar);
  976. *
  977. * // Callback syntax:
  978. * emitter.once("foo", (bar) => console.log(bar));
  979. * ```
  980. */
  981. once(event, cb) {
  982. return new Promise((resolve) => {
  983. let unsub;
  984. const onceProxy = (...args) => {
  985. cb == null ? undefined : cb(...args);
  986. unsub == null ? undefined : unsub();
  987. resolve(args);
  988. };
  989. unsub = this.events.on(event, onceProxy);
  990. this.eventUnsubscribes.push(unsub);
  991. });
  992. }
  993. /**
  994. * Emits an event on this instance.
  995. * ⚠️ Needs `publicEmit` to be set to true in the NanoEmitter constructor or super() call!
  996. * @param event The event to emit
  997. * @param args The arguments to pass to the event listeners
  998. * @returns Returns true if `publicEmit` is true and the event was emitted successfully
  999. */
  1000. emit(event, ...args) {
  1001. if (this.emitterOptions.publicEmit) {
  1002. this.events.emit(event, ...args);
  1003. return true;
  1004. }
  1005. return false;
  1006. }
  1007. /** Unsubscribes all event listeners from this instance */
  1008. unsubscribeAll() {
  1009. for (const unsub of this.eventUnsubscribes)
  1010. unsub();
  1011. this.eventUnsubscribes = [];
  1012. }
  1013. };
  1014.  
  1015. // lib/Debouncer.ts
  1016. var Debouncer = class extends NanoEmitter {
  1017. /**
  1018. * Creates a new debouncer with the specified timeout and edge type.
  1019. * @param timeout Timeout in milliseconds between letting through calls - defaults to 200
  1020. * @param type The edge type to use for the debouncer - see {@linkcode DebouncerType} for details or [the documentation for an explanation and diagram](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer) - defaults to "immediate"
  1021. */
  1022. constructor(timeout = 200, type = "immediate") {
  1023. super();
  1024. this.timeout = timeout;
  1025. this.type = type;
  1026. /** All registered listener functions and the time they were attached */
  1027. __publicField(this, "listeners", []);
  1028. /** The currently active timeout */
  1029. __publicField(this, "activeTimeout");
  1030. /** The latest queued call */
  1031. __publicField(this, "queuedCall");
  1032. }
  1033. //#region listeners
  1034. /** Adds a listener function that will be called on timeout */
  1035. addListener(fn) {
  1036. this.listeners.push(fn);
  1037. }
  1038. /** Removes the listener with the specified function reference */
  1039. removeListener(fn) {
  1040. const idx = this.listeners.findIndex((l) => l === fn);
  1041. idx !== -1 && this.listeners.splice(idx, 1);
  1042. }
  1043. /** Removes all listeners */
  1044. removeAllListeners() {
  1045. this.listeners = [];
  1046. }
  1047. /** Returns all registered listeners */
  1048. getListeners() {
  1049. return this.listeners;
  1050. }
  1051. //#region timeout
  1052. /** Sets the timeout for the debouncer */
  1053. setTimeout(timeout) {
  1054. this.emit("change", this.timeout = timeout, this.type);
  1055. }
  1056. /** Returns the current timeout */
  1057. getTimeout() {
  1058. return this.timeout;
  1059. }
  1060. /** Whether the timeout is currently active, meaning any latest call to the {@linkcode call()} method will be queued */
  1061. isTimeoutActive() {
  1062. return typeof this.activeTimeout !== "undefined";
  1063. }
  1064. //#region type
  1065. /** Sets the edge type for the debouncer */
  1066. setType(type) {
  1067. this.emit("change", this.timeout, this.type = type);
  1068. }
  1069. /** Returns the current edge type */
  1070. getType() {
  1071. return this.type;
  1072. }
  1073. //#region call
  1074. /** Use this to call the debouncer with the specified arguments that will be passed to all listener functions registered with {@linkcode addListener()} */
  1075. call(...args) {
  1076. const cl = (...a) => {
  1077. this.queuedCall = undefined;
  1078. this.emit("call", ...a);
  1079. this.listeners.forEach((l) => l.call(this, ...a));
  1080. };
  1081. const setRepeatTimeout = () => {
  1082. this.activeTimeout = setTimeout(() => {
  1083. if (this.queuedCall) {
  1084. this.queuedCall();
  1085. setRepeatTimeout();
  1086. } else
  1087. this.activeTimeout = undefined;
  1088. }, this.timeout);
  1089. };
  1090. switch (this.type) {
  1091. case "immediate":
  1092. if (typeof this.activeTimeout === "undefined") {
  1093. cl(...args);
  1094. setRepeatTimeout();
  1095. } else
  1096. this.queuedCall = () => cl(...args);
  1097. break;
  1098. case "idle":
  1099. if (this.activeTimeout)
  1100. clearTimeout(this.activeTimeout);
  1101. this.activeTimeout = setTimeout(() => {
  1102. cl(...args);
  1103. this.activeTimeout = undefined;
  1104. }, this.timeout);
  1105. break;
  1106. default:
  1107. throw new TypeError(`Invalid debouncer type: ${this.type}`);
  1108. }
  1109. }
  1110. };
  1111. function debounce(fn, timeout = 200, type = "immediate") {
  1112. const debouncer = new Debouncer(timeout, type);
  1113. debouncer.addListener(fn);
  1114. const func = (...args) => debouncer.call(...args);
  1115. func.debouncer = debouncer;
  1116. return func;
  1117. }
  1118.  
  1119. // lib/Dialog.ts
  1120. var defaultDialogCss = `.uu-no-select {
  1121. user-select: none;
  1122. }
  1123.  
  1124. .uu-dialog-bg {
  1125. --uu-dialog-bg: #333333;
  1126. --uu-dialog-bg-highlight: #252525;
  1127. --uu-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
  1128. --uu-dialog-separator-color: #797979;
  1129. --uu-dialog-border-radius: 10px;
  1130. }
  1131.  
  1132. .uu-dialog-bg {
  1133. display: block;
  1134. position: fixed;
  1135. width: 100%;
  1136. height: 100%;
  1137. top: 0;
  1138. left: 0;
  1139. z-index: 5;
  1140. background-color: rgba(0, 0, 0, 0.6);
  1141. }
  1142.  
  1143. .uu-dialog {
  1144. --uu-calc-dialog-height: calc(min(100vh - 40px, var(--uu-dialog-height-max)));
  1145. position: absolute;
  1146. display: flex;
  1147. flex-direction: column;
  1148. width: calc(min(100% - 60px, var(--uu-dialog-width-max)));
  1149. border-radius: var(--uu-dialog-border-radius);
  1150. height: auto;
  1151. max-height: var(--uu-calc-dialog-height);
  1152. left: 50%;
  1153. top: 50%;
  1154. transform: translate(-50%, -50%);
  1155. z-index: 6;
  1156. color: #fff;
  1157. background-color: var(--uu-dialog-bg);
  1158. }
  1159.  
  1160. .uu-dialog.align-top {
  1161. top: 0;
  1162. transform: translate(-50%, 40px);
  1163. }
  1164.  
  1165. .uu-dialog.align-bottom {
  1166. top: 100%;
  1167. transform: translate(-50%, -100%);
  1168. }
  1169.  
  1170. .uu-dialog-body {
  1171. font-size: 1.5rem;
  1172. padding: 20px;
  1173. }
  1174.  
  1175. .uu-dialog-body.small {
  1176. padding: 15px;
  1177. }
  1178.  
  1179. #uu-dialog-opts {
  1180. display: flex;
  1181. flex-direction: column;
  1182. position: relative;
  1183. padding: 30px 0px;
  1184. overflow-y: auto;
  1185. }
  1186.  
  1187. .uu-dialog-header {
  1188. display: flex;
  1189. justify-content: space-between;
  1190. align-items: center;
  1191. margin-bottom: 6px;
  1192. padding: 15px 20px 15px 20px;
  1193. background-color: var(--uu-dialog-bg);
  1194. border: 2px solid var(--uu-dialog-separator-color);
  1195. border-style: none none solid none !important;
  1196. border-radius: var(--uu-dialog-border-radius) var(--uu-dialog-border-radius) 0px 0px;
  1197. }
  1198.  
  1199. .uu-dialog-header.small {
  1200. padding: 10px 15px;
  1201. border-style: none none solid none !important;
  1202. }
  1203.  
  1204. .uu-dialog-header-pad {
  1205. content: " ";
  1206. min-height: 32px;
  1207. }
  1208.  
  1209. .uu-dialog-header-pad.small {
  1210. min-height: 24px;
  1211. }
  1212.  
  1213. .uu-dialog-titlecont {
  1214. display: flex;
  1215. align-items: center;
  1216. }
  1217.  
  1218. .uu-dialog-titlecont-no-title {
  1219. display: flex;
  1220. justify-content: flex-end;
  1221. align-items: center;
  1222. }
  1223.  
  1224. .uu-dialog-title {
  1225. position: relative;
  1226. display: inline-block;
  1227. font-size: 22px;
  1228. }
  1229.  
  1230. .uu-dialog-close {
  1231. cursor: pointer;
  1232. }
  1233.  
  1234. .uu-dialog-header-img,
  1235. .uu-dialog-close
  1236. {
  1237. width: 32px;
  1238. height: 32px;
  1239. }
  1240.  
  1241. .uu-dialog-header-img.small,
  1242. .uu-dialog-close.small
  1243. {
  1244. width: 24px;
  1245. height: 24px;
  1246. }
  1247.  
  1248. .uu-dialog-footer {
  1249. font-size: 17px;
  1250. text-decoration: underline;
  1251. }
  1252.  
  1253. .uu-dialog-footer.hidden {
  1254. display: none;
  1255. }
  1256.  
  1257. .uu-dialog-footer-cont {
  1258. margin-top: 6px;
  1259. padding: 15px 20px;
  1260. background: var(--uu-dialog-bg);
  1261. background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--uu-dialog-bg) 30%, var(--uu-dialog-bg) 100%);
  1262. border: 2px solid var(--uu-dialog-separator-color);
  1263. border-style: solid none none none !important;
  1264. border-radius: 0px 0px var(--uu-dialog-border-radius) var(--uu-dialog-border-radius);
  1265. }
  1266.  
  1267. .uu-dialog-footer-buttons-cont button:not(:last-of-type) {
  1268. margin-right: 15px;
  1269. }`;
  1270. exports.currentDialogId = null;
  1271. var openDialogs = [];
  1272. var defaultStrings = {
  1273. closeDialogTooltip: "Click to close the dialog"
  1274. };
  1275. var Dialog = class _Dialog extends NanoEmitter {
  1276. constructor(options) {
  1277. super();
  1278. /** Options passed to the dialog in the constructor */
  1279. __publicField(this, "options");
  1280. /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
  1281. __publicField(this, "id");
  1282. /** Strings used in the dialog (used for translations) */
  1283. __publicField(this, "strings");
  1284. __publicField(this, "dialogOpen", false);
  1285. __publicField(this, "dialogMounted", false);
  1286. const _a = options, { strings } = _a, opts = __objRest(_a, ["strings"]);
  1287. this.strings = __spreadValues(__spreadValues({}, defaultStrings), strings != null ? strings : {});
  1288. this.options = __spreadValues({
  1289. closeOnBgClick: true,
  1290. closeOnEscPress: true,
  1291. destroyOnClose: false,
  1292. unmountOnClose: true,
  1293. removeListenersOnDestroy: true,
  1294. small: false,
  1295. verticalAlign: "center"
  1296. }, opts);
  1297. this.id = opts.id;
  1298. }
  1299. //#region public
  1300. /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
  1301. mount() {
  1302. return __async(this, null, function* () {
  1303. var _a;
  1304. if (this.dialogMounted)
  1305. return;
  1306. this.dialogMounted = true;
  1307. if (!document.querySelector("style.uu-dialog-css"))
  1308. addGlobalStyle((_a = this.options.dialogCss) != null ? _a : defaultDialogCss).classList.add("uu-dialog-css");
  1309. const bgElem = document.createElement("div");
  1310. bgElem.id = `uu-${this.id}-dialog-bg`;
  1311. bgElem.classList.add("uu-dialog-bg");
  1312. if (this.options.closeOnBgClick)
  1313. bgElem.ariaLabel = bgElem.title = this.getString("closeDialogTooltip");
  1314. bgElem.style.setProperty("--uu-dialog-width-max", `${this.options.width}px`);
  1315. bgElem.style.setProperty("--uu-dialog-height-max", `${this.options.height}px`);
  1316. bgElem.style.visibility = "hidden";
  1317. bgElem.style.display = "none";
  1318. bgElem.inert = true;
  1319. bgElem.appendChild(yield this.getDialogContent());
  1320. document.body.appendChild(bgElem);
  1321. this.attachListeners(bgElem);
  1322. this.events.emit("render");
  1323. return bgElem;
  1324. });
  1325. }
  1326. /** Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call */
  1327. unmount() {
  1328. var _a;
  1329. this.close();
  1330. this.dialogMounted = false;
  1331. const clearSelectors = [
  1332. `#uu-${this.id}-dialog-bg`,
  1333. `#uu-style-dialog-${this.id}`
  1334. ];
  1335. for (const sel of clearSelectors)
  1336. (_a = document.querySelector(sel)) == null ? undefined : _a.remove();
  1337. this.events.emit("clear");
  1338. }
  1339. /** Clears the DOM of the dialog and then renders it again */
  1340. remount() {
  1341. return __async(this, null, function* () {
  1342. this.unmount();
  1343. yield this.mount();
  1344. });
  1345. }
  1346. /**
  1347. * Opens the dialog - also mounts it if it hasn't been mounted yet
  1348. * Prevents default action and immediate propagation of the passed event
  1349. */
  1350. open(e) {
  1351. return __async(this, null, function* () {
  1352. var _a;
  1353. e == null ? undefined : e.preventDefault();
  1354. e == null ? undefined : e.stopImmediatePropagation();
  1355. if (this.isOpen())
  1356. return;
  1357. this.dialogOpen = true;
  1358. if (openDialogs.includes(this.id))
  1359. throw new Error(`A dialog with the same ID of '${this.id}' already exists and is open!`);
  1360. if (!this.isMounted())
  1361. yield this.mount();
  1362. const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
  1363. if (!dialogBg)
  1364. return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
  1365. dialogBg.style.visibility = "visible";
  1366. dialogBg.style.display = "block";
  1367. dialogBg.inert = false;
  1368. exports.currentDialogId = this.id;
  1369. openDialogs.unshift(this.id);
  1370. for (const dialogId of openDialogs)
  1371. if (dialogId !== this.id)
  1372. (_a = document.querySelector(`#uu-${dialogId}-dialog-bg`)) == null ? undefined : _a.setAttribute("inert", "true");
  1373. document.body.classList.remove("uu-no-select");
  1374. document.body.setAttribute("inert", "true");
  1375. this.events.emit("open");
  1376. return dialogBg;
  1377. });
  1378. }
  1379. /** Closes the dialog - prevents default action and immediate propagation of the passed event */
  1380. close(e) {
  1381. var _a, _b;
  1382. e == null ? undefined : e.preventDefault();
  1383. e == null ? undefined : e.stopImmediatePropagation();
  1384. if (!this.isOpen())
  1385. return;
  1386. this.dialogOpen = false;
  1387. const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
  1388. if (!dialogBg)
  1389. return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
  1390. dialogBg.style.visibility = "hidden";
  1391. dialogBg.style.display = "none";
  1392. dialogBg.inert = true;
  1393. openDialogs.splice(openDialogs.indexOf(this.id), 1);
  1394. exports.currentDialogId = (_a = openDialogs[0]) != null ? _a : null;
  1395. if (exports.currentDialogId)
  1396. (_b = document.querySelector(`#uu-${exports.currentDialogId}-dialog-bg`)) == null ? undefined : _b.removeAttribute("inert");
  1397. if (openDialogs.length === 0) {
  1398. document.body.classList.add("uu-no-select");
  1399. document.body.removeAttribute("inert");
  1400. }
  1401. this.events.emit("close");
  1402. if (this.options.destroyOnClose)
  1403. this.destroy();
  1404. else if (this.options.unmountOnClose)
  1405. this.unmount();
  1406. }
  1407. /** Returns true if the dialog is currently open */
  1408. isOpen() {
  1409. return this.dialogOpen;
  1410. }
  1411. /** Returns true if the dialog is currently mounted */
  1412. isMounted() {
  1413. return this.dialogMounted;
  1414. }
  1415. /** Clears the DOM of the dialog and removes all event listeners */
  1416. destroy() {
  1417. this.unmount();
  1418. this.events.emit("destroy");
  1419. this.options.removeListenersOnDestroy && this.unsubscribeAll();
  1420. }
  1421. //#region static
  1422. /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
  1423. static getCurrentDialogId() {
  1424. return exports.currentDialogId;
  1425. }
  1426. /** Returns the IDs of all currently open dialogs, top-most first */
  1427. static getOpenDialogs() {
  1428. return openDialogs;
  1429. }
  1430. //#region protected
  1431. getString(key) {
  1432. var _a;
  1433. return (_a = this.strings[key]) != null ? _a : defaultStrings[key];
  1434. }
  1435. /** Called once to attach all generic event listeners */
  1436. attachListeners(bgElem) {
  1437. if (this.options.closeOnBgClick) {
  1438. bgElem.addEventListener("click", (e) => {
  1439. var _a;
  1440. if (this.isOpen() && ((_a = e.target) == null ? undefined : _a.id) === `uu-${this.id}-dialog-bg`)
  1441. this.close(e);
  1442. });
  1443. }
  1444. if (this.options.closeOnEscPress) {
  1445. document.body.addEventListener("keydown", (e) => {
  1446. if (e.key === "Escape" && this.isOpen() && _Dialog.getCurrentDialogId() === this.id)
  1447. this.close(e);
  1448. });
  1449. }
  1450. }
  1451. //#region protected
  1452. /**
  1453. * Adds generic, accessible interaction listeners to the passed element.
  1454. * All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
  1455. * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
  1456. */
  1457. onInteraction(elem, listener, listenerOptions) {
  1458. const _a = listenerOptions != null ? listenerOptions : {}, { preventDefault = true, stopPropagation = true } = _a, listenerOpts = __objRest(_a, ["preventDefault", "stopPropagation"]);
  1459. const interactionKeys = ["Enter", " ", "Space"];
  1460. const proxListener = (e) => {
  1461. if (e instanceof KeyboardEvent) {
  1462. if (interactionKeys.includes(e.key)) {
  1463. preventDefault && e.preventDefault();
  1464. stopPropagation && e.stopPropagation();
  1465. } else return;
  1466. } else if (e instanceof MouseEvent) {
  1467. preventDefault && e.preventDefault();
  1468. stopPropagation && e.stopPropagation();
  1469. }
  1470. (listenerOpts == null ? undefined : listenerOpts.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOpts);
  1471. (listenerOpts == null ? undefined : listenerOpts.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOpts);
  1472. listener(e);
  1473. };
  1474. elem.addEventListener("click", proxListener, listenerOpts);
  1475. elem.addEventListener("keydown", proxListener, listenerOpts);
  1476. }
  1477. /** Returns the dialog content element and all its children */
  1478. getDialogContent() {
  1479. return __async(this, null, function* () {
  1480. var _a, _b, _c, _d;
  1481. const header = (_b = (_a = this.options).renderHeader) == null ? undefined : _b.call(_a);
  1482. const footer = (_d = (_c = this.options).renderFooter) == null ? undefined : _d.call(_c);
  1483. const dialogWrapperEl = document.createElement("div");
  1484. dialogWrapperEl.id = `uu-${this.id}-dialog`;
  1485. dialogWrapperEl.classList.add("uu-dialog");
  1486. dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
  1487. dialogWrapperEl.role = "dialog";
  1488. dialogWrapperEl.setAttribute("aria-labelledby", `uu-${this.id}-dialog-title`);
  1489. dialogWrapperEl.setAttribute("aria-describedby", `uu-${this.id}-dialog-body`);
  1490. if (this.options.verticalAlign !== "center")
  1491. dialogWrapperEl.classList.add(`align-${this.options.verticalAlign}`);
  1492. const headerWrapperEl = document.createElement("div");
  1493. headerWrapperEl.classList.add("uu-dialog-header");
  1494. this.options.small && headerWrapperEl.classList.add("small");
  1495. if (header) {
  1496. const headerTitleWrapperEl = document.createElement("div");
  1497. headerTitleWrapperEl.id = `uu-${this.id}-dialog-title`;
  1498. headerTitleWrapperEl.classList.add("uu-dialog-title-wrapper");
  1499. headerTitleWrapperEl.role = "heading";
  1500. headerTitleWrapperEl.ariaLevel = "1";
  1501. headerTitleWrapperEl.appendChild(header instanceof Promise ? yield header : header);
  1502. headerWrapperEl.appendChild(headerTitleWrapperEl);
  1503. } else {
  1504. const padEl = document.createElement("div");
  1505. padEl.classList.add("uu-dialog-header-pad", this.options.small ? "small" : "");
  1506. headerWrapperEl.appendChild(padEl);
  1507. }
  1508. if (this.options.renderCloseBtn) {
  1509. const closeBtnEl = yield this.options.renderCloseBtn();
  1510. closeBtnEl.classList.add("uu-dialog-close");
  1511. this.options.small && closeBtnEl.classList.add("small");
  1512. closeBtnEl.tabIndex = 0;
  1513. if (closeBtnEl.hasAttribute("alt"))
  1514. closeBtnEl.setAttribute("alt", this.getString("closeDialogTooltip"));
  1515. closeBtnEl.title = closeBtnEl.ariaLabel = this.getString("closeDialogTooltip");
  1516. this.onInteraction(closeBtnEl, () => this.close());
  1517. headerWrapperEl.appendChild(closeBtnEl);
  1518. }
  1519. dialogWrapperEl.appendChild(headerWrapperEl);
  1520. const dialogBodyElem = document.createElement("div");
  1521. dialogBodyElem.id = `uu-${this.id}-dialog-body`;
  1522. dialogBodyElem.classList.add("uu-dialog-body");
  1523. this.options.small && dialogBodyElem.classList.add("small");
  1524. const body = this.options.renderBody();
  1525. dialogBodyElem.appendChild(body instanceof Promise ? yield body : body);
  1526. dialogWrapperEl.appendChild(dialogBodyElem);
  1527. if (footer) {
  1528. const footerWrapper = document.createElement("div");
  1529. footerWrapper.classList.add("uu-dialog-footer-cont");
  1530. dialogWrapperEl.appendChild(footerWrapper);
  1531. footerWrapper.appendChild(footer instanceof Promise ? yield footer : footer);
  1532. }
  1533. return dialogWrapperEl;
  1534. });
  1535. }
  1536. };
  1537.  
  1538. // lib/misc.ts
  1539. function autoPlural(term, num, pluralType = "auto") {
  1540. let n = num;
  1541. if (typeof n !== "number")
  1542. n = getListLength(n, false);
  1543. if (!["-s", "-ies"].includes(pluralType))
  1544. pluralType = "auto";
  1545. if (isNaN(n))
  1546. n = 2;
  1547. const pType = pluralType === "auto" ? String(term).endsWith("y") ? "-ies" : "-s" : pluralType;
  1548. switch (pType) {
  1549. case "-s":
  1550. return `${term}${n === 1 ? "" : "s"}`;
  1551. case "-ies":
  1552. return `${String(term).slice(0, -1)}${n === 1 ? "y" : "ies"}`;
  1553. }
  1554. }
  1555. function insertValues(input, ...values) {
  1556. return input.replace(/%\d/gm, (match) => {
  1557. var _a, _b;
  1558. const argIndex = Number(match.substring(1)) - 1;
  1559. return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? undefined : _b.toString();
  1560. });
  1561. }
  1562. function pauseFor(time, signal, rejectOnAbort = false) {
  1563. return new Promise((res, rej) => {
  1564. const timeout = setTimeout(() => res(), time);
  1565. signal == null ? undefined : signal.addEventListener("abort", () => {
  1566. clearTimeout(timeout);
  1567. rejectOnAbort ? rej(new Error("The pause was aborted")) : res();
  1568. });
  1569. });
  1570. }
  1571. function fetchAdvanced(_0) {
  1572. return __async(this, arguments, function* (input, options = {}) {
  1573. const { timeout = 1e4 } = options;
  1574. const ctl = new AbortController();
  1575. const _a = options, { signal } = _a, restOpts = __objRest(_a, ["signal"]);
  1576. signal == null ? undefined : signal.addEventListener("abort", () => ctl.abort());
  1577. let sigOpts = {}, id = undefined;
  1578. if (timeout >= 0) {
  1579. id = setTimeout(() => ctl.abort(), timeout);
  1580. sigOpts = { signal: ctl.signal };
  1581. }
  1582. try {
  1583. const res = yield fetch(input, __spreadValues(__spreadValues({}, restOpts), sigOpts));
  1584. typeof id !== "undefined" && clearTimeout(id);
  1585. return res;
  1586. } catch (err) {
  1587. typeof id !== "undefined" && clearTimeout(id);
  1588. throw new Error("Error while calling fetch", { cause: err });
  1589. }
  1590. });
  1591. }
  1592. function consumeGen(valGen) {
  1593. return __async(this, null, function* () {
  1594. return yield typeof valGen === "function" ? valGen() : valGen;
  1595. });
  1596. }
  1597. function consumeStringGen(strGen) {
  1598. return __async(this, null, function* () {
  1599. return typeof strGen === "string" ? strGen : String(
  1600. typeof strGen === "function" ? yield strGen() : strGen
  1601. );
  1602. });
  1603. }
  1604. function getListLength(obj, zeroOnInvalid = true) {
  1605. return "length" in obj ? obj.length : "size" in obj ? obj.size : "count" in obj ? obj.count : zeroOnInvalid ? 0 : NaN;
  1606. }
  1607. function purifyObj(obj) {
  1608. return Object.assign(/* @__PURE__ */ Object.create(null), obj);
  1609. }
  1610.  
  1611. // lib/Mixins.ts
  1612. var Mixins = class {
  1613. /**
  1614. * Creates a new Mixins instance.
  1615. * @param config Configuration object to customize the behavior.
  1616. */
  1617. constructor(config = {}) {
  1618. /** List of all registered mixins */
  1619. __publicField(this, "mixins", []);
  1620. /** Default configuration object for mixins */
  1621. __publicField(this, "defaultMixinCfg");
  1622. /** Whether the priorities should auto-increment if not specified */
  1623. __publicField(this, "autoIncPrioEnabled");
  1624. /** The current auto-increment priority counter */
  1625. __publicField(this, "autoIncPrioCounter", /* @__PURE__ */ new Map());
  1626. var _a, _b, _c;
  1627. this.defaultMixinCfg = purifyObj({
  1628. priority: (_a = config.defaultPriority) != null ? _a : 0,
  1629. stopPropagation: (_b = config.defaultStopPropagation) != null ? _b : false,
  1630. signal: config.defaultSignal
  1631. });
  1632. this.autoIncPrioEnabled = (_c = config.autoIncrementPriority) != null ? _c : false;
  1633. }
  1634. //#region public
  1635. /**
  1636. * Adds a mixin function to the given {@linkcode mixinKey}.
  1637. * If no priority is specified, it will be calculated via the protected method {@linkcode calcPriority()} based on the constructor configuration, or fall back to the default priority.
  1638. * @param mixinKey The key to identify the mixin function.
  1639. * @param mixinFn The function to be called to apply the mixin. The first argument is the input value, the second argument is the context object (if any).
  1640. * @param config Configuration object to customize the mixin behavior, or just the priority if a number is passed.
  1641. * @returns Returns a cleanup function, to be called when this mixin is no longer needed.
  1642. */
  1643. add(mixinKey, mixinFn, config = purifyObj({})) {
  1644. const calcPrio = typeof config === "number" ? config : this.calcPriority(mixinKey, config);
  1645. const mixin = purifyObj(__spreadValues(__spreadValues(__spreadProps(__spreadValues({}, this.defaultMixinCfg), {
  1646. key: mixinKey,
  1647. fn: mixinFn
  1648. }), typeof config === "object" ? config : {}), typeof calcPrio === "number" && !isNaN(calcPrio) ? { priority: calcPrio } : {}));
  1649. this.mixins.push(mixin);
  1650. const rem = () => {
  1651. this.mixins = this.mixins.filter((m) => m !== mixin);
  1652. };
  1653. if (mixin.signal)
  1654. mixin.signal.addEventListener("abort", rem, { once: true });
  1655. return rem;
  1656. }
  1657. /** Returns a list of all added mixins with their keys and configuration objects, but not their functions */
  1658. list() {
  1659. return this.mixins.map((_a) => {
  1660. var _b = _a, rest = __objRest(_b, ["fn"]);
  1661. return rest;
  1662. });
  1663. }
  1664. /**
  1665. * Applies all mixins with the given key to the input value, respecting the priority and stopPropagation settings.
  1666. * If additional context is set in the MixinMap, it will need to be passed as the third argument.
  1667. * @returns The modified value after all mixins have been applied.
  1668. */
  1669. resolve(mixinKey, inputValue, ...inputCtx) {
  1670. const mixins = this.mixins.filter((m) => m.key === mixinKey);
  1671. const sortedMixins = [...mixins].sort((a, b) => b.priority - a.priority);
  1672. let result = inputValue;
  1673. for (let i = 0; i < sortedMixins.length; i++) {
  1674. const mixin = sortedMixins[i];
  1675. result = mixin.fn(result, ...inputCtx);
  1676. if (result instanceof Promise) {
  1677. return (() => __async(this, null, function* () {
  1678. result = yield result;
  1679. if (mixin.stopPropagation)
  1680. return result;
  1681. for (let j = i + 1; j < sortedMixins.length; j++) {
  1682. const mixin2 = sortedMixins[j];
  1683. result = yield mixin2.fn(result, ...inputCtx);
  1684. if (mixin2.stopPropagation)
  1685. break;
  1686. }
  1687. return result;
  1688. }))();
  1689. } else if (mixin.stopPropagation)
  1690. break;
  1691. }
  1692. return result;
  1693. }
  1694. //#region protected
  1695. /** Calculates the priority for a mixin based on the given configuration and the current auto-increment state of the instance */
  1696. calcPriority(mixinKey, config) {
  1697. var _a;
  1698. if (config.priority !== undefined)
  1699. return undefined;
  1700. if (!this.autoIncPrioEnabled)
  1701. return (_a = config.priority) != null ? _a : this.defaultMixinCfg.priority;
  1702. if (!this.autoIncPrioCounter.has(mixinKey))
  1703. this.autoIncPrioCounter.set(mixinKey, this.defaultMixinCfg.priority);
  1704. let prio = this.autoIncPrioCounter.get(mixinKey);
  1705. while (this.mixins.some((m) => m.key === mixinKey && m.priority === prio))
  1706. prio++;
  1707. this.autoIncPrioCounter.set(mixinKey, prio + 1);
  1708. return prio;
  1709. }
  1710. /** Removes all mixins with the given key */
  1711. removeAll(mixinKey) {
  1712. this.mixins.filter((m) => m.key === mixinKey);
  1713. this.mixins = this.mixins.filter((m) => m.key !== mixinKey);
  1714. }
  1715. };
  1716.  
  1717. // lib/SelectorObserver.ts
  1718. var SelectorObserver = class {
  1719. constructor(baseElement, options = {}) {
  1720. __publicField(this, "enabled", false);
  1721. __publicField(this, "baseElement");
  1722. __publicField(this, "observer");
  1723. __publicField(this, "observerOptions");
  1724. __publicField(this, "customOptions");
  1725. __publicField(this, "listenerMap");
  1726. this.baseElement = baseElement;
  1727. this.listenerMap = /* @__PURE__ */ new Map();
  1728. const _a = options, {
  1729. defaultDebounce,
  1730. defaultDebounceType,
  1731. disableOnNoListeners,
  1732. enableOnAddListener
  1733. } = _a, observerOptions = __objRest(_a, [
  1734. "defaultDebounce",
  1735. "defaultDebounceType",
  1736. "disableOnNoListeners",
  1737. "enableOnAddListener"
  1738. ]);
  1739. this.observerOptions = __spreadValues({
  1740. childList: true,
  1741. subtree: true
  1742. }, observerOptions);
  1743. this.customOptions = {
  1744. defaultDebounce: defaultDebounce != null ? defaultDebounce : 0,
  1745. defaultDebounceType: defaultDebounceType != null ? defaultDebounceType : "immediate",
  1746. disableOnNoListeners: disableOnNoListeners != null ? disableOnNoListeners : false,
  1747. enableOnAddListener: enableOnAddListener != null ? enableOnAddListener : true
  1748. };
  1749. if (typeof this.customOptions.checkInterval !== "number") {
  1750. this.observer = new MutationObserver(() => this.checkAllSelectors());
  1751. } else {
  1752. this.checkAllSelectors();
  1753. setInterval(() => this.checkAllSelectors(), this.customOptions.checkInterval);
  1754. }
  1755. }
  1756. /** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
  1757. checkAllSelectors() {
  1758. if (!this.enabled || !isDomLoaded())
  1759. return;
  1760. for (const [selector, listeners] of this.listenerMap.entries())
  1761. this.checkSelector(selector, listeners);
  1762. }
  1763. /** Checks if the element(s) with the given {@linkcode selector} exist in the DOM and calls the respective {@linkcode listeners} accordingly */
  1764. checkSelector(selector, listeners) {
  1765. var _a;
  1766. if (!this.enabled)
  1767. return;
  1768. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  1769. if (!baseElement)
  1770. return;
  1771. const all = listeners.some((listener) => listener.all);
  1772. const one = listeners.some((listener) => !listener.all);
  1773. const allElements = all ? baseElement.querySelectorAll(selector) : null;
  1774. const oneElement = one ? baseElement.querySelector(selector) : null;
  1775. for (const options of listeners) {
  1776. if (options.all) {
  1777. if (allElements && allElements.length > 0) {
  1778. options.listener(allElements);
  1779. if (!options.continuous)
  1780. this.removeListener(selector, options);
  1781. }
  1782. } else {
  1783. if (oneElement) {
  1784. options.listener(oneElement);
  1785. if (!options.continuous)
  1786. this.removeListener(selector, options);
  1787. }
  1788. }
  1789. if (((_a = this.listenerMap.get(selector)) == null ? undefined : _a.length) === 0)
  1790. this.listenerMap.delete(selector);
  1791. if (this.listenerMap.size === 0 && this.customOptions.disableOnNoListeners)
  1792. this.disable();
  1793. }
  1794. }
  1795. /**
  1796. * Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
  1797. * @param selector The selector to observe
  1798. * @param options Options for the selector observation
  1799. * @param options.listener Gets called whenever the selector was found in the DOM
  1800. * @param [options.all] Whether to use `querySelectorAll()` instead - default is false
  1801. * @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
  1802. * @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
  1803. * @returns Returns a function that can be called to remove this listener more easily
  1804. */
  1805. addListener(selector, options) {
  1806. options = __spreadValues({
  1807. all: false,
  1808. continuous: false,
  1809. debounce: 0
  1810. }, options);
  1811. if (options.debounce && options.debounce > 0 || this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0) {
  1812. options.listener = debounce(
  1813. options.listener,
  1814. options.debounce || this.customOptions.defaultDebounce,
  1815. options.debounceType || this.customOptions.defaultDebounceType
  1816. );
  1817. }
  1818. if (this.listenerMap.has(selector))
  1819. this.listenerMap.get(selector).push(options);
  1820. else
  1821. this.listenerMap.set(selector, [options]);
  1822. if (this.enabled === false && this.customOptions.enableOnAddListener)
  1823. this.enable();
  1824. this.checkSelector(selector, [options]);
  1825. return () => this.removeListener(selector, options);
  1826. }
  1827. /** Disables the observation of the child elements */
  1828. disable() {
  1829. var _a;
  1830. if (!this.enabled)
  1831. return;
  1832. this.enabled = false;
  1833. (_a = this.observer) == null ? undefined : _a.disconnect();
  1834. }
  1835. /**
  1836. * Enables or reenables the observation of the child elements.
  1837. * @param immediatelyCheckSelectors Whether to immediately check if all previously registered selectors exist (default is true)
  1838. * @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found)
  1839. */
  1840. enable(immediatelyCheckSelectors = true) {
  1841. var _a;
  1842. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  1843. if (this.enabled || !baseElement)
  1844. return false;
  1845. this.enabled = true;
  1846. (_a = this.observer) == null ? undefined : _a.observe(baseElement, this.observerOptions);
  1847. if (immediatelyCheckSelectors)
  1848. this.checkAllSelectors();
  1849. return true;
  1850. }
  1851. /** Returns whether the observation of the child elements is currently enabled */
  1852. isEnabled() {
  1853. return this.enabled;
  1854. }
  1855. /** Removes all listeners that have been registered with {@linkcode addListener()} */
  1856. clearListeners() {
  1857. this.listenerMap.clear();
  1858. }
  1859. /**
  1860. * Removes all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()}
  1861. * @returns Returns true when all listeners for the associated selector were found and removed, false otherwise
  1862. */
  1863. removeAllListeners(selector) {
  1864. return this.listenerMap.delete(selector);
  1865. }
  1866. /**
  1867. * Removes a single listener for the given {@linkcode selector} and {@linkcode options} that has been registered with {@linkcode addListener()}
  1868. * @returns Returns true when the listener was found and removed, false otherwise
  1869. */
  1870. removeListener(selector, options) {
  1871. const listeners = this.listenerMap.get(selector);
  1872. if (!listeners)
  1873. return false;
  1874. const index = listeners.indexOf(options);
  1875. if (index > -1) {
  1876. listeners.splice(index, 1);
  1877. return true;
  1878. }
  1879. return false;
  1880. }
  1881. /** Returns all listeners that have been registered with {@linkcode addListener()} */
  1882. getAllListeners() {
  1883. return this.listenerMap;
  1884. }
  1885. /** Returns all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} */
  1886. getListeners(selector) {
  1887. return this.listenerMap.get(selector);
  1888. }
  1889. };
  1890.  
  1891. // lib/translation.ts
  1892. var trans = {};
  1893. var valTransforms = [];
  1894. var fallbackLang;
  1895. function translate(language, key, ...trArgs) {
  1896. if (typeof language !== "string")
  1897. language = fallbackLang != null ? fallbackLang : "";
  1898. const trObj = trans[language];
  1899. if (typeof language !== "string" || language.length === 0 || typeof trObj !== "object" || trObj === null)
  1900. return fallbackLang ? translate(fallbackLang, key, ...trArgs) : key;
  1901. const transformTrVal = (trKey, trValue) => {
  1902. const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(String(trValue)));
  1903. if (tfs.length === 0)
  1904. return String(trValue);
  1905. let retStr = String(trValue);
  1906. for (const tf of tfs) {
  1907. const re = new RegExp(tf.regex);
  1908. const matches = [];
  1909. let execRes;
  1910. while ((execRes = re.exec(trValue)) !== null) {
  1911. if (matches.some((m) => m[0] === (execRes == null ? undefined : execRes[0])))
  1912. break;
  1913. matches.push(execRes);
  1914. }
  1915. retStr = String(tf.fn({
  1916. language,
  1917. trValue,
  1918. currentValue: retStr,
  1919. matches,
  1920. trKey,
  1921. trArgs
  1922. }));
  1923. }
  1924. return retStr;
  1925. };
  1926. const keyParts = key.split(".");
  1927. let value = trObj;
  1928. for (const part of keyParts) {
  1929. if (typeof value !== "object" || value === null) {
  1930. value = undefined;
  1931. break;
  1932. }
  1933. value = value == null ? undefined : value[part];
  1934. }
  1935. if (typeof value === "string")
  1936. return transformTrVal(key, value);
  1937. value = trObj == null ? undefined : trObj[key];
  1938. if (typeof value === "string")
  1939. return transformTrVal(key, value);
  1940. return fallbackLang ? translate(fallbackLang, key, ...trArgs) : key;
  1941. }
  1942. function trFor(language, key, ...args) {
  1943. const txt = translate(language, key, ...args);
  1944. if (txt === key)
  1945. return fallbackLang ? translate(fallbackLang, key, ...args) : key;
  1946. return txt;
  1947. }
  1948. function useTr(language) {
  1949. return (key, ...args) => translate(language, key, ...args);
  1950. }
  1951. function hasKey(language = fallbackLang != null ? fallbackLang : "", key) {
  1952. return tr.for(language, key) !== key;
  1953. }
  1954. function addTranslations(language, translations) {
  1955. trans[language] = JSON.parse(JSON.stringify(translations));
  1956. }
  1957. function getTranslations(language = fallbackLang != null ? fallbackLang : "") {
  1958. return trans[language];
  1959. }
  1960. var deleteTranslations = (language) => {
  1961. if (language in trans) {
  1962. delete trans[language];
  1963. return true;
  1964. }
  1965. return false;
  1966. };
  1967. function setFallbackLanguage(fallbackLanguage) {
  1968. fallbackLang = fallbackLanguage;
  1969. }
  1970. function getFallbackLanguage() {
  1971. return fallbackLang;
  1972. }
  1973. function addTransform(transform) {
  1974. const [regex, fn] = transform;
  1975. valTransforms.push({
  1976. fn,
  1977. regex
  1978. });
  1979. }
  1980. function deleteTransform(patternOrFn) {
  1981. const idx = valTransforms.findIndex(
  1982. (t) => typeof patternOrFn === "function" ? t.fn === patternOrFn : t.regex === patternOrFn
  1983. );
  1984. if (idx !== -1) {
  1985. valTransforms.splice(idx, 1);
  1986. return true;
  1987. }
  1988. return false;
  1989. }
  1990. var templateLiteralTransform = [
  1991. /\$\{([a-zA-Z0-9$_-]+)\}/gm,
  1992. ({ matches, trArgs, trValue }) => {
  1993. const patternStart = "${", patternEnd = "}", patternRegex = /\$\{.+\}/m;
  1994. let str = String(trValue);
  1995. const eachKeyInTrString = (keys) => keys.every((key) => trValue.includes(`${patternStart}${key}${patternEnd}`));
  1996. const namedMapping = () => {
  1997. var _a;
  1998. if (!str.includes(patternStart) || typeof trArgs[0] === "undefined" || typeof trArgs[0] !== "object" || !eachKeyInTrString(Object.keys((_a = trArgs[0]) != null ? _a : {})))
  1999. return;
  2000. for (const match of matches) {
  2001. const repl = match[1] !== undefined ? trArgs[0][match[1]] : undefined;
  2002. if (typeof repl !== "undefined")
  2003. str = str.replace(match[0], String(repl));
  2004. }
  2005. };
  2006. const positionalMapping = () => {
  2007. if (!patternRegex.test(str) || !trArgs[0])
  2008. return;
  2009. let matchNum = -1;
  2010. for (const match of matches) {
  2011. matchNum++;
  2012. if (typeof trArgs[matchNum] !== "undefined")
  2013. str = str.replace(match[0], String(trArgs[matchNum]));
  2014. }
  2015. };
  2016. const isArgsObject = trArgs[0] && typeof trArgs[0] === "object" && trArgs[0] !== null && String(trArgs[0]).startsWith("[object");
  2017. if (isArgsObject && eachKeyInTrString(Object.keys(trArgs[0])))
  2018. namedMapping();
  2019. else
  2020. positionalMapping();
  2021. return str;
  2022. }
  2023. ];
  2024. var percentTransform = [
  2025. /%(\d+)/gm,
  2026. ({ matches, trArgs, trValue }) => {
  2027. let str = String(trValue);
  2028. for (const match of matches) {
  2029. const repl = match[1] !== undefined ? trArgs == null ? undefined : trArgs[Number(match[1]) - 1] : undefined;
  2030. if (typeof repl !== "undefined")
  2031. str = str.replace(match[0], String(repl));
  2032. }
  2033. return str;
  2034. }
  2035. ];
  2036. var tr = {
  2037. for: (...params) => trFor(...params),
  2038. use: (...params) => useTr(...params),
  2039. hasKey: (language = fallbackLang != null ? fallbackLang : "", key) => hasKey(language, key),
  2040. addTranslations,
  2041. getTranslations,
  2042. deleteTranslations,
  2043. setFallbackLanguage,
  2044. getFallbackLanguage,
  2045. addTransform,
  2046. deleteTransform,
  2047. transforms: {
  2048. templateLiteral: templateLiteralTransform,
  2049. percent: percentTransform
  2050. }
  2051. };
  2052.  
  2053. exports.ChecksumMismatchError = ChecksumMismatchError;
  2054. exports.DataStore = DataStore;
  2055. exports.DataStoreSerializer = DataStoreSerializer;
  2056. exports.Debouncer = Debouncer;
  2057. exports.Dialog = Dialog;
  2058. exports.MigrationError = MigrationError;
  2059. exports.Mixins = Mixins;
  2060. exports.NanoEmitter = NanoEmitter;
  2061. exports.PlatformError = PlatformError;
  2062. exports.SelectorObserver = SelectorObserver;
  2063. exports.UUError = UUError;
  2064. exports.addGlobalStyle = addGlobalStyle;
  2065. exports.addParent = addParent;
  2066. exports.autoPlural = autoPlural;
  2067. exports.bitSetHas = bitSetHas;
  2068. exports.clamp = clamp;
  2069. exports.compress = compress;
  2070. exports.computeHash = computeHash;
  2071. exports.consumeGen = consumeGen;
  2072. exports.consumeStringGen = consumeStringGen;
  2073. exports.darkenColor = darkenColor;
  2074. exports.debounce = debounce;
  2075. exports.decompress = decompress;
  2076. exports.defaultDialogCss = defaultDialogCss;
  2077. exports.defaultStrings = defaultStrings;
  2078. exports.digitCount = digitCount;
  2079. exports.fetchAdvanced = fetchAdvanced;
  2080. exports.getListLength = getListLength;
  2081. exports.getSiblingsFrame = getSiblingsFrame;
  2082. exports.getUnsafeWindow = getUnsafeWindow;
  2083. exports.hexToRgb = hexToRgb;
  2084. exports.insertValues = insertValues;
  2085. exports.interceptEvent = interceptEvent;
  2086. exports.interceptWindowEvent = interceptWindowEvent;
  2087. exports.isDomLoaded = isDomLoaded;
  2088. exports.isScrollable = isScrollable;
  2089. exports.lightenColor = lightenColor;
  2090. exports.mapRange = mapRange;
  2091. exports.observeElementProp = observeElementProp;
  2092. exports.onDomLoad = onDomLoad;
  2093. exports.openDialogs = openDialogs;
  2094. exports.openInNewTab = openInNewTab;
  2095. exports.pauseFor = pauseFor;
  2096. exports.preloadImages = preloadImages;
  2097. exports.probeElementStyle = probeElementStyle;
  2098. exports.purifyObj = purifyObj;
  2099. exports.randRange = randRange;
  2100. exports.randomId = randomId;
  2101. exports.randomItem = randomItem;
  2102. exports.randomItemIndex = randomItemIndex;
  2103. exports.randomizeArray = randomizeArray;
  2104. exports.rgbToHex = rgbToHex;
  2105. exports.roundFixed = roundFixed;
  2106. exports.setInnerHtmlUnsafe = setInnerHtmlUnsafe;
  2107. exports.takeRandomItem = takeRandomItem;
  2108. exports.tr = tr;
  2109.  
  2110. return exports;
  2111.  
  2112. })({});