@sv443-network/userutils 0.5.3 → 1.1.0

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,26 @@
1
1
  # @sv443-network/userutils
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - db5cded: Added `isScrollable()` to check whether an element has a horizontal or vertical scroll bar
8
+
9
+ ### Patch Changes
10
+
11
+ - 9e26464: Replaced most occurrences of `HTMLElement` in the docs with `Element` for better compatibility
12
+
13
+ ## 1.0.0
14
+
15
+ ### Major Changes
16
+
17
+ - a500a98: Added ConfigManager to manage persistent user configurations including data versioning and migration
18
+
19
+ ### Patch Changes
20
+
21
+ - 6d0a700: Event interceptor can now be toggled at runtime ([#16](https://github.com/Sv443-Network/UserUtils/issues/16))
22
+ - d038b21: Global (IIFE) build now comes with a header
23
+
3
24
  ## 0.5.3
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  <div style="text-align: center;" align="center">
2
2
 
3
3
  ## UserUtils
4
- Zero-dependency library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, modify the DOM more easily and more.
4
+ Zero-dependency 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.
5
+
5
6
  Contains builtin TypeScript declarations. Webpack compatible and supports ESM and CJS.
6
7
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
7
8
 
@@ -26,11 +27,13 @@ If you like using this library, please consider [supporting the development ❤
26
27
  - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
27
28
  - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
28
29
  - [amplifyMedia()](#amplifymedia) - amplify an audio or video element's volume past the maximum of 100%
30
+ - [isScrollable()](#isscrollable) - check if an element has a horizontal or vertical scroll bar
29
31
  - [Math:](#math)
30
32
  - [clamp()](#clamp) - constrain a number between a min and max value
31
33
  - [mapRange()](#maprange) - map a number from one range to the same spot in another range
32
34
  - [randRange()](#randrange) - generate a random number between a min and max boundary
33
35
  - [Misc:](#misc)
36
+ - [ConfigManager()](#configmanager) - class that manages persistent userscript configurations, including data migration
34
37
  - [autoPlural()](#autoplural) - automatically pluralize a string
35
38
  - [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time
36
39
  - [debounce()](#debounce) - call a function only once, after a given amount of time
@@ -51,10 +54,12 @@ If you like using this library, please consider [supporting the development ❤
51
54
  Then, import it in your script as usual:
52
55
  ```ts
53
56
  import { addGlobalStyle } from "@sv443-network/userutils";
54
- // or
55
- import * as userUtils from "@sv443-network/userutils";
57
+
58
+ // or just import everything (not recommended because this doesn't allow for treeshaking):
59
+
60
+ import * as UserUtils from "@sv443-network/userutils";
56
61
  ```
57
- Shameless plug: I also have a [webpack-based template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly.
62
+ Shameless plug: I made a [template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly. It also includes this library by default.
58
63
 
59
64
  <br>
60
65
 
@@ -77,12 +82,13 @@ If you like using this library, please consider [supporting the development ❤
77
82
 
78
83
  ## Preamble:
79
84
  This library is written in TypeScript and contains builtin TypeScript declarations.
80
- The usages and examples in this readme are written in TypeScript, but the library can also be used in plain JavaScript.
81
85
 
82
- Some functions require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
83
- Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
86
+ Each feature has example code that can be expanded by clicking on the text "Example - click to view".
87
+ The usages and examples are written in TypeScript, but the library can also be used in plain JavaScript after removing the type annotations (and changing the imports if you are using CommonJS).
88
+ If the usage section contains multiple definitions of the function, each occurrence represents an overload and you can choose which one you want to use.
84
89
 
85
- If the usage contains multiple definitions of the function, each line represents an overload and you can choose which one you want to use.
90
+ Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
91
+ Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
86
92
 
87
93
  <br><br>
88
94
 
@@ -118,15 +124,18 @@ If set to `false` (default), querySelector() will be used and only the first mat
118
124
  If `continuous` is set to `true`, the listener will not be deregistered after it was called once (defaults to false).
119
125
 
120
126
  When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that the listener will return.
127
+ It will default to `HTMLElement` if left undefined.
121
128
 
122
129
  ⚠️ In order to use this function, [`initOnSelector()`](#initonselector) has to be called as soon as possible.
123
130
  This initialization function has to be called after `DOMContentLoaded` is fired (or immediately if `@run-at document-end` is set).
124
131
 
125
132
  Calling onSelector() before `DOMContentLoaded` is fired will not throw an error, but it also won't trigger listeners until the DOM is accessible.
126
133
 
127
- <details><summary><b>Example - click to view</b></summary>
134
+ <details><summary><h4>Example - click to view</h4></summary>
128
135
 
129
136
  ```ts
137
+ import { initOnSelector, onSelector } from "@sv443-network/userutils";
138
+
130
139
  document.addEventListener("DOMContentLoaded", initOnSelector);
131
140
 
132
141
  // Continuously checks if `div` elements are added to the DOM, then returns all of them (even previously detected ones) in a NodeList
@@ -155,7 +164,7 @@ Usage:
155
164
  ```ts
156
165
  initOnSelector(options?: MutationObserverInit): void
157
166
  ```
158
-
167
+
159
168
  Initializes the MutationObserver that is used by [`onSelector()`](#onselector) to check for the registered selectors whenever a DOM change occurs on the `<body>`
160
169
  By default, this only checks if elements are added or removed (at any depth).
161
170
 
@@ -169,9 +178,11 @@ You may see all options [here](https://developer.mozilla.org/en-US/docs/Web/API/
169
178
  >
170
179
  > ⚠️ Using these extra options can have a performance impact on larger sites or sites with a constantly changing DOM.
171
180
 
172
- <details><summary><b>Example - click to view</b></summary>
181
+ <details><summary><h4>Example - click to view</h4></summary>
173
182
 
174
183
  ```ts
184
+ import { initOnSelector } from "@sv443-network/userutils";
185
+
175
186
  document.addEventListener("DOMContentLoaded", () => {
176
187
  initOnSelector({
177
188
  attributes: true,
@@ -185,14 +196,19 @@ document.addEventListener("DOMContentLoaded", () => {
185
196
  <br>
186
197
 
187
198
  ### getSelectorMap()
188
- Usage: `getSelectorMap(): Map<string, OnSelectorOptions[]>`
199
+ Usage:
200
+ ```ts
201
+ getSelectorMap(): Map<string, OnSelectorOptions[]>
202
+ ```
189
203
 
190
204
  Returns a Map of all currently registered selectors and their options, including listener function.
191
205
  Since multiple listeners can be registered for the same selector, the value of the Map is an array of `OnSelectorOptions` objects.
192
206
 
193
- <details><summary><b>Example - click to view</b></summary>
207
+ <details><summary><h4>Example - click to view</h4></summary>
194
208
 
195
209
  ```ts
210
+ import { initOnSelector, onSelector, getSelectorMap } from "@sv443-network/userutils";
211
+
196
212
  document.addEventListener("DOMContentLoaded", initOnSelector);
197
213
 
198
214
  onSelector<HTMLDivElement>("div", {
@@ -225,14 +241,19 @@ const selectorMap = getSelectorMap();
225
241
  <br>
226
242
 
227
243
  ### getUnsafeWindow()
228
- Usage: `getUnsafeWindow(): Window`
244
+ Usage:
245
+ ```ts
246
+ getUnsafeWindow(): Window
247
+ ```
229
248
 
230
249
  Returns the unsafeWindow object or falls back to the regular window object if the `@grant unsafeWindow` is not given.
231
250
  Userscripts are sandboxed and do not have access to the regular window object, so this function is useful for websites that reject some events that were dispatched by the userscript.
232
251
 
233
- <details><summary><b>Example - click to view</b></summary>
252
+ <details><summary><h4>Example - click to view</h4></summary>
234
253
 
235
254
  ```ts
255
+ import { getUnsafeWindow } from "@sv443-network/userutils";
256
+
236
257
  // trick the site into thinking the mouse was moved:
237
258
  const mouseEvent = new MouseEvent("mousemove", {
238
259
  view: getUnsafeWindow(),
@@ -241,6 +262,7 @@ const mouseEvent = new MouseEvent("mousemove", {
241
262
  movementX: 10,
242
263
  movementY: 0,
243
264
  });
265
+
244
266
  document.body.dispatchEvent(mouseEvent);
245
267
  ```
246
268
 
@@ -249,20 +271,26 @@ document.body.dispatchEvent(mouseEvent);
249
271
  <br>
250
272
 
251
273
  ### insertAfter()
252
- Usage: `insertAfter(beforeElement: HTMLElement, afterElement: HTMLElement): HTMLElement`
274
+ Usage:
275
+ ```ts
276
+ insertAfter(beforeElement: Element, afterElement: Element): Element
277
+ ```
253
278
 
254
279
  Inserts the element passed as `afterElement` as a sibling after the passed `beforeElement`.
255
280
  The passed `afterElement` will be returned.
256
281
 
257
282
  ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
258
283
 
259
- <details><summary><b>Example - click to view</b></summary>
284
+ <details><summary><h4>Example - click to view</h4></summary>
260
285
 
261
286
  ```ts
287
+ import { insertAfter } from "@sv443-network/userutils";
288
+
262
289
  // insert a <div> as a sibling next to an element
263
290
  const beforeElement = document.querySelector("#before");
264
291
  const afterElement = document.createElement("div");
265
292
  afterElement.innerText = "After";
293
+
266
294
  insertAfter(beforeElement, afterElement);
267
295
  ```
268
296
 
@@ -271,20 +299,26 @@ insertAfter(beforeElement, afterElement);
271
299
  <br>
272
300
 
273
301
  ### addParent()
274
- Usage: `addParent(element: HTMLElement, newParent: HTMLElement): HTMLElement`
302
+ Usage:
303
+ ```ts
304
+ addParent(element: Element, newParent: Element): Element
305
+ ```
275
306
 
276
307
  Adds a parent element around the passed `element` and returns the new parent.
277
308
  Previously registered event listeners are kept intact.
278
309
 
279
310
  ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
280
311
 
281
- <details><summary><b>Example - click to view</b></summary>
312
+ <details><summary><h4>Example - click to view</h4></summary>
282
313
 
283
314
  ```ts
315
+ import { addParent } from "@sv443-network/userutils";
316
+
284
317
  // add an <a> around an element
285
318
  const element = document.querySelector("#element");
286
319
  const newParent = document.createElement("a");
287
320
  newParent.href = "https://example.org/";
321
+
288
322
  addParent(element, newParent);
289
323
  ```
290
324
 
@@ -293,14 +327,19 @@ addParent(element, newParent);
293
327
  <br>
294
328
 
295
329
  ### addGlobalStyle()
296
- Usage: `addGlobalStyle(css: string): void`
330
+ Usage:
331
+ ```ts
332
+ addGlobalStyle(css: string): void
333
+ ```
297
334
 
298
335
  Adds a global style to the page in form of a `<style>` element that's inserted into the `<head>`.
299
336
  ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
300
337
 
301
- <details><summary><b>Example - click to view</b></summary>
338
+ <details><summary><h4>Example - click to view</h4></summary>
302
339
 
303
340
  ```ts
341
+ import { addGlobalStyle } from "@sv443-network/userutils";
342
+
304
343
  document.addEventListener("DOMContentLoaded", () => {
305
344
  addGlobalStyle(`
306
345
  body {
@@ -315,15 +354,20 @@ document.addEventListener("DOMContentLoaded", () => {
315
354
  <br>
316
355
 
317
356
  ### preloadImages()
318
- Usage: `preloadImages(urls: string[], rejects?: boolean): Promise<void>`
357
+ Usage:
358
+ ```ts
359
+ preloadImages(urls: string[], rejects?: boolean): Promise<void>
360
+ ```
319
361
 
320
362
  Preloads images into browser cache by creating an invisible `<img>` element for each URL passed.
321
363
  The images will be loaded in parallel and the returned Promise will only resolve once all images have been loaded.
322
364
  The resulting PromiseSettledResult array will contain the image elements if resolved, or an ErrorEvent if rejected, but only if `rejects` is set to true.
323
365
 
324
- <details><summary><b>Example - click to view</b></summary>
366
+ <details><summary><h4>Example - click to view</h4></summary>
325
367
 
326
368
  ```ts
369
+ import { preloadImages } from "@sv443-network/userutils";
370
+
327
371
  preloadImages([
328
372
  "https://example.org/image1.png",
329
373
  "https://example.org/image2.png",
@@ -339,7 +383,10 @@ preloadImages([
339
383
  <br>
340
384
 
341
385
  ### openInNewTab()
342
- Usage: `openInNewTab(url: string): void`
386
+ Usage:
387
+ ```ts
388
+ openInNewTab(url: string): void
389
+ ```
343
390
 
344
391
  Creates an invisible anchor with a `_blank` target and clicks it.
345
392
  Contrary to `window.open()`, this has a lesser chance to get blocked by the browser's popup blocker and doesn't open the URL as a new window.
@@ -347,9 +394,11 @@ This function has to be run in response to a user interaction event, else the br
347
394
 
348
395
  ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
349
396
 
350
- <details><summary><b>Example - click to view</b></summary>
397
+ <details><summary><h4>Example - click to view</h4></summary>
351
398
 
352
399
  ```ts
400
+ import { openInNewTab } from "@sv443-network/userutils";
401
+
353
402
  document.querySelector("#my-button").addEventListener("click", () => {
354
403
  openInNewTab("https://example.org/");
355
404
  });
@@ -360,17 +409,27 @@ document.querySelector("#my-button").addEventListener("click", () => {
360
409
  <br>
361
410
 
362
411
  ### interceptEvent()
363
- Usage: `interceptEvent(eventObject: EventTarget, eventName: string, predicate: () => boolean): void`
412
+ Usage:
413
+ ```ts
414
+ interceptEvent(
415
+ eventObject: EventTarget,
416
+ eventName: string,
417
+ predicate: (event: Event) => boolean
418
+ ): void
419
+ ```
364
420
 
365
421
  Intercepts all events dispatched on the `eventObject` and prevents the listeners from being called as long as the predicate function returns a truthy value.
366
422
  Calling this function will set the `Error.stackTraceLimit` to 1000 (if it's not already higher) to ensure the stack trace is preserved.
367
423
 
368
424
  ⚠️ This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are *attached* after this function is called.
369
425
 
370
- <details><summary><b>Example - click to view</b></summary>
426
+ <details><summary><h4>Example - click to view</h4></summary>
371
427
 
372
428
  ```ts
373
- interceptEvent(document.body, "click", () => {
429
+ import { interceptEvent } from "@sv443-network/userutils";
430
+
431
+ interceptEvent(document.body, "click", (event) => {
432
+ console.log("Intercepting click event:", event);
374
433
  return true; // prevent all click events on the body element
375
434
  });
376
435
  ```
@@ -380,17 +439,26 @@ interceptEvent(document.body, "click", () => {
380
439
  <br>
381
440
 
382
441
  ### interceptWindowEvent()
383
- Usage: `interceptWindowEvent(eventName: string, predicate: () => boolean): void`
442
+ Usage:
443
+ ```ts
444
+ interceptWindowEvent(
445
+ eventName: string,
446
+ predicate: (event: Event) => boolean
447
+ ): void
448
+ ```
384
449
 
385
450
  Intercepts all events dispatched on the `window` object and prevents the listeners from being called as long as the predicate function returns a truthy value.
386
451
  This is essentially the same as [`interceptEvent()`](#interceptevent), but automatically uses the `unsafeWindow` (or falls back to regular `window`).
387
452
 
388
453
  ⚠️ This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are *attached* after this function is called.
454
+ ⚠️ In order for all events to be interceptable, the directive `@grant unsafeWindow` should be set.
389
455
 
390
- <details><summary><b>Example - click to view</b></summary>
456
+ <details><summary><h4>Example - click to view</h4></summary>
391
457
 
392
458
  ```ts
393
- interceptWindowEvent("beforeunload", () => {
459
+ import { interceptWindowEvent } from "@sv443-network/userutils";
460
+
461
+ interceptWindowEvent("beforeunload", (event) => {
394
462
  return true; // prevent the pesky "Are you sure you want to leave this page?" popup
395
463
  });
396
464
  ```
@@ -400,7 +468,10 @@ interceptWindowEvent("beforeunload", () => {
400
468
  <br>
401
469
 
402
470
  ### amplifyMedia()
403
- Usage: `amplifyMedia(mediaElement: HTMLMediaElement, multiplier?: number): AmplifyMediaResult`
471
+ Usage:
472
+ ```ts
473
+ amplifyMedia(mediaElement: HTMLMediaElement, multiplier?: number): AmplifyMediaResult
474
+ ```
404
475
 
405
476
  Amplifies the gain of a media element (like `<audio>` or `<video>`) by a given multiplier (defaults to 1.0).
406
477
  This is how you can increase the volume of a media element beyond the default maximum volume of 1.0 or 100%.
@@ -408,7 +479,7 @@ Make sure to limit the multiplier to a reasonable value ([clamp()](#clamp) is go
408
479
 
409
480
  ⚠️ This function has to be run in response to a user interaction event, else the browser will reject it because of the strict autoplay policy.
410
481
 
411
- Returns an object with the following properties:
482
+ The returned AmplifyMediaResult object has the following properties:
412
483
  | Property | Description |
413
484
  | :-- | :-- |
414
485
  | `mediaElement` | The passed media element |
@@ -418,9 +489,11 @@ Returns an object with the following properties:
418
489
  | `source` | The MediaElementSourceNode instance |
419
490
  | `gain` | The GainNode instance |
420
491
 
421
- <details><summary><b>Example - click to view</b></summary>
492
+ <details><summary><h4>Example - click to view</h4></summary>
422
493
 
423
494
  ```ts
495
+ import { amplifyMedia } from "@sv443-network/userutils";
496
+
424
497
  const audio = document.querySelector<HTMLAudioElement>("audio");
425
498
  const button = document.querySelector<HTMLButtonElement>("button");
426
499
 
@@ -443,22 +516,56 @@ button.addEventListener("click", () => {
443
516
 
444
517
  </details>
445
518
 
519
+ <br>
520
+
521
+ ### isScrollable()
522
+ Usage:
523
+ ```ts
524
+ isScrollable(element: Element): { horizontal: boolean, vertical: boolean }
525
+ ```
526
+
527
+ Checks if an element has a horizontal or vertical scroll bar.
528
+ This uses the computed style of the element, so it will also work if the element is hidden.
529
+
530
+ <details><summary><h4>Example - click to view</h4></summary>
531
+
532
+ ```ts
533
+ import { isScrollable } from "@sv443-network/userutils";
534
+
535
+ const element = document.querySelector("#element");
536
+ const { horizontal, vertical } = isScrollable(element);
537
+
538
+ console.log("Element has a horizontal scroll bar:", horizontal);
539
+ console.log("Element has a vertical scroll bar:", vertical);
540
+ ```
541
+
542
+ </details>
543
+
446
544
  <br><br>
447
545
 
448
546
  ## Math:
449
547
 
450
548
  ### clamp()
451
- Usage: `clamp(num: number, min: number, max: number): number`
549
+ Usage:
550
+ ```ts
551
+ clamp(num: number, min: number, max: number): number
552
+ ```
452
553
 
453
- Clamps a number between a min and max value.
554
+ Clamps a number between a min and max boundary (inclusive).
454
555
 
455
- <details><summary><b>Example - click to view</b></summary>
556
+ <details><summary><h4>Example - click to view</h4></summary>
456
557
 
457
558
  ```ts
458
- clamp(5, 0, 10); // 5
459
- clamp(-1, 0, 10); // 0
460
- clamp(7, 0, 10); // 7
461
- clamp(Infinity, 0, 10); // 10
559
+ import { clamp } from "@sv443-network/userutils";
560
+
561
+ clamp(7, 0, 10); // 7
562
+ clamp(-1, 0, 10); // 0
563
+ clamp(5, -5, 0); // 0
564
+ clamp(99999, 0, 10); // 10
565
+
566
+ // clamp without a min or max boundary:
567
+ clamp(-99999, -Infinity, 0); // -99999
568
+ clamp(99999, 0, Infinity); // 99999
462
569
  ```
463
570
 
464
571
  </details>
@@ -466,16 +573,29 @@ clamp(Infinity, 0, 10); // 10
466
573
  <br>
467
574
 
468
575
  ### mapRange()
469
- Usage: `mapRange(value: number, range_1_min: number, range_1_max: number, range_2_min: number, range_2_max: number): number`
576
+ Usage:
577
+ ```ts
578
+ mapRange(
579
+ value: number,
580
+ range_1_min: number,
581
+ range_1_max: number,
582
+ range_2_min: number,
583
+ range_2_max: number
584
+ ): number
585
+ ```
470
586
 
471
587
  Maps a number from one range to the spot it would be in another range.
472
588
 
473
- <details><summary><b>Example - click to view</b></summary>
589
+ <details><summary><h4>Example - click to view</h4></summary>
474
590
 
475
591
  ```ts
592
+ import { mapRange } from "@sv443-network/userutils";
593
+
476
594
  mapRange(5, 0, 10, 0, 100); // 50
477
595
  mapRange(5, 0, 10, 0, 50); // 25
478
- // to calculate a percentage from arbitrary values, use 0 and 100 as the second range:
596
+
597
+ // to calculate a percentage from arbitrary values, use 0 and 100 as the second range
598
+ // for example, if 4 files of a total of 13 were downloaded:
479
599
  mapRange(4, 0, 13, 0, 100); // 30.76923076923077
480
600
  ```
481
601
 
@@ -493,9 +613,11 @@ randRange(max: number): number
493
613
  Returns a random number between `min` and `max` (inclusive).
494
614
  If only one argument is passed, it will be used as the `max` value and `min` will be set to 0.
495
615
 
496
- <details><summary><b>Example - click to view</b></summary>
616
+ <details><summary><h4>Example - click to view</h4></summary>
497
617
 
498
618
  ```ts
619
+ import { randRange } from "@sv443-network/userutils";
620
+
499
621
  randRange(0, 10); // 4
500
622
  randRange(10, 20); // 17
501
623
  randRange(10); // 7
@@ -507,15 +629,150 @@ randRange(10); // 7
507
629
 
508
630
  ## Misc:
509
631
 
632
+ ### ConfigManager()
633
+ Usage:
634
+ ```ts
635
+ new ConfigManager(options: ConfigManagerOptions)
636
+ ```
637
+
638
+ A class that manages a userscript's configuration that is persistently saved to and loaded from GM storage.
639
+ Also supports automatic migration of outdated data formats via provided migration functions.
640
+
641
+ ⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used.
642
+ ⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required for this to work.
643
+
644
+ The options object has the following properties:
645
+ | Property | Description |
646
+ | :-- | :-- |
647
+ | `id` | A unique internal identification string for this configuration. If two ConfigManagers share the same ID, they will overwrite each other's data. Choose wisely because if it is changed, the previously saved data will not be able to be loaded anymore. |
648
+ | `defaultConfig` | The default config data to use if no data is saved in persistent storage yet. Until the data is loaded from persistent storage, this will be the data returned by `getData()`. For TypeScript, the type of the data passed here is what will be used for all other methods of the instance. |
649
+ | `formatVersion` | An incremental version of the data format. If the format of the data is changed in any way, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively. Never decrement this number or skip numbers. |
650
+ | `migrations?` | (Optional) A dictionary of functions that can be used to migrate data from older versions of the configuration to newer ones. The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value. The values should be functions that take the data in the old format and return the data in the new format. The functions will be run in order from the oldest to the newest version. If the current format version is not in the dictionary, no migrations will be run. |
651
+
652
+ <br>
653
+
654
+ ### Methods:
655
+ `loadData(): Promise<TData>`
656
+ Asynchronously loads the configuration data from persistent storage and returns it.
657
+ If no data was saved in persistent storage before, the value of `options.defaultConfig` will be returned and written to persistent storage.
658
+ If the formatVersion of the saved data is lower than the current one and the `options.migrations` property is present, the data will be migrated to the latest format before the Promise resolves.
659
+
660
+ `getData(): TData`
661
+ Synchronously returns the current data that is stored in the internal cache.
662
+ If no data was loaded from persistent storage yet using `loadData()`, the value of `options.defaultConfig` will be returned.
663
+
664
+ `setData(data: TData): Promise<void>`
665
+ Writes the given data synchronously to the internal cache and asynchronously to persistent storage.
666
+
667
+ `saveDefaultData(): Promise<void>`
668
+ Writes the default configuration given in `options.defaultConfig` synchronously to the internal cache and asynchronously to persistent storage.
669
+
670
+ `deleteConfig(): Promise<void>`
671
+ Fully deletes the configuration from persistent storage.
672
+ 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.
675
+
676
+ <br>
677
+
678
+ <details><summary><h4>Example - click to view</h4></summary>
679
+
680
+ ```ts
681
+ import { ConfigManager } from "@sv443-network/userutils";
682
+
683
+ interface MyConfig {
684
+ foo: string;
685
+ bar: number;
686
+ baz: string;
687
+ qux: string;
688
+ }
689
+
690
+ /** Default config data */
691
+ const defaultConfig: MyConfig = {
692
+ foo: "hello",
693
+ bar: 42,
694
+ baz: "xyz",
695
+ qux: "something",
696
+ };
697
+ /** If any properties are added to, removed from or renamed in MyConfig, increment this number */
698
+ const formatVersion = 2;
699
+ /** 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
+ const migrations = {
701
+ // migrate from format version 0 to 1
702
+ 1: (oldData: any) => {
703
+ return {
704
+ foo: oldData.foo,
705
+ bar: oldData.bar,
706
+ baz: "world",
707
+ };
708
+ },
709
+ // asynchronously migrate from format version 1 to 2
710
+ 2: async (oldData: any) => {
711
+ // arbitrary async operation required for the new format
712
+ const qux = JSON.parse(await (await fetch("https://api.example.org/some-data")).text());
713
+ return {
714
+ foo: oldData.foo,
715
+ bar: oldData.bar,
716
+ baz: oldData.baz,
717
+ qux,
718
+ };
719
+ },
720
+ };
721
+
722
+ const configMgr = new ConfigManager({
723
+ /** A unique ID for this configuration - choose wisely as changing it is not supported yet! */
724
+ id: "my-userscript",
725
+ /** Default / fallback configuration data */
726
+ defaultConfig,
727
+ /** The current version of the script's config data format */
728
+ formatVersion,
729
+ /** Data format migration functions */
730
+ migrations,
731
+ });
732
+
733
+ /** Entrypoint of the userscript */
734
+ async function init() {
735
+ // wait for the config to be loaded from persistent storage
736
+ // if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultConfig will be returned
737
+ // 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
+
740
+ console.log(configData.foo); // "hello"
741
+
742
+ // update the config
743
+ configData.foo = "world";
744
+ configData.bar = 123;
745
+
746
+ // save the updated config - synchronously to the cache and asynchronously to persistent storage
747
+ configMgr.saveData(configData).then(() => {
748
+ console.log("Config saved to persistent storage!");
749
+ });
750
+
751
+ // 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
+ }
754
+
755
+ init();
756
+ ```
757
+
758
+ </details>
759
+
760
+ <br><br>
761
+
510
762
  ### autoPlural()
511
- Usage: `autoPlural(str: string, num: number | Array | NodeList): string`
763
+ Usage:
764
+ ```ts
765
+ autoPlural(str: string, num: number | Array | NodeList): string
766
+ ```
512
767
 
513
768
  Automatically pluralizes a string if the given number is not 1.
514
- If an array or NodeList is passed, the length of it will be used.
769
+ If an array or NodeList is passed, the amount of contained items will be used.
515
770
 
516
- <details><summary><b>Example - click to view</b></summary>
771
+ <details><summary><h4>Example - click to view</h4></summary>
517
772
 
518
773
  ```ts
774
+ import { autoPlural } from "@sv443-network/userutils";
775
+
519
776
  autoPlural("apple", 0); // "apples"
520
777
  autoPlural("apple", 1); // "apple"
521
778
  autoPlural("apple", 2); // "apples"
@@ -532,13 +789,18 @@ console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 i
532
789
  <br>
533
790
 
534
791
  ### pauseFor()
535
- Usage: `pauseFor(ms: number): Promise<void>`
792
+ Usage:
793
+ ```ts
794
+ pauseFor(ms: number): Promise<void>
795
+ ```
536
796
 
537
797
  Pauses async execution for a given amount of time.
538
798
 
539
- <details><summary><b>Example - click to view</b></summary>
799
+ <details><summary><h4>Example - click to view</h4></summary>
540
800
 
541
801
  ```ts
802
+ import { pauseFor } from "@sv443-network/userutils";
803
+
542
804
  async function run() {
543
805
  console.log("Hello");
544
806
  await pauseFor(3000); // waits for 3 seconds
@@ -551,15 +813,21 @@ async function run() {
551
813
  <br>
552
814
 
553
815
  ### debounce()
554
- Usage: `debounce(func: Function, timeout?: number): Function`
816
+ Usage:
817
+ ```ts
818
+ debounce(func: Function, timeout?: number): Function
819
+ ```
555
820
 
556
821
  Debounces a function, meaning that it will only be called once after a given amount of time.
557
822
  This is very useful for functions that are called repeatedly, like event listeners, to remove extraneous calls.
823
+ All passed properties will be passed down to the debounced function.
558
824
  The timeout will default to 300ms if left undefined.
559
825
 
560
- <details><summary><b>Example - click to view</b></summary>
826
+ <details><summary><h4>Example - click to view</h4></summary>
561
827
 
562
828
  ```ts
829
+ import { debounce } from "@sv443-network/userutils";
830
+
563
831
  window.addEventListener("resize", debounce((event) => {
564
832
  console.log("Window was resized:", event);
565
833
  }, 500)); // 500ms timeout
@@ -581,10 +849,12 @@ fetchAdvanced(url: string, options?: {
581
849
  A wrapper around the native `fetch()` function that adds options like a timeout property.
582
850
  The timeout will default to 10 seconds if left undefined.
583
851
 
584
- <details><summary><b>Example - click to view</b></summary>
852
+ <details><summary><h4>Example - click to view</h4></summary>
585
853
 
586
854
  ```ts
587
- fetchAdvanced("https://api.example.org/data", {
855
+ import { fetchAdvanced } from "@sv443-network/userutils";
856
+
857
+ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
588
858
  timeout: 5000,
589
859
  // also accepts any other fetch options like headers:
590
860
  headers: {
@@ -602,14 +872,19 @@ fetchAdvanced("https://api.example.org/data", {
602
872
  ## Arrays:
603
873
 
604
874
  ### randomItem()
605
- Usage: `randomItem(array: Array): any`
875
+ Usage:
876
+ ```ts
877
+ randomItem(array: Array): any
878
+ ```
606
879
 
607
880
  Returns a random item from an array.
608
881
  Returns undefined if the array is empty.
609
882
 
610
- <details><summary><b>Example - click to view</b></summary>
883
+ <details><summary><h4>Example - click to view</h4></summary>
611
884
 
612
885
  ```ts
886
+ import { randomItem } from "@sv443-network/userutils";
887
+
613
888
  randomItem(["foo", "bar", "baz"]); // "bar"
614
889
  randomItem([ ]); // undefined
615
890
  ```
@@ -619,20 +894,26 @@ randomItem([ ]); // undefined
619
894
  <br>
620
895
 
621
896
  ### randomItemIndex()
622
- Usage: `randomItemIndex(array: Array): [item: any, index: number]`
897
+ Usage:
898
+ ```ts
899
+ randomItemIndex(array: Array): [item: any, index: number]
900
+ ```
623
901
 
624
902
  Returns a tuple of a random item and its index from an array.
625
903
  If the array is empty, it will return undefined for both values.
626
904
 
627
- <details><summary><b>Example - click to view</b></summary>
905
+ <details><summary><h4>Example - click to view</h4></summary>
628
906
 
629
907
  ```ts
908
+ import { randomItemIndex } from "@sv443-network/userutils";
909
+
630
910
  randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
631
911
  randomItemIndex([ ]); // [undefined, undefined]
912
+
632
913
  // using array destructuring:
633
- const [item, index] = randomItemIndex(["foo", "bar", "baz"]);
914
+ const [item, index] = randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
634
915
  // or if you only want the index:
635
- const [, index] = randomItemIndex(["foo", "bar", "baz"]);
916
+ const [, index] = randomItemIndex(["foo", "bar", "baz"]); // 1
636
917
  ```
637
918
 
638
919
  </details>
@@ -640,14 +921,19 @@ const [, index] = randomItemIndex(["foo", "bar", "baz"]);
640
921
  <br>
641
922
 
642
923
  ### takeRandomItem()
643
- Usage: `takeRandomItem(array: Array): any`
924
+ Usage:
925
+ ```ts
926
+ takeRandomItem(array: Array): any
927
+ ```
644
928
 
645
929
  Returns a random item from an array and mutates the array by removing the item.
646
930
  Returns undefined if the array is empty.
647
931
 
648
- <details><summary><b>Example - click to view</b></summary>
932
+ <details><summary><h4>Example - click to view</h4></summary>
649
933
 
650
934
  ```ts
935
+ import { takeRandomItem } from "@sv443-network/userutils";
936
+
651
937
  const arr = ["foo", "bar", "baz"];
652
938
  takeRandomItem(arr); // "bar"
653
939
  console.log(arr); // ["foo", "baz"]
@@ -658,15 +944,25 @@ console.log(arr); // ["foo", "baz"]
658
944
  <br>
659
945
 
660
946
  ### randomizeArray()
661
- Usage: `randomizeArray(array: Array): Array`
947
+ Usage:
948
+ ```ts
949
+ randomizeArray(array: Array): Array
950
+ ```
662
951
 
663
- Returns a copy of the array with its items in a random order.
664
- If the array is empty, the originally passed array will be returned.
952
+ Returns a copy of an array with its items in a random order.
953
+ If the array is empty, the originally passed empty array will be returned without copying.
665
954
 
666
- <details><summary><b>Example - click to view</b></summary>
955
+ <details><summary><h4>Example - click to view</h4></summary>
667
956
 
668
957
  ```ts
669
- randomizeArray([1, 2, 3, 4, 5, 6]); // [3, 1, 5, 2, 4, 6]
958
+ import { randomizeArray } from "@sv443-network/userutils";
959
+
960
+ const foo = [1, 2, 3, 4, 5, 6];
961
+
962
+ console.log(randomizeArray(foo)); // [3, 1, 5, 2, 4, 6]
963
+ console.log(randomizeArray(foo)); // [4, 5, 2, 1, 6, 3]
964
+
965
+ console.log(foo); // [1, 2, 3, 4, 5, 6] - original array is not mutated
670
966
  ```
671
967
 
672
968
  </details>
package/dist/index.js CHANGED
@@ -1,28 +1,30 @@
1
1
  'use strict';
2
2
 
3
- var h=Object.defineProperty,y=Object.defineProperties;var g=Object.getOwnPropertyDescriptors;var p=Object.getOwnPropertySymbols;var w=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable;var f=(t,e,n)=>e in t?h(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,c=(t,e)=>{for(var n in e||(e={}))w.call(e,n)&&f(t,n,e[n]);if(p)for(var n of p(e))v.call(e,n)&&f(t,n,e[n]);return t},b=(t,e)=>y(t,g(e));var T=(t,e,n)=>new Promise((r,o)=>{var i=s=>{try{a(n.next(s));}catch(m){o(m);}},u=s=>{try{a(n.throw(s));}catch(m){o(m);}},a=s=>s.done?r(s.value):Promise.resolve(s.value).then(i,u);a((n=n.apply(t,e)).next());});function S(t,e,n){return Math.max(Math.min(t,n),e)}function A(t,e,n,r,o){return Number(e)===0&&Number(r)===0?t*(o/n):(t-e)*((o-r)/(n-e))+r}function d(...t){let e,n;if(typeof t[0]=="number"&&typeof t[1]=="number")[e,n]=t;else if(typeof t[0]=="number"&&typeof t[1]!="number")e=0,n=t[0];else throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof t[0]}" and "${typeof t[1]}"`);if(e=Number(e),n=Number(n),isNaN(e)||isNaN(n))throw new TypeError(`Parameters "min" and "max" can't be NaN`);if(e>n)throw new TypeError(`Parameter "min" can't be bigger than "max"`);return Math.floor(Math.random()*(n-e+1))+e}function H(t){return x(t)[0]}function x(t){if(t.length===0)return [void 0,void 0];let e=d(t.length-1);return [t[e],e]}function I(t){let[e,n]=x(t);if(n!==void 0)return t.splice(n,1),e}function P(t){let e=[...t];if(t.length===0)return t;for(let n=e.length-1;n>0;n--){let r=Math.floor(d(0,1e4)/1e4*(n+1));[e[n],e[r]]=[e[r],e[n]];}return e}function O(){try{return unsafeWindow}catch(t){return window}}function j(t,e){var n;return (n=t.parentNode)==null||n.insertBefore(e,t.nextSibling),e}function R(t,e){let n=t.parentNode;if(!n)throw new Error("Element doesn't have a parent node");return n.replaceChild(e,t),e.appendChild(t),e}function F(t){let e=document.createElement("style");e.innerHTML=t,document.head.appendChild(e);}function W(t,e=!1){let n=t.map(r=>new Promise((o,i)=>{let u=new Image;u.src=r,u.addEventListener("load",()=>o(u)),u.addEventListener("error",a=>e&&i(a));}));return Promise.allSettled(n)}function $(t){let e=document.createElement("a");Object.assign(e,{className:"userutils-open-in-new-tab",target:"_blank",rel:"noopener noreferrer",href:t}),e.style.display="none",document.body.appendChild(e),e.click(),setTimeout(e.remove,50);}function L(t,e,n){typeof Error.stackTraceLimit=="number"&&Error.stackTraceLimit<1e3&&(Error.stackTraceLimit=1e3),function(r){element.__proto__.addEventListener=function(...o){if(!(o[0]===e&&n()))return r.apply(this,o)};}(t.__proto__.addEventListener);}function B(t,e){return L(O(),t,e)}function q(t,e=1){let n=new(window.AudioContext||window.webkitAudioContext),r={mediaElement:t,amplify:o=>{r.gain.gain.value=o;},getAmpLevel:()=>r.gain.gain.value,context:n,source:n.createMediaElementSource(t),gain:n.createGain()};return r.source.connect(r.gain),r.gain.connect(n.destination),r.amplify(e),r}function z(t,e){return (Array.isArray(e)||e instanceof NodeList)&&(e=e.length),`${t}${e===1?"":"s"}`}function U(t){return new Promise(e=>{setTimeout(e,t);})}function D(t,e=300){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>t.apply(this,r),e);}}function J(n){return T(this,arguments,function*(t,e={}){let{timeout:r=1e4}=e,o=new AbortController,i=setTimeout(()=>o.abort(),r),u=yield fetch(t,b(c({},e),{signal:o.signal}));return clearTimeout(i),u})}var l=new Map;function V(t,e){let n=[];l.has(t)&&(n=l.get(t)),n.push(e),l.set(t,n),E(t,n);}function X(t){return l.delete(t)}function E(t,e){let n=[];if(e.forEach((r,o)=>{try{let i=r.all?document.querySelectorAll(t):document.querySelector(t);(i!==null&&i instanceof NodeList&&i.length>0||i!==null)&&(r.listener(i),r.continuous||n.push(o));}catch(i){console.error(`Couldn't call listener for selector '${t}'`,i);}}),n.length>0){let r=e.filter((o,i)=>!n.includes(i));r.length===0?l.delete(t):l.set(t,r);}}function Y(t={}){new MutationObserver(()=>{for(let[n,r]of l.entries())E(n,r);}).observe(document.body,c({subtree:!0,childList:!0},t));}function Z(){return l}
3
+ var v=Object.defineProperty,x=Object.defineProperties;var w=Object.getOwnPropertyDescriptors;var h=Object.getOwnPropertySymbols;var O=Object.prototype.hasOwnProperty,M=Object.prototype.propertyIsEnumerable;var p=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,f=(t,e)=>{for(var n in e||(e={}))O.call(e,n)&&p(t,n,e[n]);if(h)for(var n of h(e))M.call(e,n)&&p(t,n,e[n]);return t},b=(t,e)=>x(t,w(e));var m=(t,e,n)=>(p(t,typeof e!="symbol"?e+"":e,n),n);var c=(t,e,n)=>new Promise((r,i)=>{var o=s=>{try{u(n.next(s));}catch(l){i(l);}},a=s=>{try{u(n.throw(s));}catch(l){i(l);}},u=s=>s.done?r(s.value):Promise.resolve(s.value).then(o,a);u((n=n.apply(t,e)).next());});function D(t,e,n){return Math.max(Math.min(t,n),e)}function L(t,e,n,r,i){return Number(e)===0&&Number(r)===0?t*(i/n):(t-e)*((i-r)/(n-e))+r}function g(...t){let e,n;if(typeof t[0]=="number"&&typeof t[1]=="number")[e,n]=t;else if(typeof t[0]=="number"&&typeof t[1]!="number")e=0,n=t[0];else throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof t[0]}" and "${typeof t[1]}"`);if(e=Number(e),n=Number(n),isNaN(e)||isNaN(n))throw new TypeError(`Parameters "min" and "max" can't be NaN`);if(e>n)throw new TypeError(`Parameter "min" can't be bigger than "max"`);return Math.floor(Math.random()*(n-e+1))+e}function V(t){return T(t)[0]}function T(t){if(t.length===0)return [void 0,void 0];let e=g(t.length-1);return [t[e],e]}function $(t){let[e,n]=T(t);if(n!==void 0)return t.splice(n,1),e}function k(t){let e=[...t];if(t.length===0)return t;for(let n=e.length-1;n>0;n--){let r=Math.floor(g(0,1e4)/1e4*(n+1));[e[n],e[r]]=[e[r],e[n]];}return e}var y=class{constructor(e){m(this,"id");m(this,"formatVersion");m(this,"defaultConfig");m(this,"cachedConfig");m(this,"migrations");this.id=e.id,this.formatVersion=e.formatVersion,this.defaultConfig=e.defaultConfig,this.cachedConfig=e.defaultConfig,this.migrations=e.migrations;}loadData(){return c(this,null,function*(){try{let e=yield GM.getValue(`_uucfg-${this.id}`,this.defaultConfig),n=Number(yield GM.getValue(`_uucfgver-${this.id}`));if(typeof e!="string")return yield this.saveDefaultData(),this.defaultConfig;isNaN(n)&&(yield GM.setValue(`_uucfgver-${this.id}`,n=this.formatVersion));let r=JSON.parse(e);return n<this.formatVersion&&this.migrations&&(r=yield this.runMigrations(r,n)),this.cachedConfig=typeof r=="object"?r:void 0}catch(e){return yield this.saveDefaultData(),this.defaultConfig}})}getData(){return this.deepCopy(this.cachedConfig)}setData(e){return this.cachedConfig=e,new Promise(n=>c(this,null,function*(){yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(e)),GM.setValue(`_uucfgver-${this.id}`,this.formatVersion)]),n();}))}saveDefaultData(){return c(this,null,function*(){return this.cachedConfig=this.defaultConfig,new Promise(e=>c(this,null,function*(){yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(this.defaultConfig)),GM.setValue(`_uucfgver-${this.id}`,this.formatVersion)]),e();}))})}deleteConfig(){return c(this,null,function*(){yield Promise.allSettled([GM.deleteValue(`_uucfg-${this.id}`),GM.deleteValue(`_uucfgver-${this.id}`)]);})}runMigrations(e,n){return c(this,null,function*(){if(!this.migrations)return e;let r=e,i=Object.entries(this.migrations).sort(([a],[u])=>Number(a)-Number(u)),o=n;for(let[a,u]of i){let s=Number(a);if(n<this.formatVersion&&n<s)try{let l=u(r);r=l instanceof Promise?yield l:l,o=n=s;}catch(l){console.error(`Error while running migration function for format version ${a}:`,l);}}return yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(r)),GM.setValue(`_uucfgver-${this.id}`,o)]),r})}deepCopy(e){return JSON.parse(JSON.stringify(e))}};function S(){try{return unsafeWindow}catch(t){return window}}function _(t,e){var n;return (n=t.parentNode)==null||n.insertBefore(e,t.nextSibling),e}function j(t,e){let n=t.parentNode;if(!n)throw new Error("Element doesn't have a parent node");return n.replaceChild(e,t),e.appendChild(t),e}function W(t){let e=document.createElement("style");e.innerHTML=t,document.head.appendChild(e);}function F(t,e=!1){let n=t.map(r=>new Promise((i,o)=>{let a=new Image;a.src=r,a.addEventListener("load",()=>i(a)),a.addEventListener("error",u=>e&&o(u));}));return Promise.allSettled(n)}function H(t){let e=document.createElement("a");Object.assign(e,{className:"userutils-open-in-new-tab",target:"_blank",rel:"noopener noreferrer",href:t}),e.style.display="none",document.body.appendChild(e),e.click(),setTimeout(e.remove,50);}function C(t,e,n){typeof Error.stackTraceLimit=="number"&&Error.stackTraceLimit<1e3&&(Error.stackTraceLimit=1e3),function(r){t.__proto__.addEventListener=function(...i){var a,u;let o=typeof i[1]=="function"?i[1]:(u=(a=i[1])==null?void 0:a.handleEvent)!=null?u:()=>{};i[1]=function(...s){if(!(i[0]===e&&n(Array.isArray(s)?s[0]:s)))return o.apply(this,s)},r.apply(this,i);};}(t.__proto__.addEventListener);}function J(t,e){return C(S(),t,e)}function q(t,e=1){let n=new(window.AudioContext||window.webkitAudioContext),r={mediaElement:t,amplify:i=>{r.gain.gain.value=i;},getAmpLevel:()=>r.gain.gain.value,context:n,source:n.createMediaElementSource(t),gain:n.createGain()};return r.source.connect(r.gain),r.gain.connect(n.destination),r.amplify(e),r}function K(t){let{overflowX:e,overflowY:n}=getComputedStyle(t);return {vertical:(n==="scroll"||n==="auto")&&t.scrollHeight>t.clientHeight,horizontal:(e==="scroll"||e==="auto")&&t.scrollWidth>t.clientWidth}}function B(t,e){return (Array.isArray(e)||e instanceof NodeList)&&(e=e.length),`${t}${e===1?"":"s"}`}function U(t){return new Promise(e=>{setTimeout(()=>e(),t);})}function X(t,e=300){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>t.apply(this,r),e);}}function Y(n){return c(this,arguments,function*(t,e={}){let{timeout:r=1e4}=e,i=new AbortController,o=setTimeout(()=>i.abort(),r),a=yield fetch(t,b(f({},e),{signal:i.signal}));return clearTimeout(o),a})}var d=new Map;function ee(t,e){let n=[];d.has(t)&&(n=d.get(t)),n.push(e),d.set(t,n),E(t,n);}function te(t){return d.delete(t)}function E(t,e){let n=[];if(e.forEach((r,i)=>{try{let o=r.all?document.querySelectorAll(t):document.querySelector(t);(o!==null&&o instanceof NodeList&&o.length>0||o!==null)&&(r.listener(o),r.continuous||n.push(i));}catch(o){console.error(`Couldn't call listener for selector '${t}'`,o);}}),n.length>0){let r=e.filter((i,o)=>!n.includes(o));r.length===0?d.delete(t):d.set(t,r);}}function ne(t={}){new MutationObserver(()=>{for(let[n,r]of d.entries())E(n,r);}).observe(document.body,f({subtree:!0,childList:!0},t));}function re(){return d}
4
4
 
5
- exports.addGlobalStyle = F;
6
- exports.addParent = R;
5
+ exports.ConfigManager = y;
6
+ exports.addGlobalStyle = W;
7
+ exports.addParent = j;
7
8
  exports.amplifyMedia = q;
8
- exports.autoPlural = z;
9
- exports.clamp = S;
10
- exports.debounce = D;
11
- exports.fetchAdvanced = J;
12
- exports.getSelectorMap = Z;
13
- exports.getUnsafeWindow = O;
14
- exports.initOnSelector = Y;
15
- exports.insertAfter = j;
16
- exports.interceptEvent = L;
17
- exports.interceptWindowEvent = B;
18
- exports.mapRange = A;
19
- exports.onSelector = V;
20
- exports.openInNewTab = $;
9
+ exports.autoPlural = B;
10
+ exports.clamp = D;
11
+ exports.debounce = X;
12
+ exports.fetchAdvanced = Y;
13
+ exports.getSelectorMap = re;
14
+ exports.getUnsafeWindow = S;
15
+ exports.initOnSelector = ne;
16
+ exports.insertAfter = _;
17
+ exports.interceptEvent = C;
18
+ exports.interceptWindowEvent = J;
19
+ exports.isScrollable = K;
20
+ exports.mapRange = L;
21
+ exports.onSelector = ee;
22
+ exports.openInNewTab = H;
21
23
  exports.pauseFor = U;
22
- exports.preloadImages = W;
23
- exports.randRange = d;
24
- exports.randomItem = H;
25
- exports.randomItemIndex = x;
26
- exports.randomizeArray = P;
27
- exports.removeOnSelector = X;
28
- exports.takeRandomItem = I;
24
+ exports.preloadImages = F;
25
+ exports.randRange = g;
26
+ exports.randomItem = V;
27
+ exports.randomItemIndex = T;
28
+ exports.randomizeArray = k;
29
+ exports.removeOnSelector = te;
30
+ exports.takeRandomItem = $;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var h=Object.defineProperty,y=Object.defineProperties;var g=Object.getOwnPropertyDescriptors;var p=Object.getOwnPropertySymbols;var w=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable;var f=(t,e,n)=>e in t?h(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,c=(t,e)=>{for(var n in e||(e={}))w.call(e,n)&&f(t,n,e[n]);if(p)for(var n of p(e))v.call(e,n)&&f(t,n,e[n]);return t},b=(t,e)=>y(t,g(e));var T=(t,e,n)=>new Promise((r,o)=>{var i=s=>{try{a(n.next(s));}catch(m){o(m);}},u=s=>{try{a(n.throw(s));}catch(m){o(m);}},a=s=>s.done?r(s.value):Promise.resolve(s.value).then(i,u);a((n=n.apply(t,e)).next());});function S(t,e,n){return Math.max(Math.min(t,n),e)}function A(t,e,n,r,o){return Number(e)===0&&Number(r)===0?t*(o/n):(t-e)*((o-r)/(n-e))+r}function d(...t){let e,n;if(typeof t[0]=="number"&&typeof t[1]=="number")[e,n]=t;else if(typeof t[0]=="number"&&typeof t[1]!="number")e=0,n=t[0];else throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof t[0]}" and "${typeof t[1]}"`);if(e=Number(e),n=Number(n),isNaN(e)||isNaN(n))throw new TypeError(`Parameters "min" and "max" can't be NaN`);if(e>n)throw new TypeError(`Parameter "min" can't be bigger than "max"`);return Math.floor(Math.random()*(n-e+1))+e}function H(t){return x(t)[0]}function x(t){if(t.length===0)return [void 0,void 0];let e=d(t.length-1);return [t[e],e]}function I(t){let[e,n]=x(t);if(n!==void 0)return t.splice(n,1),e}function P(t){let e=[...t];if(t.length===0)return t;for(let n=e.length-1;n>0;n--){let r=Math.floor(d(0,1e4)/1e4*(n+1));[e[n],e[r]]=[e[r],e[n]];}return e}function O(){try{return unsafeWindow}catch(t){return window}}function j(t,e){var n;return (n=t.parentNode)==null||n.insertBefore(e,t.nextSibling),e}function R(t,e){let n=t.parentNode;if(!n)throw new Error("Element doesn't have a parent node");return n.replaceChild(e,t),e.appendChild(t),e}function F(t){let e=document.createElement("style");e.innerHTML=t,document.head.appendChild(e);}function W(t,e=!1){let n=t.map(r=>new Promise((o,i)=>{let u=new Image;u.src=r,u.addEventListener("load",()=>o(u)),u.addEventListener("error",a=>e&&i(a));}));return Promise.allSettled(n)}function $(t){let e=document.createElement("a");Object.assign(e,{className:"userutils-open-in-new-tab",target:"_blank",rel:"noopener noreferrer",href:t}),e.style.display="none",document.body.appendChild(e),e.click(),setTimeout(e.remove,50);}function L(t,e,n){typeof Error.stackTraceLimit=="number"&&Error.stackTraceLimit<1e3&&(Error.stackTraceLimit=1e3),function(r){element.__proto__.addEventListener=function(...o){if(!(o[0]===e&&n()))return r.apply(this,o)};}(t.__proto__.addEventListener);}function B(t,e){return L(O(),t,e)}function q(t,e=1){let n=new(window.AudioContext||window.webkitAudioContext),r={mediaElement:t,amplify:o=>{r.gain.gain.value=o;},getAmpLevel:()=>r.gain.gain.value,context:n,source:n.createMediaElementSource(t),gain:n.createGain()};return r.source.connect(r.gain),r.gain.connect(n.destination),r.amplify(e),r}function z(t,e){return (Array.isArray(e)||e instanceof NodeList)&&(e=e.length),`${t}${e===1?"":"s"}`}function U(t){return new Promise(e=>{setTimeout(e,t);})}function D(t,e=300){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>t.apply(this,r),e);}}function J(n){return T(this,arguments,function*(t,e={}){let{timeout:r=1e4}=e,o=new AbortController,i=setTimeout(()=>o.abort(),r),u=yield fetch(t,b(c({},e),{signal:o.signal}));return clearTimeout(i),u})}var l=new Map;function V(t,e){let n=[];l.has(t)&&(n=l.get(t)),n.push(e),l.set(t,n),E(t,n);}function X(t){return l.delete(t)}function E(t,e){let n=[];if(e.forEach((r,o)=>{try{let i=r.all?document.querySelectorAll(t):document.querySelector(t);(i!==null&&i instanceof NodeList&&i.length>0||i!==null)&&(r.listener(i),r.continuous||n.push(o));}catch(i){console.error(`Couldn't call listener for selector '${t}'`,i);}}),n.length>0){let r=e.filter((o,i)=>!n.includes(i));r.length===0?l.delete(t):l.set(t,r);}}function Y(t={}){new MutationObserver(()=>{for(let[n,r]of l.entries())E(n,r);}).observe(document.body,c({subtree:!0,childList:!0},t));}function Z(){return l}
1
+ var v=Object.defineProperty,x=Object.defineProperties;var w=Object.getOwnPropertyDescriptors;var h=Object.getOwnPropertySymbols;var O=Object.prototype.hasOwnProperty,M=Object.prototype.propertyIsEnumerable;var p=(t,e,n)=>e in t?v(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,f=(t,e)=>{for(var n in e||(e={}))O.call(e,n)&&p(t,n,e[n]);if(h)for(var n of h(e))M.call(e,n)&&p(t,n,e[n]);return t},b=(t,e)=>x(t,w(e));var m=(t,e,n)=>(p(t,typeof e!="symbol"?e+"":e,n),n);var c=(t,e,n)=>new Promise((r,i)=>{var o=s=>{try{u(n.next(s));}catch(l){i(l);}},a=s=>{try{u(n.throw(s));}catch(l){i(l);}},u=s=>s.done?r(s.value):Promise.resolve(s.value).then(o,a);u((n=n.apply(t,e)).next());});function D(t,e,n){return Math.max(Math.min(t,n),e)}function L(t,e,n,r,i){return Number(e)===0&&Number(r)===0?t*(i/n):(t-e)*((i-r)/(n-e))+r}function g(...t){let e,n;if(typeof t[0]=="number"&&typeof t[1]=="number")[e,n]=t;else if(typeof t[0]=="number"&&typeof t[1]!="number")e=0,n=t[0];else throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof t[0]}" and "${typeof t[1]}"`);if(e=Number(e),n=Number(n),isNaN(e)||isNaN(n))throw new TypeError(`Parameters "min" and "max" can't be NaN`);if(e>n)throw new TypeError(`Parameter "min" can't be bigger than "max"`);return Math.floor(Math.random()*(n-e+1))+e}function V(t){return T(t)[0]}function T(t){if(t.length===0)return [void 0,void 0];let e=g(t.length-1);return [t[e],e]}function $(t){let[e,n]=T(t);if(n!==void 0)return t.splice(n,1),e}function k(t){let e=[...t];if(t.length===0)return t;for(let n=e.length-1;n>0;n--){let r=Math.floor(g(0,1e4)/1e4*(n+1));[e[n],e[r]]=[e[r],e[n]];}return e}var y=class{constructor(e){m(this,"id");m(this,"formatVersion");m(this,"defaultConfig");m(this,"cachedConfig");m(this,"migrations");this.id=e.id,this.formatVersion=e.formatVersion,this.defaultConfig=e.defaultConfig,this.cachedConfig=e.defaultConfig,this.migrations=e.migrations;}loadData(){return c(this,null,function*(){try{let e=yield GM.getValue(`_uucfg-${this.id}`,this.defaultConfig),n=Number(yield GM.getValue(`_uucfgver-${this.id}`));if(typeof e!="string")return yield this.saveDefaultData(),this.defaultConfig;isNaN(n)&&(yield GM.setValue(`_uucfgver-${this.id}`,n=this.formatVersion));let r=JSON.parse(e);return n<this.formatVersion&&this.migrations&&(r=yield this.runMigrations(r,n)),this.cachedConfig=typeof r=="object"?r:void 0}catch(e){return yield this.saveDefaultData(),this.defaultConfig}})}getData(){return this.deepCopy(this.cachedConfig)}setData(e){return this.cachedConfig=e,new Promise(n=>c(this,null,function*(){yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(e)),GM.setValue(`_uucfgver-${this.id}`,this.formatVersion)]),n();}))}saveDefaultData(){return c(this,null,function*(){return this.cachedConfig=this.defaultConfig,new Promise(e=>c(this,null,function*(){yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(this.defaultConfig)),GM.setValue(`_uucfgver-${this.id}`,this.formatVersion)]),e();}))})}deleteConfig(){return c(this,null,function*(){yield Promise.allSettled([GM.deleteValue(`_uucfg-${this.id}`),GM.deleteValue(`_uucfgver-${this.id}`)]);})}runMigrations(e,n){return c(this,null,function*(){if(!this.migrations)return e;let r=e,i=Object.entries(this.migrations).sort(([a],[u])=>Number(a)-Number(u)),o=n;for(let[a,u]of i){let s=Number(a);if(n<this.formatVersion&&n<s)try{let l=u(r);r=l instanceof Promise?yield l:l,o=n=s;}catch(l){console.error(`Error while running migration function for format version ${a}:`,l);}}return yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(r)),GM.setValue(`_uucfgver-${this.id}`,o)]),r})}deepCopy(e){return JSON.parse(JSON.stringify(e))}};function S(){try{return unsafeWindow}catch(t){return window}}function _(t,e){var n;return (n=t.parentNode)==null||n.insertBefore(e,t.nextSibling),e}function j(t,e){let n=t.parentNode;if(!n)throw new Error("Element doesn't have a parent node");return n.replaceChild(e,t),e.appendChild(t),e}function W(t){let e=document.createElement("style");e.innerHTML=t,document.head.appendChild(e);}function F(t,e=!1){let n=t.map(r=>new Promise((i,o)=>{let a=new Image;a.src=r,a.addEventListener("load",()=>i(a)),a.addEventListener("error",u=>e&&o(u));}));return Promise.allSettled(n)}function H(t){let e=document.createElement("a");Object.assign(e,{className:"userutils-open-in-new-tab",target:"_blank",rel:"noopener noreferrer",href:t}),e.style.display="none",document.body.appendChild(e),e.click(),setTimeout(e.remove,50);}function C(t,e,n){typeof Error.stackTraceLimit=="number"&&Error.stackTraceLimit<1e3&&(Error.stackTraceLimit=1e3),function(r){t.__proto__.addEventListener=function(...i){var a,u;let o=typeof i[1]=="function"?i[1]:(u=(a=i[1])==null?void 0:a.handleEvent)!=null?u:()=>{};i[1]=function(...s){if(!(i[0]===e&&n(Array.isArray(s)?s[0]:s)))return o.apply(this,s)},r.apply(this,i);};}(t.__proto__.addEventListener);}function J(t,e){return C(S(),t,e)}function q(t,e=1){let n=new(window.AudioContext||window.webkitAudioContext),r={mediaElement:t,amplify:i=>{r.gain.gain.value=i;},getAmpLevel:()=>r.gain.gain.value,context:n,source:n.createMediaElementSource(t),gain:n.createGain()};return r.source.connect(r.gain),r.gain.connect(n.destination),r.amplify(e),r}function K(t){let{overflowX:e,overflowY:n}=getComputedStyle(t);return {vertical:(n==="scroll"||n==="auto")&&t.scrollHeight>t.clientHeight,horizontal:(e==="scroll"||e==="auto")&&t.scrollWidth>t.clientWidth}}function B(t,e){return (Array.isArray(e)||e instanceof NodeList)&&(e=e.length),`${t}${e===1?"":"s"}`}function U(t){return new Promise(e=>{setTimeout(()=>e(),t);})}function X(t,e=300){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>t.apply(this,r),e);}}function Y(n){return c(this,arguments,function*(t,e={}){let{timeout:r=1e4}=e,i=new AbortController,o=setTimeout(()=>i.abort(),r),a=yield fetch(t,b(f({},e),{signal:i.signal}));return clearTimeout(o),a})}var d=new Map;function ee(t,e){let n=[];d.has(t)&&(n=d.get(t)),n.push(e),d.set(t,n),E(t,n);}function te(t){return d.delete(t)}function E(t,e){let n=[];if(e.forEach((r,i)=>{try{let o=r.all?document.querySelectorAll(t):document.querySelector(t);(o!==null&&o instanceof NodeList&&o.length>0||o!==null)&&(r.listener(o),r.continuous||n.push(i));}catch(o){console.error(`Couldn't call listener for selector '${t}'`,o);}}),n.length>0){let r=e.filter((i,o)=>!n.includes(o));r.length===0?d.delete(t):d.set(t,r);}}function ne(t={}){new MutationObserver(()=>{for(let[n,r]of d.entries())E(n,r);}).observe(document.body,f({subtree:!0,childList:!0},t));}function re(){return d}
2
2
 
3
- export { F as addGlobalStyle, R as addParent, q as amplifyMedia, z as autoPlural, S as clamp, D as debounce, J as fetchAdvanced, Z as getSelectorMap, O as getUnsafeWindow, Y as initOnSelector, j as insertAfter, L as interceptEvent, B as interceptWindowEvent, A as mapRange, V as onSelector, $ as openInNewTab, U as pauseFor, W as preloadImages, d as randRange, H as randomItem, x as randomItemIndex, P as randomizeArray, X as removeOnSelector, I as takeRandomItem };
3
+ export { y as ConfigManager, W as addGlobalStyle, j as addParent, q as amplifyMedia, B as autoPlural, D as clamp, X as debounce, Y as fetchAdvanced, re as getSelectorMap, S as getUnsafeWindow, ne as initOnSelector, _ as insertAfter, C as interceptEvent, J as interceptWindowEvent, K as isScrollable, L as mapRange, ee as onSelector, H as openInNewTab, U as pauseFor, F as preloadImages, g as randRange, V as randomItem, T as randomItemIndex, k as randomizeArray, te as removeOnSelector, $ as takeRandomItem };
@@ -0,0 +1,83 @@
1
+ /** Function that takes the data in the old format and returns the data in the new format. Also supports an asynchronous migration. */
2
+ type MigrationFunc = <TOldData = any>(oldData: TOldData) => any | Promise<any>;
3
+ /** Dictionary of format version numbers and the function that migrates to them from the previous whole integer */
4
+ type MigrationsDict = Record<number, MigrationFunc>;
5
+ /** Options for the ConfigManager instance */
6
+ export interface ConfigManagerOptions<TData> {
7
+ /** A unique internal ID for this configuration - choose wisely as changing it is not supported yet. */
8
+ id: string;
9
+ /**
10
+ * The default config data object to use if no data is saved in persistent storage yet.
11
+ * Until the data is loaded from persistent storage with `loadData()`, this will be the data returned by `getData()`
12
+ *
13
+ * ⚠️ This has to be an object that can be serialized to JSON, so no functions or circular references are allowed, they will cause unexpected behavior.
14
+ */
15
+ defaultConfig: TData;
16
+ /**
17
+ * An incremental, whole integer version number of the current format of config data.
18
+ * If the format of the data is changed, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively.
19
+ *
20
+ * ⚠️ Never decrement this number and optimally don't skip any numbers either!
21
+ */
22
+ formatVersion: number;
23
+ /**
24
+ * A dictionary of functions that can be used to migrate data from older versions of the configuration to newer ones.
25
+ * The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value.
26
+ * The values should be functions that take the data in the old format and return the data in the new format.
27
+ * The functions will be run in order from the oldest to the newest version.
28
+ * If the current format version is not in the dictionary, no migrations will be run.
29
+ */
30
+ migrations?: MigrationsDict;
31
+ }
32
+ /**
33
+ * Manages a user configuration that is cached in memory and persistently saved across sessions.
34
+ * 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.
35
+ *
36
+ * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
37
+ * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
38
+ *
39
+ * @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`
40
+ */
41
+ export declare class ConfigManager<TData = any> {
42
+ readonly id: string;
43
+ readonly formatVersion: number;
44
+ readonly defaultConfig: TData;
45
+ private cachedConfig;
46
+ private migrations?;
47
+ /**
48
+ * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
49
+ * 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.
50
+ *
51
+ * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
52
+ * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
53
+ *
54
+ * @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`
55
+ * @param options The options for this ConfigManager instance
56
+ */
57
+ constructor(options: ConfigManagerOptions<TData>);
58
+ /**
59
+ * Loads the data saved in persistent storage into the in-memory cache and also returns it.
60
+ * Automatically populates persistent storage with default data if it doesn't contain any data yet.
61
+ * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
62
+ */
63
+ loadData(): Promise<TData>;
64
+ /** 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). */
65
+ getData(): TData;
66
+ /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
67
+ setData(data: TData): Promise<void>;
68
+ /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
69
+ saveDefaultData(): Promise<void>;
70
+ /**
71
+ * Call this method to clear all persistently stored data associated with this ConfigManager instance.
72
+ * The in-memory cache will be left untouched, so you may still access the data with `getData()`.
73
+ * Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.
74
+ *
75
+ * ⚠️ This requires the additional directive `@grant GM.deleteValue`
76
+ */
77
+ deleteConfig(): Promise<void>;
78
+ /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
79
+ protected runMigrations(oldData: any, oldFmtVer: number): Promise<TData>;
80
+ /** Copies a JSON-compatible object and loses its internal references */
81
+ private deepCopy;
82
+ }
83
+ export {};
package/dist/lib/dom.d.ts CHANGED
@@ -6,12 +6,12 @@ export declare function getUnsafeWindow(): Window;
6
6
  * Inserts `afterElement` as a sibling just after the provided `beforeElement`
7
7
  * @returns Returns the `afterElement`
8
8
  */
9
- export declare function insertAfter(beforeElement: HTMLElement, afterElement: HTMLElement): HTMLElement;
9
+ export declare function insertAfter(beforeElement: Element, afterElement: Element): Element;
10
10
  /**
11
11
  * Adds a parent container around the provided element
12
12
  * @returns Returns the new parent element
13
13
  */
14
- export declare function addParent(element: HTMLElement, newParent: HTMLElement): HTMLElement;
14
+ export declare function addParent(element: Element, newParent: Element): Element;
15
15
  /**
16
16
  * Adds global CSS style in the form of a `<style>` element in the document's `<head>`
17
17
  * This needs to be run after the `DOMContentLoaded` event has fired on the document object (or instantly if `@run-at document-end` is used).
@@ -36,13 +36,13 @@ export declare function openInNewTab(href: string): void;
36
36
  * This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are added after this function is called.
37
37
  * Calling this function will set the `Error.stackTraceLimit` to 1000 to ensure the stack trace is preserved.
38
38
  */
39
- export declare function interceptEvent<TEvtObj extends EventTarget>(eventObject: TEvtObj, eventName: Parameters<TEvtObj["addEventListener"]>[0], predicate: () => boolean): void;
39
+ export declare function interceptEvent<TEvtObj extends EventTarget, TPredicateEvt extends Event>(eventObject: TEvtObj, eventName: Parameters<TEvtObj["addEventListener"]>[0], predicate: (event: TPredicateEvt) => boolean): void;
40
40
  /**
41
41
  * Intercepts the specified event on the window object and prevents it from being called if the called `predicate` function returns a truthy value.
42
42
  * This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are added after this function is called.
43
43
  * Calling this function will set the `Error.stackTraceLimit` to 1000 to ensure the stack trace is preserved.
44
44
  */
45
- export declare function interceptWindowEvent(eventName: keyof WindowEventMap, predicate: () => boolean): void;
45
+ export declare function interceptWindowEvent<TEvtKey extends keyof WindowEventMap>(eventName: TEvtKey, predicate: (event: WindowEventMap[TEvtKey]) => boolean): void;
46
46
  /**
47
47
  * Amplifies the gain of the passed media element's audio by the specified multiplier.
48
48
  * This function supports any media element like `<audio>` or `<video>`
@@ -67,3 +67,8 @@ export declare function amplifyMedia<TElem extends HTMLMediaElement>(mediaElemen
67
67
  source: MediaElementAudioSourceNode;
68
68
  gain: GainNode;
69
69
  };
70
+ /** Checks if an element is scrollable in the horizontal and vertical directions */
71
+ export declare function isScrollable(element: Element): {
72
+ vertical: boolean;
73
+ horizontal: boolean;
74
+ };
@@ -1,4 +1,5 @@
1
1
  export * from "./array";
2
+ export * from "./config";
2
3
  export * from "./dom";
3
4
  export * from "./math";
4
5
  export * from "./misc";
@@ -1,7 +1,3 @@
1
- export type FetchAdvancedOpts = RequestInit & Partial<{
2
- /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
3
- timeout: number;
4
- }>;
5
1
  /**
6
2
  * Automatically appends an `s` to the passed `word`, if `num` is not equal to 1
7
3
  * @param word A word in singular form, to auto-convert to plural
@@ -9,11 +5,16 @@ export type FetchAdvancedOpts = RequestInit & Partial<{
9
5
  */
10
6
  export declare function autoPlural(word: string, num: number | unknown[] | NodeList): string;
11
7
  /** Pauses async execution for the specified time in ms */
12
- export declare function pauseFor(time: number): Promise<unknown>;
8
+ export declare function pauseFor(time: number): Promise<void>;
13
9
  /**
14
- * Calls the passed `func` after the specified `timeout` in ms.
10
+ * Calls the passed `func` after the specified `timeout` in ms (defaults to 300).
15
11
  * Any subsequent calls to this function will reset the timer and discard previous calls.
16
12
  */
17
13
  export declare function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout?: number): (...args: TArgs[]) => void;
14
+ /** Options for the `fetchAdvanced()` function */
15
+ export type FetchAdvancedOpts = RequestInit & Partial<{
16
+ /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
17
+ timeout: number;
18
+ }>;
18
19
  /** Calls the fetch API with special options like a timeout */
19
20
  export declare function fetchAdvanced(url: string, options?: FetchAdvancedOpts): Promise<Response>;
@@ -1,17 +1,18 @@
1
+ /** Options for the `onSelector()` function */
1
2
  export type OnSelectorOpts<TElem extends Element = HTMLElement> = SelectorOptsOne<TElem> | SelectorOptsAll<TElem>;
2
- type SelectorOptsOne<TElem extends Element> = SelectorOptsBase & {
3
+ type SelectorOptsOne<TElem extends Element> = SelectorOptsCommon & {
3
4
  /** Whether to use `querySelectorAll()` instead - default is false */
4
5
  all?: false;
5
6
  /** Gets called whenever the selector was found in the DOM */
6
7
  listener: (element: TElem) => void;
7
8
  };
8
- type SelectorOptsAll<TElem extends Element> = SelectorOptsBase & {
9
+ type SelectorOptsAll<TElem extends Element> = SelectorOptsCommon & {
9
10
  /** Whether to use `querySelectorAll()` instead - default is false */
10
11
  all: true;
11
12
  /** Gets called whenever the selector was found in the DOM */
12
13
  listener: (elements: NodeListOf<TElem>) => void;
13
14
  };
14
- type SelectorOptsBase = {
15
+ type SelectorOptsCommon = {
15
16
  /** Whether to call the listener continuously instead of once - default is false */
16
17
  continuous?: boolean;
17
18
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sv443-network/userutils",
3
- "version": "0.5.3",
4
- "description": "Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, modify the DOM more easily and more ",
3
+ "version": "1.1.0",
4
+ "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",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/lib/index.d.ts",
@@ -9,10 +9,12 @@
9
9
  "lint": "tsc --noEmit && eslint .",
10
10
  "build-types": "tsc --emitDeclarationOnly --declaration --outDir dist",
11
11
  "build-common": "tsup lib/index.ts --format cjs,esm --clean --treeshake",
12
- "build-iife": "tsup lib/index.ts --format cjs,esm,iife --clean --treeshake --minify",
12
+ "build-global": "tsup lib/index.ts --format cjs,esm,iife --treeshake --onSuccess \"npm run post-build-global\"",
13
13
  "build": "npm run build-common -- --minify && npm run build-types",
14
+ "post-build-global": "npm run node-ts -- ./tools/post-build-global.mts",
14
15
  "dev": "npm run build-common -- --sourcemap --watch --onSuccess \"npm run build-types\"",
15
- "publish-package": "npm run build && changeset publish"
16
+ "publish-package": "npm run build && changeset publish",
17
+ "node-ts": "node --no-warnings=ExperimentalWarning --enable-source-maps --loader ts-node/esm"
16
18
  },
17
19
  "repository": {
18
20
  "type": "git",
@@ -30,13 +32,15 @@
30
32
  "bugs": {
31
33
  "url": "https://github.com/Sv443-Network/UserUtils/issues"
32
34
  },
33
- "homepage": "https://github.com/Sv443-Network/UserUtils#readme",
35
+ "homepage": "https://github.com/Sv443-Network/UserUtils",
34
36
  "devDependencies": {
35
37
  "@changesets/cli": "^2.26.2",
36
38
  "@types/greasemonkey": "^4.0.4",
39
+ "@types/node": "^20.5.9",
37
40
  "@typescript-eslint/eslint-plugin": "^6.2.1",
38
41
  "@typescript-eslint/parser": "^6.2.1",
39
42
  "eslint": "^8.46.0",
43
+ "ts-node": "^10.9.1",
40
44
  "tslib": "^2.6.1",
41
45
  "tsup": "^7.2.0",
42
46
  "typescript": "^5.1.6"
@@ -44,7 +48,8 @@
44
48
  "files": [
45
49
  "/dist/index.js",
46
50
  "/dist/index.mjs",
47
- "/dist/lib/**.*",
51
+ "/dist/index.global.js",
52
+ "/dist/lib/**.d.ts",
48
53
  "/package.json",
49
54
  "/README.md",
50
55
  "/CHANGELOG.md",