@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 +21 -0
- package/README.md +363 -67
- package/dist/index.js +25 -23
- package/dist/index.mjs +2 -2
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/dom.d.ts +9 -4
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/misc.d.ts +7 -6
- package/dist/lib/onSelector.d.ts +4 -3
- package/package.json +11 -6
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
|
-
|
|
55
|
-
import
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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><
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
426
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
371
427
|
|
|
372
428
|
```ts
|
|
373
|
-
interceptEvent
|
|
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:
|
|
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><
|
|
456
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
391
457
|
|
|
392
458
|
```ts
|
|
393
|
-
interceptWindowEvent
|
|
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:
|
|
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
|
-
|
|
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><
|
|
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:
|
|
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
|
|
554
|
+
Clamps a number between a min and max boundary (inclusive).
|
|
454
555
|
|
|
455
|
-
<details><summary><
|
|
556
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
456
557
|
|
|
457
558
|
```ts
|
|
458
|
-
clamp
|
|
459
|
-
|
|
460
|
-
clamp(7, 0, 10);
|
|
461
|
-
clamp(
|
|
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:
|
|
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><
|
|
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
|
-
|
|
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><
|
|
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:
|
|
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
|
|
769
|
+
If an array or NodeList is passed, the amount of contained items will be used.
|
|
515
770
|
|
|
516
|
-
<details><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:
|
|
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><
|
|
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:
|
|
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><
|
|
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><
|
|
852
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
585
853
|
|
|
586
854
|
```ts
|
|
587
|
-
fetchAdvanced
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
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><
|
|
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:
|
|
947
|
+
Usage:
|
|
948
|
+
```ts
|
|
949
|
+
randomizeArray(array: Array): Array
|
|
950
|
+
```
|
|
662
951
|
|
|
663
|
-
Returns a copy of
|
|
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><
|
|
955
|
+
<details><summary><h4>Example - click to view</h4></summary>
|
|
667
956
|
|
|
668
957
|
```ts
|
|
669
|
-
|
|
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
|
|
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.
|
|
6
|
-
exports.
|
|
5
|
+
exports.ConfigManager = y;
|
|
6
|
+
exports.addGlobalStyle = W;
|
|
7
|
+
exports.addParent = j;
|
|
7
8
|
exports.amplifyMedia = q;
|
|
8
|
-
exports.autoPlural =
|
|
9
|
-
exports.clamp =
|
|
10
|
-
exports.debounce =
|
|
11
|
-
exports.fetchAdvanced =
|
|
12
|
-
exports.getSelectorMap =
|
|
13
|
-
exports.getUnsafeWindow =
|
|
14
|
-
exports.initOnSelector =
|
|
15
|
-
exports.insertAfter =
|
|
16
|
-
exports.interceptEvent =
|
|
17
|
-
exports.interceptWindowEvent =
|
|
18
|
-
exports.
|
|
19
|
-
exports.
|
|
20
|
-
exports.
|
|
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 =
|
|
23
|
-
exports.randRange =
|
|
24
|
-
exports.randomItem =
|
|
25
|
-
exports.randomItemIndex =
|
|
26
|
-
exports.randomizeArray =
|
|
27
|
-
exports.removeOnSelector =
|
|
28
|
-
exports.takeRandomItem =
|
|
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
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
};
|
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,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> =
|
|
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> =
|
|
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
|
|
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": "
|
|
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-
|
|
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"
|
|
@@ -44,7 +48,8 @@
|
|
|
44
48
|
"files": [
|
|
45
49
|
"/dist/index.js",
|
|
46
50
|
"/dist/index.mjs",
|
|
47
|
-
"/dist/
|
|
51
|
+
"/dist/index.global.js",
|
|
52
|
+
"/dist/lib/**.d.ts",
|
|
48
53
|
"/package.json",
|
|
49
54
|
"/README.md",
|
|
50
55
|
"/CHANGELOG.md",
|