@sv443-network/userutils 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sv443-network/userutils
2
2
 
3
+ ## 1.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 049aeb0: Export ConfigMigrationsDict for easier use with TypeScript
8
+
9
+ ## 1.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 4799a9f: Fix TS error in ConfigManager migration functions
14
+
3
15
  ## 1.1.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -637,8 +637,9 @@ new ConfigManager(options: ConfigManagerOptions)
637
637
 
638
638
  A class that manages a userscript's configuration that is persistently saved to and loaded from GM storage.
639
639
  Also supports automatic migration of outdated data formats via provided migration functions.
640
+ You may create as many instances as you like as long as they have different IDs.
640
641
 
641
- ⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used.
642
+ ⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used. Circular structures and complex objects will throw an error on load and save.
642
643
  ⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required for this to work.
643
644
 
644
645
  The options object has the following properties:
@@ -670,8 +671,8 @@ Writes the default configuration given in `options.defaultConfig` synchronously
670
671
  `deleteConfig(): Promise<void>`
671
672
  Fully deletes the configuration from persistent storage.
672
673
  The internal cache will be left untouched, so any subsequent calls to `getData()` will return the data that was last loaded.
673
- If `loadData()` or `setData()` are called after this, the persistent storage will be populated again.
674
- If you want to use this method, the additional directive `@grant GM.deleteValue` is required.
674
+ If `loadData()` or `setData()` are called after this, the persistent storage will be populated with the value of `options.defaultConfig` again.
675
+ ⚠️ If you want to use this method, the additional directive `@grant GM.deleteValue` is required.
675
676
 
676
677
  <br>
677
678
 
@@ -699,7 +700,7 @@ const formatVersion = 2;
699
700
  /** Functions that migrate outdated data to the latest format - make sure a function exists for every previously used formatVersion and that no numbers are skipped! */
700
701
  const migrations = {
701
702
  // migrate from format version 0 to 1
702
- 1: (oldData: any) => {
703
+ 1: (oldData: Record<string, unknown>) => {
703
704
  return {
704
705
  foo: oldData.foo,
705
706
  bar: oldData.bar,
@@ -707,7 +708,7 @@ const migrations = {
707
708
  };
708
709
  },
709
710
  // asynchronously migrate from format version 1 to 2
710
- 2: async (oldData: any) => {
711
+ 2: async (oldData: Record<string, unknown>) => {
711
712
  // arbitrary async operation required for the new format
712
713
  const qux = JSON.parse(await (await fetch("https://api.example.org/some-data")).text());
713
714
  return {
@@ -719,7 +720,7 @@ const migrations = {
719
720
  },
720
721
  };
721
722
 
722
- const configMgr = new ConfigManager({
723
+ const manager = new ConfigManager({
723
724
  /** A unique ID for this configuration - choose wisely as changing it is not supported yet! */
724
725
  id: "my-userscript",
725
726
  /** Default / fallback configuration data */
@@ -735,7 +736,7 @@ async function init() {
735
736
  // wait for the config to be loaded from persistent storage
736
737
  // if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultConfig will be returned
737
738
  // if the previously saved data needs to be migrated to a newer version, it will happen in this function call
738
- const configData = await configMgr.loadData();
739
+ const configData = await manager.loadData();
739
740
 
740
741
  console.log(configData.foo); // "hello"
741
742
 
@@ -744,12 +745,12 @@ async function init() {
744
745
  configData.bar = 123;
745
746
 
746
747
  // save the updated config - synchronously to the cache and asynchronously to persistent storage
747
- configMgr.saveData(configData).then(() => {
748
+ manager.saveData(configData).then(() => {
748
749
  console.log("Config saved to persistent storage!");
749
750
  });
750
751
 
751
752
  // the internal cache is updated synchronously, so the updated data can be accessed before the Promise resolves:
752
- console.log(configMgr.getData().foo); // "world"
753
+ console.log(manager.getData().foo); // "world"
753
754
  }
754
755
 
755
756
  init();
@@ -0,0 +1,443 @@
1
+ // ==UserScript==
2
+ // @name UserUtils
3
+ // @description Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more
4
+ // @namespace https://github.com/Sv443-Network/UserUtils
5
+ // @version 1.1.2
6
+ // @license MIT
7
+ // @author Sv443
8
+ // @copyright Sv443 (https://github.com/Sv443)
9
+ // @supportURL https://github.com/Sv443-Network/UserUtils/issues
10
+ // @homepageURL https://github.com/Sv443-Network/UserUtils#readme
11
+ // ==/UserScript==
12
+
13
+ var UserUtils = (function (exports) {
14
+ var __defProp = Object.defineProperty;
15
+ var __defProps = Object.defineProperties;
16
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
17
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
18
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
19
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
20
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
21
+ var __spreadValues = (a, b) => {
22
+ for (var prop in b || (b = {}))
23
+ if (__hasOwnProp.call(b, prop))
24
+ __defNormalProp(a, prop, b[prop]);
25
+ if (__getOwnPropSymbols)
26
+ for (var prop of __getOwnPropSymbols(b)) {
27
+ if (__propIsEnum.call(b, prop))
28
+ __defNormalProp(a, prop, b[prop]);
29
+ }
30
+ return a;
31
+ };
32
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
33
+ var __publicField = (obj, key, value) => {
34
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
35
+ return value;
36
+ };
37
+ var __async = (__this, __arguments, generator) => {
38
+ return new Promise((resolve, reject) => {
39
+ var fulfilled = (value) => {
40
+ try {
41
+ step(generator.next(value));
42
+ } catch (e) {
43
+ reject(e);
44
+ }
45
+ };
46
+ var rejected = (value) => {
47
+ try {
48
+ step(generator.throw(value));
49
+ } catch (e) {
50
+ reject(e);
51
+ }
52
+ };
53
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
54
+ step((generator = generator.apply(__this, __arguments)).next());
55
+ });
56
+ };
57
+
58
+ // lib/math.ts
59
+ function clamp(value, min, max) {
60
+ return Math.max(Math.min(value, max), min);
61
+ }
62
+ function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
63
+ if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
64
+ return value * (range_2_max / range_1_max);
65
+ return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
66
+ }
67
+ function randRange(...args) {
68
+ let min, max;
69
+ if (typeof args[0] === "number" && typeof args[1] === "number") {
70
+ [min, max] = args;
71
+ } else if (typeof args[0] === "number" && typeof args[1] !== "number") {
72
+ min = 0;
73
+ max = args[0];
74
+ } else
75
+ throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
76
+ min = Number(min);
77
+ max = Number(max);
78
+ if (isNaN(min) || isNaN(max))
79
+ throw new TypeError(`Parameters "min" and "max" can't be NaN`);
80
+ if (min > max)
81
+ throw new TypeError(`Parameter "min" can't be bigger than "max"`);
82
+ return Math.floor(Math.random() * (max - min + 1)) + min;
83
+ }
84
+
85
+ // lib/array.ts
86
+ function randomItem(array) {
87
+ return randomItemIndex(array)[0];
88
+ }
89
+ function randomItemIndex(array) {
90
+ if (array.length === 0)
91
+ return [void 0, void 0];
92
+ const idx = randRange(array.length - 1);
93
+ return [array[idx], idx];
94
+ }
95
+ function takeRandomItem(arr) {
96
+ const [itm, idx] = randomItemIndex(arr);
97
+ if (idx === void 0)
98
+ return void 0;
99
+ arr.splice(idx, 1);
100
+ return itm;
101
+ }
102
+ function randomizeArray(array) {
103
+ const retArray = [...array];
104
+ if (array.length === 0)
105
+ return array;
106
+ for (let i = retArray.length - 1; i > 0; i--) {
107
+ const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
108
+ [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
109
+ }
110
+ return retArray;
111
+ }
112
+
113
+ // lib/config.ts
114
+ var ConfigManager = class {
115
+ /**
116
+ * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
117
+ * Supports migrating data from older versions of the configuration to newer ones and populating the cache with default data if no persistent data is found.
118
+ *
119
+ * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
120
+ * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
121
+ *
122
+ * @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `config.defaultConfig`) - this should also be the type of the data format associated with the current `options.formatVersion`
123
+ * @param options The options for this ConfigManager instance
124
+ */
125
+ constructor(options) {
126
+ __publicField(this, "id");
127
+ __publicField(this, "formatVersion");
128
+ __publicField(this, "defaultConfig");
129
+ __publicField(this, "cachedConfig");
130
+ __publicField(this, "migrations");
131
+ this.id = options.id;
132
+ this.formatVersion = options.formatVersion;
133
+ this.defaultConfig = options.defaultConfig;
134
+ this.cachedConfig = options.defaultConfig;
135
+ this.migrations = options.migrations;
136
+ }
137
+ /**
138
+ * Loads the data saved in persistent storage into the in-memory cache and also returns it.
139
+ * Automatically populates persistent storage with default data if it doesn't contain any data yet.
140
+ * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
141
+ */
142
+ loadData() {
143
+ return __async(this, null, function* () {
144
+ try {
145
+ const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
146
+ let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
147
+ if (typeof gmData !== "string") {
148
+ yield this.saveDefaultData();
149
+ return this.defaultConfig;
150
+ }
151
+ if (isNaN(gmFmtVer))
152
+ yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
153
+ let parsed = JSON.parse(gmData);
154
+ if (gmFmtVer < this.formatVersion && this.migrations)
155
+ parsed = yield this.runMigrations(parsed, gmFmtVer);
156
+ return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
157
+ } catch (err) {
158
+ yield this.saveDefaultData();
159
+ return this.defaultConfig;
160
+ }
161
+ });
162
+ }
163
+ /** Returns a copy of the data from the in-memory cache. Use `loadData()` to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage). */
164
+ getData() {
165
+ return this.deepCopy(this.cachedConfig);
166
+ }
167
+ /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
168
+ setData(data) {
169
+ this.cachedConfig = data;
170
+ return new Promise((resolve) => __async(this, null, function* () {
171
+ yield Promise.all([
172
+ GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
173
+ GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
174
+ ]);
175
+ resolve();
176
+ }));
177
+ }
178
+ /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
179
+ saveDefaultData() {
180
+ return __async(this, null, function* () {
181
+ this.cachedConfig = this.defaultConfig;
182
+ return new Promise((resolve) => __async(this, null, function* () {
183
+ yield Promise.all([
184
+ GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
185
+ GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
186
+ ]);
187
+ resolve();
188
+ }));
189
+ });
190
+ }
191
+ /**
192
+ * Call this method to clear all persistently stored data associated with this ConfigManager instance.
193
+ * The in-memory cache will be left untouched, so you may still access the data with `getData()`.
194
+ * Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.
195
+ *
196
+ * ⚠️ This requires the additional directive `@grant GM.deleteValue`
197
+ */
198
+ deleteConfig() {
199
+ return __async(this, null, function* () {
200
+ yield Promise.all([
201
+ GM.deleteValue(`_uucfg-${this.id}`),
202
+ GM.deleteValue(`_uucfgver-${this.id}`)
203
+ ]);
204
+ });
205
+ }
206
+ /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
207
+ runMigrations(oldData, oldFmtVer) {
208
+ return __async(this, null, function* () {
209
+ if (!this.migrations)
210
+ return oldData;
211
+ let newData = oldData;
212
+ const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
213
+ let lastFmtVer = oldFmtVer;
214
+ for (const [fmtVer, migrationFunc] of sortedMigrations) {
215
+ const ver = Number(fmtVer);
216
+ if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
217
+ try {
218
+ const migRes = migrationFunc(newData);
219
+ newData = migRes instanceof Promise ? yield migRes : migRes;
220
+ lastFmtVer = oldFmtVer = ver;
221
+ } catch (err) {
222
+ console.error(`Error while running migration function for format version ${fmtVer}:`, err);
223
+ }
224
+ }
225
+ }
226
+ yield Promise.all([
227
+ GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
228
+ GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
229
+ ]);
230
+ return newData;
231
+ });
232
+ }
233
+ /** Copies a JSON-compatible object and loses its internal references */
234
+ deepCopy(obj) {
235
+ return JSON.parse(JSON.stringify(obj));
236
+ }
237
+ };
238
+
239
+ // lib/dom.ts
240
+ function getUnsafeWindow() {
241
+ try {
242
+ return unsafeWindow;
243
+ } catch (e) {
244
+ return window;
245
+ }
246
+ }
247
+ function insertAfter(beforeElement, afterElement) {
248
+ var _a;
249
+ (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
250
+ return afterElement;
251
+ }
252
+ function addParent(element, newParent) {
253
+ const oldParent = element.parentNode;
254
+ if (!oldParent)
255
+ throw new Error("Element doesn't have a parent node");
256
+ oldParent.replaceChild(newParent, element);
257
+ newParent.appendChild(element);
258
+ return newParent;
259
+ }
260
+ function addGlobalStyle(style) {
261
+ const styleElem = document.createElement("style");
262
+ styleElem.innerHTML = style;
263
+ document.head.appendChild(styleElem);
264
+ }
265
+ function preloadImages(srcUrls, rejects = false) {
266
+ const promises = srcUrls.map((src) => new Promise((res, rej) => {
267
+ const image = new Image();
268
+ image.src = src;
269
+ image.addEventListener("load", () => res(image));
270
+ image.addEventListener("error", (evt) => rejects && rej(evt));
271
+ }));
272
+ return Promise.allSettled(promises);
273
+ }
274
+ function openInNewTab(href) {
275
+ const openElem = document.createElement("a");
276
+ Object.assign(openElem, {
277
+ className: "userutils-open-in-new-tab",
278
+ target: "_blank",
279
+ rel: "noopener noreferrer",
280
+ href
281
+ });
282
+ openElem.style.display = "none";
283
+ document.body.appendChild(openElem);
284
+ openElem.click();
285
+ setTimeout(openElem.remove, 50);
286
+ }
287
+ function interceptEvent(eventObject, eventName, predicate) {
288
+ if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
289
+ Error.stackTraceLimit = 1e3;
290
+ }
291
+ (function(original) {
292
+ eventObject.__proto__.addEventListener = function(...args) {
293
+ var _a, _b;
294
+ const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
295
+ args[1] = function(...a) {
296
+ if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
297
+ return;
298
+ else
299
+ return origListener.apply(this, a);
300
+ };
301
+ original.apply(this, args);
302
+ };
303
+ })(eventObject.__proto__.addEventListener);
304
+ }
305
+ function interceptWindowEvent(eventName, predicate) {
306
+ return interceptEvent(getUnsafeWindow(), eventName, predicate);
307
+ }
308
+ function amplifyMedia(mediaElement, multiplier = 1) {
309
+ const context = new (window.AudioContext || window.webkitAudioContext)();
310
+ const result = {
311
+ mediaElement,
312
+ amplify: (multiplier2) => {
313
+ result.gain.gain.value = multiplier2;
314
+ },
315
+ getAmpLevel: () => result.gain.gain.value,
316
+ context,
317
+ source: context.createMediaElementSource(mediaElement),
318
+ gain: context.createGain()
319
+ };
320
+ result.source.connect(result.gain);
321
+ result.gain.connect(context.destination);
322
+ result.amplify(multiplier);
323
+ return result;
324
+ }
325
+ function isScrollable(element) {
326
+ const { overflowX, overflowY } = getComputedStyle(element);
327
+ return {
328
+ vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
329
+ horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
330
+ };
331
+ }
332
+
333
+ // lib/misc.ts
334
+ function autoPlural(word, num) {
335
+ if (Array.isArray(num) || num instanceof NodeList)
336
+ num = num.length;
337
+ return `${word}${num === 1 ? "" : "s"}`;
338
+ }
339
+ function pauseFor(time) {
340
+ return new Promise((res) => {
341
+ setTimeout(() => res(), time);
342
+ });
343
+ }
344
+ function debounce(func, timeout = 300) {
345
+ let timer;
346
+ return function(...args) {
347
+ clearTimeout(timer);
348
+ timer = setTimeout(() => func.apply(this, args), timeout);
349
+ };
350
+ }
351
+ function fetchAdvanced(_0) {
352
+ return __async(this, arguments, function* (url, options = {}) {
353
+ const { timeout = 1e4 } = options;
354
+ const controller = new AbortController();
355
+ const id = setTimeout(() => controller.abort(), timeout);
356
+ const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
357
+ signal: controller.signal
358
+ }));
359
+ clearTimeout(id);
360
+ return res;
361
+ });
362
+ }
363
+
364
+ // lib/onSelector.ts
365
+ var selectorMap = /* @__PURE__ */ new Map();
366
+ function onSelector(selector, options) {
367
+ let selectorMapItems = [];
368
+ if (selectorMap.has(selector))
369
+ selectorMapItems = selectorMap.get(selector);
370
+ selectorMapItems.push(options);
371
+ selectorMap.set(selector, selectorMapItems);
372
+ checkSelectorExists(selector, selectorMapItems);
373
+ }
374
+ function removeOnSelector(selector) {
375
+ return selectorMap.delete(selector);
376
+ }
377
+ function checkSelectorExists(selector, options) {
378
+ const deleteIndices = [];
379
+ options.forEach((option, i) => {
380
+ try {
381
+ const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
382
+ if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
383
+ option.listener(elements);
384
+ if (!option.continuous)
385
+ deleteIndices.push(i);
386
+ }
387
+ } catch (err) {
388
+ console.error(`Couldn't call listener for selector '${selector}'`, err);
389
+ }
390
+ });
391
+ if (deleteIndices.length > 0) {
392
+ const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
393
+ if (newOptsArray.length === 0)
394
+ selectorMap.delete(selector);
395
+ else {
396
+ selectorMap.set(selector, newOptsArray);
397
+ }
398
+ }
399
+ }
400
+ function initOnSelector(options = {}) {
401
+ const observer = new MutationObserver(() => {
402
+ for (const [selector, options2] of selectorMap.entries())
403
+ checkSelectorExists(selector, options2);
404
+ });
405
+ observer.observe(document.body, __spreadValues({
406
+ subtree: true,
407
+ childList: true
408
+ }, options));
409
+ }
410
+ function getSelectorMap() {
411
+ return selectorMap;
412
+ }
413
+
414
+ exports.ConfigManager = ConfigManager;
415
+ exports.addGlobalStyle = addGlobalStyle;
416
+ exports.addParent = addParent;
417
+ exports.amplifyMedia = amplifyMedia;
418
+ exports.autoPlural = autoPlural;
419
+ exports.clamp = clamp;
420
+ exports.debounce = debounce;
421
+ exports.fetchAdvanced = fetchAdvanced;
422
+ exports.getSelectorMap = getSelectorMap;
423
+ exports.getUnsafeWindow = getUnsafeWindow;
424
+ exports.initOnSelector = initOnSelector;
425
+ exports.insertAfter = insertAfter;
426
+ exports.interceptEvent = interceptEvent;
427
+ exports.interceptWindowEvent = interceptWindowEvent;
428
+ exports.isScrollable = isScrollable;
429
+ exports.mapRange = mapRange;
430
+ exports.onSelector = onSelector;
431
+ exports.openInNewTab = openInNewTab;
432
+ exports.pauseFor = pauseFor;
433
+ exports.preloadImages = preloadImages;
434
+ exports.randRange = randRange;
435
+ exports.randomItem = randomItem;
436
+ exports.randomItemIndex = randomItemIndex;
437
+ exports.randomizeArray = randomizeArray;
438
+ exports.removeOnSelector = removeOnSelector;
439
+ exports.takeRandomItem = takeRandomItem;
440
+
441
+ return exports;
442
+
443
+ })({});