@sv443-network/userutils 0.5.3 → 1.0.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 +11 -0
- package/README.md +270 -59
- package/dist/index.js +26 -25
- package/dist/index.mjs +2 -2
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/dom.d.ts +2 -2
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/misc.d.ts +7 -6
- package/dist/lib/onSelector.d.ts +1 -0
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @sv443-network/userutils
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- a500a98: Added ConfigManager to manage persistent user configurations including data versioning and migration
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 6d0a700: Event interceptor can now be toggled at runtime ([#16](https://github.com/Sv443-Network/UserUtils/issues/16))
|
|
12
|
+
- d038b21: Global (IIFE) build now comes with a header
|
|
13
|
+
|
|
3
14
|
## 0.5.3
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
Contains builtin TypeScript declarations. Webpack compatible and supports ESM and CJS.
|
|
6
6
|
If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
|
|
7
7
|
|
|
@@ -31,6 +31,7 @@ If you like using this library, please consider [supporting the development ❤
|
|
|
31
31
|
- [mapRange()](#maprange) - map a number from one range to the same spot in another range
|
|
32
32
|
- [randRange()](#randrange) - generate a random number between a min and max boundary
|
|
33
33
|
- [Misc:](#misc)
|
|
34
|
+
- [ConfigManager()](#configmanager) - class that manages persistent userscript configurations, including data migration
|
|
34
35
|
- [autoPlural()](#autoplural) - automatically pluralize a string
|
|
35
36
|
- [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time
|
|
36
37
|
- [debounce()](#debounce) - call a function only once, after a given amount of time
|
|
@@ -51,10 +52,12 @@ If you like using this library, please consider [supporting the development ❤
|
|
|
51
52
|
Then, import it in your script as usual:
|
|
52
53
|
```ts
|
|
53
54
|
import { addGlobalStyle } from "@sv443-network/userutils";
|
|
54
|
-
|
|
55
|
-
import
|
|
55
|
+
|
|
56
|
+
// or just import everything (not recommended because this doesn't allow for treeshaking):
|
|
57
|
+
|
|
58
|
+
import * as UserUtils from "@sv443-network/userutils";
|
|
56
59
|
```
|
|
57
|
-
Shameless plug: I
|
|
60
|
+
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
61
|
|
|
59
62
|
<br>
|
|
60
63
|
|
|
@@ -79,10 +82,12 @@ If you like using this library, please consider [supporting the development ❤
|
|
|
79
82
|
This library is written in TypeScript and contains builtin TypeScript declarations.
|
|
80
83
|
The usages and examples in this readme are written in TypeScript, but the library can also be used in plain JavaScript.
|
|
81
84
|
|
|
82
|
-
Some
|
|
85
|
+
Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
|
|
83
86
|
Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
Each feature has example code that can be expanded by clicking on the text "Example - click to view".
|
|
89
|
+
|
|
90
|
+
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.
|
|
86
91
|
|
|
87
92
|
<br><br>
|
|
88
93
|
|
|
@@ -124,7 +129,7 @@ This initialization function has to be called after `DOMContentLoaded` is fired
|
|
|
124
129
|
|
|
125
130
|
Calling onSelector() before `DOMContentLoaded` is fired will not throw an error, but it also won't trigger listeners until the DOM is accessible.
|
|
126
131
|
|
|
127
|
-
<details><summary><
|
|
132
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
128
133
|
|
|
129
134
|
```ts
|
|
130
135
|
document.addEventListener("DOMContentLoaded", initOnSelector);
|
|
@@ -155,7 +160,7 @@ Usage:
|
|
|
155
160
|
```ts
|
|
156
161
|
initOnSelector(options?: MutationObserverInit): void
|
|
157
162
|
```
|
|
158
|
-
|
|
163
|
+
|
|
159
164
|
Initializes the MutationObserver that is used by [`onSelector()`](#onselector) to check for the registered selectors whenever a DOM change occurs on the `<body>`
|
|
160
165
|
By default, this only checks if elements are added or removed (at any depth).
|
|
161
166
|
|
|
@@ -169,7 +174,7 @@ You may see all options [here](https://developer.mozilla.org/en-US/docs/Web/API/
|
|
|
169
174
|
>
|
|
170
175
|
> ⚠️ Using these extra options can have a performance impact on larger sites or sites with a constantly changing DOM.
|
|
171
176
|
|
|
172
|
-
<details><summary><
|
|
177
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
173
178
|
|
|
174
179
|
```ts
|
|
175
180
|
document.addEventListener("DOMContentLoaded", () => {
|
|
@@ -185,12 +190,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
185
190
|
<br>
|
|
186
191
|
|
|
187
192
|
### getSelectorMap()
|
|
188
|
-
Usage:
|
|
193
|
+
Usage:
|
|
194
|
+
```ts
|
|
195
|
+
getSelectorMap(): Map<string, OnSelectorOptions[]>
|
|
196
|
+
```
|
|
189
197
|
|
|
190
198
|
Returns a Map of all currently registered selectors and their options, including listener function.
|
|
191
199
|
Since multiple listeners can be registered for the same selector, the value of the Map is an array of `OnSelectorOptions` objects.
|
|
192
200
|
|
|
193
|
-
<details><summary><
|
|
201
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
194
202
|
|
|
195
203
|
```ts
|
|
196
204
|
document.addEventListener("DOMContentLoaded", initOnSelector);
|
|
@@ -225,12 +233,15 @@ const selectorMap = getSelectorMap();
|
|
|
225
233
|
<br>
|
|
226
234
|
|
|
227
235
|
### getUnsafeWindow()
|
|
228
|
-
Usage:
|
|
236
|
+
Usage:
|
|
237
|
+
```ts
|
|
238
|
+
getUnsafeWindow(): Window
|
|
239
|
+
```
|
|
229
240
|
|
|
230
241
|
Returns the unsafeWindow object or falls back to the regular window object if the `@grant unsafeWindow` is not given.
|
|
231
242
|
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
243
|
|
|
233
|
-
<details><summary><
|
|
244
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
234
245
|
|
|
235
246
|
```ts
|
|
236
247
|
// trick the site into thinking the mouse was moved:
|
|
@@ -249,14 +260,17 @@ document.body.dispatchEvent(mouseEvent);
|
|
|
249
260
|
<br>
|
|
250
261
|
|
|
251
262
|
### insertAfter()
|
|
252
|
-
Usage:
|
|
263
|
+
Usage:
|
|
264
|
+
```ts
|
|
265
|
+
insertAfter(beforeElement: HTMLElement, afterElement: HTMLElement): HTMLElement
|
|
266
|
+
```
|
|
253
267
|
|
|
254
268
|
Inserts the element passed as `afterElement` as a sibling after the passed `beforeElement`.
|
|
255
269
|
The passed `afterElement` will be returned.
|
|
256
270
|
|
|
257
271
|
⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
|
|
258
272
|
|
|
259
|
-
<details><summary><
|
|
273
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
260
274
|
|
|
261
275
|
```ts
|
|
262
276
|
// insert a <div> as a sibling next to an element
|
|
@@ -271,14 +285,17 @@ insertAfter(beforeElement, afterElement);
|
|
|
271
285
|
<br>
|
|
272
286
|
|
|
273
287
|
### addParent()
|
|
274
|
-
Usage:
|
|
288
|
+
Usage:
|
|
289
|
+
```ts
|
|
290
|
+
addParent(element: HTMLElement, newParent: HTMLElement): HTMLElement
|
|
291
|
+
```
|
|
275
292
|
|
|
276
293
|
Adds a parent element around the passed `element` and returns the new parent.
|
|
277
294
|
Previously registered event listeners are kept intact.
|
|
278
295
|
|
|
279
296
|
⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
|
|
280
297
|
|
|
281
|
-
<details><summary><
|
|
298
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
282
299
|
|
|
283
300
|
```ts
|
|
284
301
|
// add an <a> around an element
|
|
@@ -293,12 +310,15 @@ addParent(element, newParent);
|
|
|
293
310
|
<br>
|
|
294
311
|
|
|
295
312
|
### addGlobalStyle()
|
|
296
|
-
Usage:
|
|
313
|
+
Usage:
|
|
314
|
+
```ts
|
|
315
|
+
addGlobalStyle(css: string): void
|
|
316
|
+
```
|
|
297
317
|
|
|
298
318
|
Adds a global style to the page in form of a `<style>` element that's inserted into the `<head>`.
|
|
299
319
|
⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
|
|
300
320
|
|
|
301
|
-
<details><summary><
|
|
321
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
302
322
|
|
|
303
323
|
```ts
|
|
304
324
|
document.addEventListener("DOMContentLoaded", () => {
|
|
@@ -315,13 +335,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
315
335
|
<br>
|
|
316
336
|
|
|
317
337
|
### preloadImages()
|
|
318
|
-
Usage:
|
|
338
|
+
Usage:
|
|
339
|
+
```ts
|
|
340
|
+
preloadImages(urls: string[], rejects?: boolean): Promise<void>
|
|
341
|
+
```
|
|
319
342
|
|
|
320
343
|
Preloads images into browser cache by creating an invisible `<img>` element for each URL passed.
|
|
321
344
|
The images will be loaded in parallel and the returned Promise will only resolve once all images have been loaded.
|
|
322
345
|
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
346
|
|
|
324
|
-
<details><summary><
|
|
347
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
325
348
|
|
|
326
349
|
```ts
|
|
327
350
|
preloadImages([
|
|
@@ -339,7 +362,10 @@ preloadImages([
|
|
|
339
362
|
<br>
|
|
340
363
|
|
|
341
364
|
### openInNewTab()
|
|
342
|
-
Usage:
|
|
365
|
+
Usage:
|
|
366
|
+
```ts
|
|
367
|
+
openInNewTab(url: string): void
|
|
368
|
+
```
|
|
343
369
|
|
|
344
370
|
Creates an invisible anchor with a `_blank` target and clicks it.
|
|
345
371
|
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,7 +373,7 @@ This function has to be run in response to a user interaction event, else the br
|
|
|
347
373
|
|
|
348
374
|
⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
|
|
349
375
|
|
|
350
|
-
<details><summary><
|
|
376
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
351
377
|
|
|
352
378
|
```ts
|
|
353
379
|
document.querySelector("#my-button").addEventListener("click", () => {
|
|
@@ -360,14 +386,21 @@ document.querySelector("#my-button").addEventListener("click", () => {
|
|
|
360
386
|
<br>
|
|
361
387
|
|
|
362
388
|
### interceptEvent()
|
|
363
|
-
Usage:
|
|
389
|
+
Usage:
|
|
390
|
+
```ts
|
|
391
|
+
interceptEvent(
|
|
392
|
+
eventObject: EventTarget,
|
|
393
|
+
eventName: string,
|
|
394
|
+
predicate: () => boolean
|
|
395
|
+
): void
|
|
396
|
+
```
|
|
364
397
|
|
|
365
398
|
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
399
|
Calling this function will set the `Error.stackTraceLimit` to 1000 (if it's not already higher) to ensure the stack trace is preserved.
|
|
367
400
|
|
|
368
401
|
⚠️ 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
402
|
|
|
370
|
-
<details><summary><
|
|
403
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
371
404
|
|
|
372
405
|
```ts
|
|
373
406
|
interceptEvent(document.body, "click", () => {
|
|
@@ -380,14 +413,20 @@ interceptEvent(document.body, "click", () => {
|
|
|
380
413
|
<br>
|
|
381
414
|
|
|
382
415
|
### interceptWindowEvent()
|
|
383
|
-
Usage:
|
|
416
|
+
Usage:
|
|
417
|
+
```ts
|
|
418
|
+
interceptWindowEvent(
|
|
419
|
+
eventName: string,
|
|
420
|
+
predicate: () => boolean
|
|
421
|
+
): void
|
|
422
|
+
```
|
|
384
423
|
|
|
385
424
|
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
425
|
This is essentially the same as [`interceptEvent()`](#interceptevent), but automatically uses the `unsafeWindow` (or falls back to regular `window`).
|
|
387
426
|
|
|
388
427
|
⚠️ 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.
|
|
389
428
|
|
|
390
|
-
<details><summary><
|
|
429
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
391
430
|
|
|
392
431
|
```ts
|
|
393
432
|
interceptWindowEvent("beforeunload", () => {
|
|
@@ -400,7 +439,10 @@ interceptWindowEvent("beforeunload", () => {
|
|
|
400
439
|
<br>
|
|
401
440
|
|
|
402
441
|
### amplifyMedia()
|
|
403
|
-
Usage:
|
|
442
|
+
Usage:
|
|
443
|
+
```ts
|
|
444
|
+
amplifyMedia(mediaElement: HTMLMediaElement, multiplier?: number): AmplifyMediaResult
|
|
445
|
+
```
|
|
404
446
|
|
|
405
447
|
Amplifies the gain of a media element (like `<audio>` or `<video>`) by a given multiplier (defaults to 1.0).
|
|
406
448
|
This is how you can increase the volume of a media element beyond the default maximum volume of 1.0 or 100%.
|
|
@@ -408,7 +450,7 @@ Make sure to limit the multiplier to a reasonable value ([clamp()](#clamp) is go
|
|
|
408
450
|
|
|
409
451
|
⚠️ 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
452
|
|
|
411
|
-
|
|
453
|
+
The returned AmplifyMediaResult object has the following properties:
|
|
412
454
|
| Property | Description |
|
|
413
455
|
| :-- | :-- |
|
|
414
456
|
| `mediaElement` | The passed media element |
|
|
@@ -418,7 +460,7 @@ Returns an object with the following properties:
|
|
|
418
460
|
| `source` | The MediaElementSourceNode instance |
|
|
419
461
|
| `gain` | The GainNode instance |
|
|
420
462
|
|
|
421
|
-
<details><summary><
|
|
463
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
422
464
|
|
|
423
465
|
```ts
|
|
424
466
|
const audio = document.querySelector<HTMLAudioElement>("audio");
|
|
@@ -448,17 +490,24 @@ button.addEventListener("click", () => {
|
|
|
448
490
|
## Math:
|
|
449
491
|
|
|
450
492
|
### clamp()
|
|
451
|
-
Usage:
|
|
493
|
+
Usage:
|
|
494
|
+
```ts
|
|
495
|
+
clamp(num: number, min: number, max: number): number
|
|
496
|
+
```
|
|
452
497
|
|
|
453
|
-
Clamps a number between a min and max
|
|
498
|
+
Clamps a number between a min and max boundary (inclusive).
|
|
454
499
|
|
|
455
|
-
<details><summary><
|
|
500
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
456
501
|
|
|
457
502
|
```ts
|
|
458
|
-
clamp(
|
|
459
|
-
clamp(-1, 0, 10);
|
|
460
|
-
clamp(
|
|
461
|
-
clamp(
|
|
503
|
+
clamp(7, 0, 10); // 7
|
|
504
|
+
clamp(-1, 0, 10); // 0
|
|
505
|
+
clamp(5, -5, 0); // 0
|
|
506
|
+
clamp(99999, 0, 10); // 10
|
|
507
|
+
|
|
508
|
+
// clamp without a min or max boundary:
|
|
509
|
+
clamp(-99999, -Infinity, 0); // -99999
|
|
510
|
+
clamp(99999, 0, Infinity); // 99999
|
|
462
511
|
```
|
|
463
512
|
|
|
464
513
|
</details>
|
|
@@ -466,16 +515,27 @@ clamp(Infinity, 0, 10); // 10
|
|
|
466
515
|
<br>
|
|
467
516
|
|
|
468
517
|
### mapRange()
|
|
469
|
-
Usage:
|
|
518
|
+
Usage:
|
|
519
|
+
```ts
|
|
520
|
+
mapRange(
|
|
521
|
+
value: number,
|
|
522
|
+
range_1_min: number,
|
|
523
|
+
range_1_max: number,
|
|
524
|
+
range_2_min: number,
|
|
525
|
+
range_2_max: number
|
|
526
|
+
): number
|
|
527
|
+
```
|
|
470
528
|
|
|
471
529
|
Maps a number from one range to the spot it would be in another range.
|
|
472
530
|
|
|
473
|
-
<details><summary><
|
|
531
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
474
532
|
|
|
475
533
|
```ts
|
|
476
534
|
mapRange(5, 0, 10, 0, 100); // 50
|
|
477
535
|
mapRange(5, 0, 10, 0, 50); // 25
|
|
478
|
-
|
|
536
|
+
|
|
537
|
+
// to calculate a percentage from arbitrary values, use 0 and 100 as the second range
|
|
538
|
+
// for example, if 4 files of a total of 13 were downloaded:
|
|
479
539
|
mapRange(4, 0, 13, 0, 100); // 30.76923076923077
|
|
480
540
|
```
|
|
481
541
|
|
|
@@ -493,7 +553,7 @@ randRange(max: number): number
|
|
|
493
553
|
Returns a random number between `min` and `max` (inclusive).
|
|
494
554
|
If only one argument is passed, it will be used as the `max` value and `min` will be set to 0.
|
|
495
555
|
|
|
496
|
-
<details><summary><
|
|
556
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
497
557
|
|
|
498
558
|
```ts
|
|
499
559
|
randRange(0, 10); // 4
|
|
@@ -507,13 +567,145 @@ randRange(10); // 7
|
|
|
507
567
|
|
|
508
568
|
## Misc:
|
|
509
569
|
|
|
570
|
+
### ConfigManager()
|
|
571
|
+
Usage:
|
|
572
|
+
```ts
|
|
573
|
+
new ConfigManager(options: ConfigManagerOptions)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
A class that manages a userscript's configuration that is persistently saved to and loaded from GM storage.
|
|
577
|
+
Also supports automatic migration of outdated data formats via provided migration functions.
|
|
578
|
+
|
|
579
|
+
⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used.
|
|
580
|
+
⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required for this to work.
|
|
581
|
+
|
|
582
|
+
The options object has the following properties:
|
|
583
|
+
| Property | Description |
|
|
584
|
+
| :-- | :-- |
|
|
585
|
+
| `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. |
|
|
586
|
+
| `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. |
|
|
587
|
+
| `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. |
|
|
588
|
+
| `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. |
|
|
589
|
+
|
|
590
|
+
<br>
|
|
591
|
+
|
|
592
|
+
### Methods:
|
|
593
|
+
`loadData(): Promise<TData>`
|
|
594
|
+
Asynchronously loads the configuration data from persistent storage and returns it.
|
|
595
|
+
If no data was saved in persistent storage before, the value of `options.defaultConfig` will be returned and written to persistent storage.
|
|
596
|
+
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.
|
|
597
|
+
|
|
598
|
+
`getData(): TData`
|
|
599
|
+
Synchronously returns the current data that is stored in the internal cache.
|
|
600
|
+
If no data was loaded from persistent storage yet using `loadData()`, the value of `options.defaultConfig` will be returned.
|
|
601
|
+
|
|
602
|
+
`setData(data: TData): Promise<void>`
|
|
603
|
+
Writes the given data synchronously to the internal cache and asynchronously to persistent storage.
|
|
604
|
+
|
|
605
|
+
`saveDefaultData(): Promise<void>`
|
|
606
|
+
Writes the default configuration given in `options.defaultConfig` synchronously to the internal cache and asynchronously to persistent storage.
|
|
607
|
+
|
|
608
|
+
`deleteConfig(): Promise<void>`
|
|
609
|
+
Fully deletes the configuration from persistent storage.
|
|
610
|
+
The internal cache will be left untouched, so any subsequent calls to `getData()` will return the data that was last loaded.
|
|
611
|
+
If `loadData()` or `setData()` are called after this, the persistent storage will be populated again.
|
|
612
|
+
|
|
613
|
+
<br>
|
|
614
|
+
|
|
615
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
import { ConfigManager } from "@sv443-network/userutils";
|
|
619
|
+
|
|
620
|
+
interface MyConfig {
|
|
621
|
+
foo: string;
|
|
622
|
+
bar: number;
|
|
623
|
+
baz: string;
|
|
624
|
+
qux: string;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Default config data */
|
|
628
|
+
const defaultConfig: MyConfig = {
|
|
629
|
+
foo: "hello",
|
|
630
|
+
bar: 42,
|
|
631
|
+
baz: "xyz",
|
|
632
|
+
qux: "something",
|
|
633
|
+
};
|
|
634
|
+
/** If any properties are added to, removed from or renamed in MyConfig, increment this number */
|
|
635
|
+
const formatVersion = 2;
|
|
636
|
+
/** 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! */
|
|
637
|
+
const migrations = {
|
|
638
|
+
// migrate from format version 0 to 1
|
|
639
|
+
1: (oldData: any) => {
|
|
640
|
+
return {
|
|
641
|
+
foo: oldData.foo,
|
|
642
|
+
bar: oldData.bar,
|
|
643
|
+
baz: "world",
|
|
644
|
+
};
|
|
645
|
+
},
|
|
646
|
+
// asynchronously migrate from format version 1 to 2
|
|
647
|
+
2: async (oldData: any) => {
|
|
648
|
+
// arbitrary async operation required for the new format
|
|
649
|
+
const qux = JSON.parse(await (await fetch("https://api.example.org/some-data")).text());
|
|
650
|
+
return {
|
|
651
|
+
foo: oldData.foo,
|
|
652
|
+
bar: oldData.bar,
|
|
653
|
+
baz: oldData.baz,
|
|
654
|
+
qux,
|
|
655
|
+
};
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const configMgr = new ConfigManager({
|
|
660
|
+
/** A unique ID for this configuration - choose wisely as changing it is not supported yet! */
|
|
661
|
+
id: "my-userscript",
|
|
662
|
+
/** Default / fallback configuration data */
|
|
663
|
+
defaultConfig,
|
|
664
|
+
/** The current version of the script's config data format */
|
|
665
|
+
formatVersion,
|
|
666
|
+
/** Data format migration functions */
|
|
667
|
+
migrations,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
/** Entrypoint of the userscript */
|
|
671
|
+
async function init() {
|
|
672
|
+
// wait for the config to be loaded from persistent storage
|
|
673
|
+
// if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultConfig will be returned
|
|
674
|
+
// if the previously saved data needs to be migrated to a newer version, it will happen in this function call
|
|
675
|
+
const configData = await configMgr.loadData();
|
|
676
|
+
|
|
677
|
+
console.log(configData.foo); // "hello"
|
|
678
|
+
|
|
679
|
+
// update the config
|
|
680
|
+
configData.foo = "world";
|
|
681
|
+
configData.bar = 123;
|
|
682
|
+
|
|
683
|
+
// save the updated config - synchronously to the cache and asynchronously to persistent storage
|
|
684
|
+
configMgr.saveData(configData).then(() => {
|
|
685
|
+
console.log("Config saved to persistent storage!");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// the internal cache is updated synchronously, so the updated data can be accessed before the Promise resolves:
|
|
689
|
+
console.log(configMgr.getData().foo); // "world"
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
init();
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
</details>
|
|
696
|
+
|
|
697
|
+
<br><br>
|
|
698
|
+
|
|
510
699
|
### autoPlural()
|
|
511
|
-
Usage:
|
|
700
|
+
Usage:
|
|
701
|
+
```ts
|
|
702
|
+
autoPlural(str: string, num: number | Array | NodeList): string
|
|
703
|
+
```
|
|
512
704
|
|
|
513
705
|
Automatically pluralizes a string if the given number is not 1.
|
|
514
|
-
If an array or NodeList is passed, the
|
|
706
|
+
If an array or NodeList is passed, the amount of contained items will be used.
|
|
515
707
|
|
|
516
|
-
<details><summary><
|
|
708
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
517
709
|
|
|
518
710
|
```ts
|
|
519
711
|
autoPlural("apple", 0); // "apples"
|
|
@@ -532,11 +724,14 @@ console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 i
|
|
|
532
724
|
<br>
|
|
533
725
|
|
|
534
726
|
### pauseFor()
|
|
535
|
-
Usage:
|
|
727
|
+
Usage:
|
|
728
|
+
```ts
|
|
729
|
+
pauseFor(ms: number): Promise<void>
|
|
730
|
+
```
|
|
536
731
|
|
|
537
732
|
Pauses async execution for a given amount of time.
|
|
538
733
|
|
|
539
|
-
<details><summary><
|
|
734
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
540
735
|
|
|
541
736
|
```ts
|
|
542
737
|
async function run() {
|
|
@@ -551,13 +746,17 @@ async function run() {
|
|
|
551
746
|
<br>
|
|
552
747
|
|
|
553
748
|
### debounce()
|
|
554
|
-
Usage:
|
|
749
|
+
Usage:
|
|
750
|
+
```ts
|
|
751
|
+
debounce(func: Function, timeout?: number): Function
|
|
752
|
+
```
|
|
555
753
|
|
|
556
754
|
Debounces a function, meaning that it will only be called once after a given amount of time.
|
|
557
755
|
This is very useful for functions that are called repeatedly, like event listeners, to remove extraneous calls.
|
|
756
|
+
All passed properties will be passed down to the debounced function.
|
|
558
757
|
The timeout will default to 300ms if left undefined.
|
|
559
758
|
|
|
560
|
-
<details><summary><
|
|
759
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
561
760
|
|
|
562
761
|
```ts
|
|
563
762
|
window.addEventListener("resize", debounce((event) => {
|
|
@@ -581,7 +780,7 @@ fetchAdvanced(url: string, options?: {
|
|
|
581
780
|
A wrapper around the native `fetch()` function that adds options like a timeout property.
|
|
582
781
|
The timeout will default to 10 seconds if left undefined.
|
|
583
782
|
|
|
584
|
-
<details><summary><
|
|
783
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
585
784
|
|
|
586
785
|
```ts
|
|
587
786
|
fetchAdvanced("https://api.example.org/data", {
|
|
@@ -602,12 +801,15 @@ fetchAdvanced("https://api.example.org/data", {
|
|
|
602
801
|
## Arrays:
|
|
603
802
|
|
|
604
803
|
### randomItem()
|
|
605
|
-
Usage:
|
|
804
|
+
Usage:
|
|
805
|
+
```ts
|
|
806
|
+
randomItem(array: Array): any
|
|
807
|
+
```
|
|
606
808
|
|
|
607
809
|
Returns a random item from an array.
|
|
608
810
|
Returns undefined if the array is empty.
|
|
609
811
|
|
|
610
|
-
<details><summary><
|
|
812
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
611
813
|
|
|
612
814
|
```ts
|
|
613
815
|
randomItem(["foo", "bar", "baz"]); // "bar"
|
|
@@ -619,12 +821,15 @@ randomItem([ ]); // undefined
|
|
|
619
821
|
<br>
|
|
620
822
|
|
|
621
823
|
### randomItemIndex()
|
|
622
|
-
Usage:
|
|
824
|
+
Usage:
|
|
825
|
+
```ts
|
|
826
|
+
randomItemIndex(array: Array): [item: any, index: number]
|
|
827
|
+
```
|
|
623
828
|
|
|
624
829
|
Returns a tuple of a random item and its index from an array.
|
|
625
830
|
If the array is empty, it will return undefined for both values.
|
|
626
831
|
|
|
627
|
-
<details><summary><
|
|
832
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
628
833
|
|
|
629
834
|
```ts
|
|
630
835
|
randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
|
|
@@ -640,12 +845,15 @@ const [, index] = randomItemIndex(["foo", "bar", "baz"]);
|
|
|
640
845
|
<br>
|
|
641
846
|
|
|
642
847
|
### takeRandomItem()
|
|
643
|
-
Usage:
|
|
848
|
+
Usage:
|
|
849
|
+
```ts
|
|
850
|
+
takeRandomItem(array: Array): any
|
|
851
|
+
```
|
|
644
852
|
|
|
645
853
|
Returns a random item from an array and mutates the array by removing the item.
|
|
646
854
|
Returns undefined if the array is empty.
|
|
647
855
|
|
|
648
|
-
<details><summary><
|
|
856
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
649
857
|
|
|
650
858
|
```ts
|
|
651
859
|
const arr = ["foo", "bar", "baz"];
|
|
@@ -658,12 +866,15 @@ console.log(arr); // ["foo", "baz"]
|
|
|
658
866
|
<br>
|
|
659
867
|
|
|
660
868
|
### randomizeArray()
|
|
661
|
-
Usage:
|
|
869
|
+
Usage:
|
|
870
|
+
```ts
|
|
871
|
+
randomizeArray(array: Array): Array
|
|
872
|
+
```
|
|
662
873
|
|
|
663
|
-
Returns a copy of
|
|
664
|
-
If the array is empty, the originally passed array will be returned.
|
|
874
|
+
Returns a copy of an array with its items in a random order.
|
|
875
|
+
If the array is empty, the originally passed empty array will be returned without copying.
|
|
665
876
|
|
|
666
|
-
<details><summary><
|
|
877
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
667
878
|
|
|
668
879
|
```ts
|
|
669
880
|
randomizeArray([1, 2, 3, 4, 5, 6]); // [3, 1, 5, 2, 4, 6]
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var v=Object.defineProperty,x=Object.defineProperties;var w=Object.getOwnPropertyDescriptors;var h=Object.getOwnPropertySymbols;var M=Object.prototype.hasOwnProperty,O=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={}))M.call(e,n)&&p(t,n,e[n]);if(h)for(var n of h(e))O.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 l=(t,e,n)=>new Promise((r,i)=>{var o=s=>{try{u(n.next(s));}catch(c){i(c);}},a=s=>{try{u(n.throw(s));}catch(c){i(c);}},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 l(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=>l(this,null,function*(){yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(e)),GM.setValue(`_uucfgver-${this.id}`,this.formatVersion)]),n();}))}saveDefaultData(){return l(this,null,function*(){return this.cachedConfig=this.defaultConfig,new Promise(e=>l(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 l(this,null,function*(){yield Promise.allSettled([GM.deleteValue(`_uucfg-${this.id}`),GM.deleteValue(`_uucfgver-${this.id}`)]);})}runMigrations(e,n){return l(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 c=u(r);r=c instanceof Promise?yield c:c,o=n=s;}catch(c){console.error(`Error while running migration function for format version ${a}:`,c);}}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 R(t,e){var n;return (n=t.parentNode)==null||n.insertBefore(e,t.nextSibling),e}function _(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 j(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 J(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 N(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 W(t,e){return N(S(),t,e)}function B(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,e){return (Array.isArray(e)||e instanceof NodeList)&&(e=e.length),`${t}${e===1?"":"s"}`}function z(t){return new Promise(e=>{setTimeout(()=>e(),t);})}function U(t,e=300){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>t.apply(this,r),e);}}function Q(n){return l(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 Z(t,e){let n=[];d.has(t)&&(n=d.get(t)),n.push(e),d.set(t,n),E(t,n);}function ee(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 te(t={}){new MutationObserver(()=>{for(let[n,r]of d.entries())E(n,r);}).observe(document.body,f({subtree:!0,childList:!0},t));}function ne(){return d}
|
|
4
4
|
|
|
5
|
-
exports.
|
|
6
|
-
exports.
|
|
7
|
-
exports.
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
12
|
-
exports.
|
|
13
|
-
exports.
|
|
14
|
-
exports.
|
|
15
|
-
exports.
|
|
16
|
-
exports.
|
|
17
|
-
exports.
|
|
18
|
-
exports.
|
|
19
|
-
exports.
|
|
20
|
-
exports.
|
|
21
|
-
exports.
|
|
22
|
-
exports.
|
|
23
|
-
exports.
|
|
24
|
-
exports.
|
|
25
|
-
exports.
|
|
26
|
-
exports.
|
|
27
|
-
exports.
|
|
28
|
-
exports.
|
|
5
|
+
exports.ConfigManager = y;
|
|
6
|
+
exports.addGlobalStyle = j;
|
|
7
|
+
exports.addParent = _;
|
|
8
|
+
exports.amplifyMedia = B;
|
|
9
|
+
exports.autoPlural = K;
|
|
10
|
+
exports.clamp = D;
|
|
11
|
+
exports.debounce = U;
|
|
12
|
+
exports.fetchAdvanced = Q;
|
|
13
|
+
exports.getSelectorMap = ne;
|
|
14
|
+
exports.getUnsafeWindow = S;
|
|
15
|
+
exports.initOnSelector = te;
|
|
16
|
+
exports.insertAfter = R;
|
|
17
|
+
exports.interceptEvent = N;
|
|
18
|
+
exports.interceptWindowEvent = W;
|
|
19
|
+
exports.mapRange = L;
|
|
20
|
+
exports.onSelector = Z;
|
|
21
|
+
exports.openInNewTab = J;
|
|
22
|
+
exports.pauseFor = z;
|
|
23
|
+
exports.preloadImages = F;
|
|
24
|
+
exports.randRange = g;
|
|
25
|
+
exports.randomItem = V;
|
|
26
|
+
exports.randomItemIndex = T;
|
|
27
|
+
exports.randomizeArray = k;
|
|
28
|
+
exports.removeOnSelector = ee;
|
|
29
|
+
exports.takeRandomItem = $;
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var v=Object.defineProperty,x=Object.defineProperties;var w=Object.getOwnPropertyDescriptors;var h=Object.getOwnPropertySymbols;var M=Object.prototype.hasOwnProperty,O=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={}))M.call(e,n)&&p(t,n,e[n]);if(h)for(var n of h(e))O.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 l=(t,e,n)=>new Promise((r,i)=>{var o=s=>{try{u(n.next(s));}catch(c){i(c);}},a=s=>{try{u(n.throw(s));}catch(c){i(c);}},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 l(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=>l(this,null,function*(){yield Promise.allSettled([GM.setValue(`_uucfg-${this.id}`,JSON.stringify(e)),GM.setValue(`_uucfgver-${this.id}`,this.formatVersion)]),n();}))}saveDefaultData(){return l(this,null,function*(){return this.cachedConfig=this.defaultConfig,new Promise(e=>l(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 l(this,null,function*(){yield Promise.allSettled([GM.deleteValue(`_uucfg-${this.id}`),GM.deleteValue(`_uucfgver-${this.id}`)]);})}runMigrations(e,n){return l(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 c=u(r);r=c instanceof Promise?yield c:c,o=n=s;}catch(c){console.error(`Error while running migration function for format version ${a}:`,c);}}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 R(t,e){var n;return (n=t.parentNode)==null||n.insertBefore(e,t.nextSibling),e}function _(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 j(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 J(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 N(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 W(t,e){return N(S(),t,e)}function B(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,e){return (Array.isArray(e)||e instanceof NodeList)&&(e=e.length),`${t}${e===1?"":"s"}`}function z(t){return new Promise(e=>{setTimeout(()=>e(),t);})}function U(t,e=300){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>t.apply(this,r),e);}}function Q(n){return l(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 Z(t,e){let n=[];d.has(t)&&(n=d.get(t)),n.push(e),d.set(t,n),E(t,n);}function ee(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 te(t={}){new MutationObserver(()=>{for(let[n,r]of d.entries())E(n,r);}).observe(document.body,f({subtree:!0,childList:!0},t));}function ne(){return d}
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { y as ConfigManager, j as addGlobalStyle, _ as addParent, B as amplifyMedia, K as autoPlural, D as clamp, U as debounce, Q as fetchAdvanced, ne as getSelectorMap, S as getUnsafeWindow, te as initOnSelector, R as insertAfter, N as interceptEvent, W as interceptWindowEvent, L as mapRange, Z as onSelector, J as openInNewTab, z as pauseFor, F as preloadImages, g as randRange, V as randomItem, T as randomItemIndex, k as randomizeArray, ee 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
|
@@ -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:
|
|
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>`
|
package/dist/lib/index.d.ts
CHANGED
package/dist/lib/misc.d.ts
CHANGED
|
@@ -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<
|
|
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>;
|
package/dist/lib/onSelector.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Options for the `onSelector()` function */
|
|
1
2
|
export type OnSelectorOpts<TElem extends Element = HTMLElement> = SelectorOptsOne<TElem> | SelectorOptsAll<TElem>;
|
|
2
3
|
type SelectorOptsOne<TElem extends Element> = SelectorOptsBase & {
|
|
3
4
|
/** Whether to use `querySelectorAll()` instead - default is false */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sv443-network/userutils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, modify the DOM more easily and more ",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -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-
|
|
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
|
|
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"
|