@sv443-network/userutils 2.0.0 → 3.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/README.md CHANGED
@@ -1,23 +1,27 @@
1
1
  <div style="text-align: center;" align="center">
2
2
 
3
+ <!-- #MARKER Description -->
3
4
  ## UserUtils
4
5
  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
6
 
6
- Contains builtin TypeScript declarations. Webpack compatible and supports ESM and CJS.
7
+ Contains builtin TypeScript declarations. Fully web compatible and supports ESM, CJS and global imports.
7
8
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
8
9
 
10
+ <br>
11
+
12
+ View the documentation of previous major releases: [2.0.1](https://github.com/Sv443-Network/UserUtils/blob/v2.0.1/README.md), [1.2.0](https://github.com/Sv443-Network/UserUtils/blob/v1.2.0/README.md), [0.5.3](https://github.com/Sv443-Network/UserUtils/blob/v0.5.3/README.md)
13
+
9
14
  </div>
10
15
  <br>
11
16
 
17
+ <!-- #MARKER Table of Contents -->
12
18
  ## Table of Contents:
13
19
  - [**Installation**](#installation)
14
- - [**Preamble**](#preamble)
20
+ - [**Preamble** (info about the documentation)](#preamble)
15
21
  - [**License**](#license)
16
22
  - [**Features**](#features)
17
23
  - [**DOM:**](#dom)
18
- - [onSelector()](#onselector) - call a listener once a selector is found in the DOM
19
- - [initOnSelector()](#initonselector) - needs to be called once to be able to use `onSelector()`
20
- - [getSelectorMap()](#getselectormap) - returns all currently registered selectors, listeners and options
24
+ - [SelectorObserver](#selectorobserver) - class that manages listeners that are called when selectors are found in the DOM
21
25
  - [getUnsafeWindow()](#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
22
26
  - [insertAfter()](#insertafter) - insert an element as a sibling after another element
23
27
  - [addParent()](#addparent) - add a parent element around another element
@@ -32,8 +36,9 @@ If you like using this library, please consider [supporting the development ❤
32
36
  - [clamp()](#clamp) - constrain a number between a min and max value
33
37
  - [mapRange()](#maprange) - map a number from one range to the same spot in another range
34
38
  - [randRange()](#randrange) - generate a random number between a min and max boundary
39
+ - [randomId()](#randomid) - generate a random ID of a given length and radix
35
40
  - [**Misc:**](#misc)
36
- - [ConfigManager()](#configmanager) - class that manages persistent userscript configurations, including data migration
41
+ - [ConfigManager](#configmanager) - class that manages persistent userscript configurations, including data migration
37
42
  - [autoPlural()](#autoplural) - automatically pluralize a string
38
43
  - [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time
39
44
  - [debounce()](#debounce) - call a function only once, after a given amount of time
@@ -51,9 +56,11 @@ If you like using this library, please consider [supporting the development ❤
51
56
  - [tr.getLanguage()](#trgetlanguage) - returns the currently active language
52
57
  - [**Utility types for TypeScript:**](#utility-types)
53
58
  - [Stringifiable](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly)
59
+ - [NonEmptyArray](https://github.com/Sv443-Network/UserUtils#nonemptyarray) - any array that should have at least one item
54
60
 
55
61
  <br><br>
56
62
 
63
+ <!-- #MARKER Installation -->
57
64
  ## Installation:
58
65
  - If you are using a bundler like webpack, you can install this package using npm:
59
66
  ```
@@ -75,7 +82,11 @@ If you like using this library, please consider [supporting the development ❤
75
82
  ```
76
83
  // @require https://greasyfork.org/scripts/472956-userutils/code/UserUtils.js
77
84
  ```
78
-
85
+ ```
86
+ // @require https://openuserjs.org/src/libs/Sv443/UserUtils.js
87
+ ```
88
+ (in order for your userscript not to break on a major library update, use the versioned URL at the top of the [GreasyFork page](https://greasyfork.org/scripts/472956-userutils))
89
+
79
90
  Then, access the functions on the global variable `UserUtils`:
80
91
  ```ts
81
92
  UserUtils.addGlobalStyle("body { background-color: red; }");
@@ -88,164 +99,316 @@ If you like using this library, please consider [supporting the development ❤
88
99
 
89
100
  <br><br>
90
101
 
102
+ <!-- #MARKER Preamble -->
91
103
  ## Preamble:
92
104
  This library is written in TypeScript and contains builtin TypeScript declarations.
93
105
 
94
106
  Each feature has example code that can be expanded by clicking on the text "Example - click to view".
95
- 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).
96
- 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.
107
+ The usages and examples are written in TypeScript and use ESM import syntax, but the library can also be used in plain JavaScript after removing the type annotations (and changing the imports if you are using CommonJS or the global declaration).
108
+ If the usage section contains multiple usages of the function, each occurrence represents an overload and you can choose which one you want to use.
97
109
 
98
110
  Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
99
111
  Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
100
112
 
101
113
  <br><br>
102
114
 
115
+ <!-- #MARKER License -->
103
116
  ## License:
104
117
  This library is licensed under the MIT License.
105
118
  See the [license file](./LICENSE.txt) for details.
106
119
 
107
120
  <br><br>
108
121
 
122
+ <!-- #MARKER Features -->
109
123
  ## Features:
110
124
 
111
125
  <br>
112
126
 
127
+ <!-- #SECTION DOM -->
113
128
  ## DOM:
114
129
 
115
- ### onSelector()
130
+ ### SelectorObserver
116
131
  Usage:
117
132
  ```ts
118
- onSelector<TElement = HTMLElement>(selector: string, options: {
119
- listener: (elements: TElement | NodeListOf<TElement>) => void,
120
- all?: boolean,
121
- continuous?: boolean,
122
- }): void
133
+ new SelectorObserver(baseElement: Element, options?: SelectorObserverOptions)
134
+ new SelectorObserver(baseElementSelector: string, options?: SelectorObserverOptions)
123
135
  ```
136
+
137
+ A class that manages listeners that are called when elements at given selectors are found in the DOM.
138
+ This is useful for userscripts that need to wait for elements to be added to the DOM at an indeterminate point in time before they can be interacted with.
124
139
 
125
- Registers a listener to be called whenever the element(s) behind a selector is/are found in the DOM.
126
- If the selector already exists, the listener will be called immediately.
140
+ The constructor takes a `baseElement`, which is a parent of the elements you want to observe.
141
+ If a selector string is passed instead, it will be used to find the element.
142
+ If you want to observe the entire document, you can pass `document.body`
143
+
144
+ The `options` parameter is optional and will be passed to the MutationObserver that is used internally.
145
+ The default options are `{ childList: true, subtree: true }` - you may see the [MutationObserver.observe() documentation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) for more information and a list of options.
146
+ For example, if you want to trigger the listeners when certain attributes change, pass `{ attributes: true, attributeFilter: ["class", "data-my-attribute"] }`
147
+ Additionally, there are the following extra options:
148
+ - `defaultDebounce` - if set to a number, this debounce will be applied to every listener that doesn't have a custom debounce set (defaults to 0)
127
149
 
128
- If `all` is set to `true`, querySelectorAll() will be used instead and the listener will return a NodeList of matching elements.
129
- This will also include elements that were already found in a previous listener call.
130
- If set to `false` (default), querySelector() will be used and only the first matching element will be returned.
150
+ ⚠️ Make sure to call `enable()` to actually start observing. This will need to be done after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired) **and** as soon as the `baseElement` or `baseElementSelector` is available.
151
+
152
+ <br>
153
+
154
+ #### Methods:
155
+ `addListener<TElement = HTMLElement>(selector: string, options: SelectorListenerOptions): void`
156
+ Adds a listener (specified in `options.listener`) for the given selector that will be called once the selector exists in the DOM. It will be passed the element(s) that match the selector as the only argument.
157
+ The listener will be called immediately if the selector already exists in the DOM.
158
+
159
+ > `options.listener` is the only required property of the `options` object.
160
+ > It is a function that will be called once the selector exists in the DOM.
161
+ > It will be passed the found element or NodeList of elements, depending on if `options.all` is set to true or false.
131
162
 
132
- If `continuous` is set to `true`, the listener will not be deregistered after it was called once (defaults to false).
163
+ > If `options.all` is set to true, querySelectorAll() will be used instead and the listener will be passed a `NodeList` of matching elements.
164
+ > This will also include elements that were already found in a previous listener call.
165
+ > If set to false (default), querySelector() will be used and only the first matching element will be returned.
133
166
 
134
- When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that the listener will return.
135
- It will default to `HTMLElement` if left undefined.
167
+ > If `options.continuous` is set to true, the listener will not be deregistered after it was called once (defaults to false).
168
+ >
169
+ > ⚠️ You should keep usage of this option to a minimum, as it will cause the listener to be called every time the selector is *checked for and found* and this can stack up quite quickly.
170
+ > ⚠️ You should try to only use this option on SelectorObserver instances that are scoped really low in the DOM tree to prevent as many selector checks as possible from being triggered.
171
+ > ⚠️ I also recommend always setting a debounce time (see constructor or below) if you use this option.
136
172
 
137
- ⚠️ In order to use this function, [`initOnSelector()`](#initonselector) has to be called as soon as possible.
138
- This initialization function has to be called after `DOMContentLoaded` is fired (or immediately if `@run-at document-end` is set).
173
+ > If `options.debounce` is set to a number above 0, the listener will be debounced by that amount of milliseconds (defaults to 0).
174
+ > E.g. if the debounce time is set to 200 and the selector is found twice within 100ms, only the last call of the listener will be executed.
139
175
 
140
- Calling onSelector() before `DOMContentLoaded` is fired will not throw an error, but it also won't trigger listeners until the DOM is accessible.
176
+ > When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that the listener will return.
177
+ > It will default to HTMLElement if left undefined.
141
178
 
142
- <details><summary><b>Example - click to view</b></summary>
143
-
144
- ```ts
145
- import { initOnSelector, onSelector } from "@sv443-network/userutils";
146
-
147
- document.addEventListener("DOMContentLoaded", initOnSelector);
179
+ <br>
148
180
 
149
- // Continuously checks if `div` elements are added to the DOM, then returns all of them (even previously detected ones) in a NodeList
150
- onSelector<HTMLDivElement>("div", {
151
- listener: (elements) => {
152
- console.log("Elements found:", elements); // type = NodeListOf<HTMLDivElement>
153
- },
154
- all: true,
155
- continuous: true,
156
- });
181
+ `enable(immediatelyCheckSelectors?: boolean): boolean`
182
+ Enables the observation of the child elements for the first time or if it was disabled before.
183
+ `immediatelyCheckSelectors` is set to true by default, which means all previously registered selectors will be checked. Set to false to only check them on the first detected mutation.
184
+ Returns true if the observation was enabled, false if it was already enabled or the passed `baseElementSelector` couldn't be found.
185
+
186
+ <br>
157
187
 
158
- // Checks if an input element with a value attribute of "5" is added to the DOM, then returns it and deregisters the listener
159
- onSelector<HTMLInputElement>("input[value=\"5\"]", {
160
- listener: (element) => {
161
- console.log("Element found:", element); // type = HTMLInputElement
162
- },
163
- });
164
- ```
188
+ `disable(): void`
189
+ Disables the observation of the child elements.
190
+ If selectors are currently being checked, the current selector will be finished before disabling.
191
+
192
+ <br>
165
193
 
166
- </details>
194
+ `isEnabled(): boolean`
195
+ Returns whether the observation of the child elements is currently enabled.
196
+
197
+ <br>
167
198
 
199
+ `clearListeners(): void`
200
+ Removes all listeners for all selectors.
201
+
168
202
  <br>
169
203
 
170
- ### initOnSelector()
171
- Usage:
172
- ```ts
173
- initOnSelector(options?: MutationObserverInit): void
174
- ```
204
+ `removeAllListeners(selector: string): boolean`
205
+ Removes all listeners for the given selector.
175
206
 
176
- Initializes the MutationObserver that is used by [`onSelector()`](#onselector) to check for the registered selectors whenever a DOM change occurs on the `<body>`
177
- By default, this only checks if elements are added or removed (at any depth).
207
+ <br>
178
208
 
179
- ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
209
+ `removeListener(selector: string, options: SelectorListenerOptions): boolean`
210
+ Removes a specific listener for the given selector and options.
180
211
 
181
- The options object is passed directly to the MutationObserver.observe() method.
182
- Note that `options.subtree` and `options.childList` will be set to true by default.
183
- You may see all options [here](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options), but these are the important ones:
184
- > Set `options.attributes` to `true` to also check for attribute changes on every single descendant of the `<body>` (defaults to false).
185
- > Set `options.characterData` to `true` to also check for character data changes on every single descendant of the `<body>` (defaults to false).
186
- >
187
- > ⚠️ Using these extra options can have a performance impact on larger sites or sites with a constantly changing DOM.
212
+ <br>
213
+
214
+ `getAllListeners(): Map<string, SelectorListenerOptions[]>`
215
+ Returns a Map of all selectors and their listeners.
188
216
 
189
- <details><summary><b>Example - click to view</b></summary>
217
+ <br>
218
+
219
+ `getListeners(selector: string): SelectorListenerOptions[] | undefined`
220
+ Returns all listeners for the given selector or undefined if there are none.
221
+
222
+ <br>
223
+
224
+ <details><summary><b>Examples - click to view</b></summary>
225
+
226
+ #### Basic usage:
190
227
 
191
228
  ```ts
192
- import { initOnSelector } from "@sv443-network/userutils";
229
+ import { SelectorObserver } from "@sv443-network/userutils";
230
+
231
+ // adding a single-shot listener before the element exists:
232
+ const fooObserver = new SelectorObserver("body");
233
+
234
+ fooObserver.addListener("#my-element", {
235
+ listener: (element) => {
236
+ console.log("Element found:", element);
237
+ },
238
+ });
193
239
 
194
240
  document.addEventListener("DOMContentLoaded", () => {
195
- initOnSelector({
196
- attributes: true,
197
- characterData: true,
241
+ // starting observation after the <body> element is available:
242
+ fooObserver.enable();
243
+
244
+
245
+ // adding custom observer options:
246
+
247
+ const barObserver = new SelectorObserver(document.body, {
248
+ // only check if the following attributes change:
249
+ attributeFilter: ["class", "style", "data-whatever"],
250
+ // debounce all listeners by 100ms unless specified otherwise:
251
+ defaultDebounce: 100,
252
+ });
253
+
254
+ barObserver.addListener("#my-element", {
255
+ listener: (element) => {
256
+ console.log("Element's attributes changed:", element);
257
+ },
198
258
  });
259
+
260
+ barObserver.addListener("#my-other-element", {
261
+ // set the debounce higher than provided by the defaultDebounce property:
262
+ debounce: 250,
263
+ listener: (element) => {
264
+ console.log("Other element's attributes changed:", element);
265
+ },
266
+ });
267
+
268
+ barObserver.enable();
269
+
270
+
271
+ // using custom listener options:
272
+
273
+ const bazObserver = new SelectorObserver(document.body);
274
+
275
+ // for TypeScript, specify that input elements are returned by the listener:
276
+ bazObserver.addListener<HTMLInputElement>("input", {
277
+ all: true, // use querySelectorAll() instead of querySelector()
278
+ continuous: true, // don't remove the listener after it was called once
279
+ debounce: 50, // debounce the listener by 50ms
280
+ listener: (elements) => {
281
+ // type of `elements` is NodeListOf<HTMLInputElement>
282
+ console.log("Input elements found:", elements);
283
+ },
284
+ });
285
+
286
+ bazObserver.enable();
287
+
288
+
289
+ // use a different element as the base:
290
+
291
+ const myElement = document.querySelector("#my-element");
292
+ if(myElement) {
293
+ const quxObserver = new SelectorObserver(myElement);
294
+
295
+ quxObserver.addListener("#my-child-element", {
296
+ listener: (element) => {
297
+ console.log("Child element found:", element);
298
+ },
299
+ });
300
+
301
+ quxObserver.enable();
302
+ }
199
303
  });
200
304
  ```
201
305
 
202
- </details>
203
-
204
306
  <br>
205
307
 
206
- ### getSelectorMap()
207
- Usage:
308
+ #### Get and remove listeners:
309
+
208
310
  ```ts
209
- getSelectorMap(): Map<string, OnSelectorOptions[]>
311
+ import { SelectorObserver } from "@sv443-network/userutils";
312
+
313
+ document.addEventListener("DOMContentLoaded", () => {
314
+ const observer = new SelectorObserver(document.body);
315
+
316
+ observer.addListener("#my-element-foo", {
317
+ continuous: true,
318
+ listener: (element) => {
319
+ console.log("Element found:", element);
320
+ },
321
+ });
322
+
323
+ observer.addListener("#my-element-bar", {
324
+ listener: (element) => {
325
+ console.log("Element found again:", element);
326
+ },
327
+ });
328
+
329
+ observer.enable();
330
+
331
+
332
+ // get all listeners:
333
+
334
+ console.log(observer.getAllListeners());
335
+ // Map(2) {
336
+ // '#my-element-foo' => [ { listener: [Function: listener] } ],
337
+ // '#my-element-bar' => [ { listener: [Function: listener] } ]
338
+ // }
339
+
340
+
341
+ // get listeners for a specific selector:
342
+
343
+ console.log(observer.getListeners("#my-element-foo"));
344
+ // [ { listener: [Function: listener], continuous: true } ]
345
+
346
+
347
+ // remove all listeners for a specific selector:
348
+
349
+ observer.removeAllListeners("#my-element-foo");
350
+ console.log(observer.getAllListeners());
351
+ // Map(1) {
352
+ // '#my-element-bar' => [ { listener: [Function: listener] } ]
353
+ // }
354
+ });
210
355
  ```
211
-
212
- Returns a Map of all currently registered selectors and their options, including listener function.
213
- Since multiple listeners can be registered for the same selector, the value of the Map is an array of `OnSelectorOptions` objects.
214
-
215
- <details><summary><b>Example - click to view</b></summary>
356
+
357
+ <br>
358
+
359
+ #### Chaining:
216
360
 
217
361
  ```ts
218
- import { initOnSelector, onSelector, getSelectorMap } from "@sv443-network/userutils";
362
+ import { SelectorObserver } from "@sv443-network/userutils";
363
+ import type { SelectorObserverOptions } from "@sv443-network/userutils";
219
364
 
220
- document.addEventListener("DOMContentLoaded", initOnSelector);
365
+ // apply a default debounce to all SelectorObserver instances:
366
+ const defaultOptions: SelectorObserverOptions = {
367
+ defaultDebounce: 100,
368
+ };
221
369
 
222
- onSelector<HTMLDivElement>("div", {
223
- listener: (elements) => void 0,
224
- all: true,
225
- continuous: true,
226
- });
370
+ document.addEventListener("DOMContentLoaded", () => {
371
+ // initialize generic observer that in turn initializes "sub-observers":
372
+ const fooObserver = new SelectorObserver(document.body, {
373
+ ...defaultOptions,
374
+ // define any other specific options here
375
+ });
227
376
 
228
- onSelector<HTMLDivElement>("div", {
229
- listener: (elements) => void 0,
230
- });
377
+ const myElementSelector = "#my-element";
378
+
379
+ // this relatively expensive listener (as it is in the full <body> scope) will only fire once:
380
+ fooObserver.addListener(myElementSelector, {
381
+ listener: (element) => {
382
+ // only enable barObserver once its baseElement exists:
383
+ barObserver.enable();
384
+ },
385
+ });
386
+
387
+ // barObserver is created at the same time as fooObserver, but only enabled once #my-element exists
388
+ const barObserver = new SelectorObserver(element, {
389
+ ...defaultOptions,
390
+ // define any other specific options here
391
+ });
392
+
393
+ // this selector will be checked for immediately after `enable()` is called
394
+ // and on each subsequent mutation because `continuous` is set to true.
395
+ // however it is much less expensive as it is scoped to a lower element which will receive less DOM updates
396
+ barObserver.addListener(".my-child-element", {
397
+ all: true,
398
+ continuous: true,
399
+ listener: (elements) => {
400
+ console.log("Child elements found:", elements);
401
+ },
402
+ });
231
403
 
232
- const selectorMap = getSelectorMap();
233
- // Map(1) {
234
- // "div" => [
235
- // {
236
- // listener: (elements) => void 0,
237
- // all: true,
238
- // continuous: true,
239
- // },
240
- // {
241
- // listener: (elements) => void 0,
242
- // },
243
- // ]
244
- // }
404
+ // immediately enable fooObserver as the <body> is available as soon as "DOMContentLoaded" fires:
405
+ fooObserver.enable();
406
+ });
245
407
  ```
246
408
 
247
409
  </details>
248
410
 
411
+
249
412
  <br>
250
413
 
251
414
  ### getUnsafeWindow()
@@ -383,6 +546,9 @@ preloadImages([
383
546
  ], true)
384
547
  .then((results) => {
385
548
  console.log("Images preloaded. Results:", results);
549
+ })
550
+ .catch((results) => {
551
+ console.error("Couldn't preload all images. Results:", results);
386
552
  });
387
553
  ```
388
554
 
@@ -422,11 +588,12 @@ Usage:
422
588
  interceptEvent(
423
589
  eventObject: EventTarget,
424
590
  eventName: string,
425
- predicate: (event: Event) => boolean
591
+ predicate?: (event: Event) => boolean
426
592
  ): void
427
593
  ```
428
594
 
429
595
  Intercepts all events dispatched on the `eventObject` and prevents the listeners from being called as long as the predicate function returns a truthy value.
596
+ If no predicate is specified, all events will be discarded.
430
597
  Calling this function will set the `Error.stackTraceLimit` to 1000 (if it's not already higher) to ensure the stack trace is preserved.
431
598
 
432
599
  ⚠️ 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.
@@ -437,8 +604,12 @@ Calling this function will set the `Error.stackTraceLimit` to 1000 (if it's not
437
604
  import { interceptEvent } from "@sv443-network/userutils";
438
605
 
439
606
  interceptEvent(document.body, "click", (event) => {
440
- console.log("Intercepting click event:", event);
441
- return true; // prevent all click events on the body element
607
+ // prevent all click events on <a> elements within the entire <body>
608
+ if(event.target instanceof HTMLAnchorElement) {
609
+ console.log("Intercepting click event:", event);
610
+ return true;
611
+ }
612
+ return false; // allow all other click events through
442
613
  });
443
614
  ```
444
615
 
@@ -451,11 +622,12 @@ Usage:
451
622
  ```ts
452
623
  interceptWindowEvent(
453
624
  eventName: string,
454
- predicate: (event: Event) => boolean
625
+ predicate?: (event: Event) => boolean
455
626
  ): void
456
627
  ```
457
628
 
458
629
  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.
630
+ If no predicate is specified, all events will be discarded.
459
631
  This is essentially the same as [`interceptEvent()`](#interceptevent), but automatically uses the `unsafeWindow` (or falls back to regular `window`).
460
632
 
461
633
  ⚠️ 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.
@@ -466,9 +638,9 @@ This is essentially the same as [`interceptEvent()`](#interceptevent), but autom
466
638
  ```ts
467
639
  import { interceptWindowEvent } from "@sv443-network/userutils";
468
640
 
469
- interceptWindowEvent("beforeunload", (event) => {
470
- return true; // prevent the pesky "Are you sure you want to leave this page?" popup
471
- });
641
+ // prevent the pesky "Are you sure you want to leave this page?" popup
642
+ // as no predicate is specified, all events will be discarded by default
643
+ interceptWindowEvent("beforeunload");
472
644
  ```
473
645
 
474
646
  </details>
@@ -478,36 +650,43 @@ interceptWindowEvent("beforeunload", (event) => {
478
650
  ### amplifyMedia()
479
651
  Usage:
480
652
  ```ts
481
- amplifyMedia(mediaElement: HTMLMediaElement, initialMultiplier?: number): AmplifyMediaResult
653
+ amplifyMedia(mediaElement: HTMLMediaElement, initialGain?: number): AmplifyMediaResult
482
654
  ```
483
655
 
484
- Amplifies the gain of a media element (like `<audio>` or `<video>`) by a given multiplier (defaults to 1.0).
485
- This is how you can increase the volume of a media element beyond the default maximum volume of 1.0 or 100%.
486
- Make sure to limit the multiplier to a reasonable value ([clamp()](#clamp) is good for this), as it may cause bleeding eardrums.
656
+ Amplifies the volume of a media element (like `<audio>` or `<video>`) by the given gain value.
657
+ This is how you can increase the volume of a media element beyond the default maximum volume of 100%.
658
+ Make sure to limit the value to a reasonable value ([clamp()](#clamp) is good for this), as it may otherwise cause bleeding eardrums.
487
659
 
488
- This is the processing workflow applied to the media element:
489
- `MediaElement (source)` => `DynamicsCompressorNode (limiter)` => `GainNode` => `AudioDestination (output)`
660
+ The default gain value passed to the GainNode is `1.0`
661
+ It may be read and changed at any point by calling the `getGain()` and `setGain()` methods of the returned object.
490
662
 
491
- A limiter (compression) is applied to the audio to prevent clipping.
492
- Its properties can be changed by calling the returned function `setLimiterOptions()`
493
- The default props are `{ threshold: -2, knee: 40, ratio: 12, attack: 0.003, release: 0.25 }`
663
+ To activate the amplification for the first time, call the `enable()` method of the returned object.
664
+
665
+ This is the processing workflow applied to the media element:
666
+ `MediaElement (source)` => `GainNode (pre-amplifier)` => 10x `BiquadFilterNode` => `GainNode (post-amplifier)` => `destination`
494
667
 
495
668
  ⚠️ 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.
496
669
  ⚠️ Make sure to call the returned function `enable()` after calling this function to actually enable the amplification.
497
670
 
498
- The returned object of the type `AmplifyMediaResult` has the following properties:
671
+ The returned object of the type `AmplifyMediaResult` has the following properties:
672
+ **Important properties:**
499
673
  | Property | Description |
500
674
  | :-- | :-- |
501
- | `setGain()` | Used to change the gain multiplier |
502
- | `getGain()` | Returns the current gain multiplier |
503
- | `enable()` | Call to enable the amplification for the first time or if it was disabled before |
675
+ | `enable()` | Call to enable the amplification for the first time or re-enable it if it was disabled before |
504
676
  | `disable()` | Call to disable amplification |
505
- | `setLimiterOptions()` | Used for changing the [options of the DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options) - the default is `{ threshold: -2, knee: 40, ratio: 12, attack: 0.003, release: 0.25 }` |
506
- | `context` | The AudioContext instance |
507
- | `source` | The MediaElementSourceNode instance |
508
- | `gainNode` | The GainNode instance used for actually boosting the gain |
509
- | `limiterNode` | The DynamicsCompressorNode instance used for limiting clipping and distortion |
677
+ | `enabled` | Whether the amplification is currently enabled |
678
+ | `setGain()` | Used to change the gain value from the default given by the parameter `initialGain` |
679
+ | `getGain()` | Returns the current gain value |
680
+
681
+ **Other properties:**
682
+ | Property | Description |
683
+ | :-- | :-- |
684
+ | `context` | The AudioContext instance used as the audio destination and context within the nodes are created |
685
+ | `sourceNode` | A MediaElementSourceNode instance created from the passed `mediaElement` |
686
+ | `gainNode` | The GainNode instance used for volume amplification |
510
687
 
688
+ <br>
689
+
511
690
  <details><summary><b>Example - click to view</b></summary>
512
691
 
513
692
  ```ts
@@ -518,11 +697,12 @@ const audioElement = document.querySelector<HTMLAudioElement>("audio");
518
697
 
519
698
  let ampResult: AmplifyMediaResult | undefined;
520
699
 
521
- function setGain(newValue: number) {
700
+ function updateGainValue(gainValue: number) {
522
701
  if(!ampResult)
523
702
  return;
524
703
  // constrain the value to between 0 and 3 for safety
525
- ampResult.setGain(clamp(newValue, 0, 3));
704
+ ampResult.setGain(clamp(gainValue, 0, 3));
705
+
526
706
  console.log("Gain set to", ampResult.getGain());
527
707
  }
528
708
 
@@ -534,15 +714,17 @@ amplifyButton.addEventListener("click", () => {
534
714
  // only needs to be initialized once, afterwards the returned object
535
715
  // can be used to change settings and enable/disable the amplification
536
716
  if(!ampResult) {
537
- // initialize amplification and set gain to 2x
538
- ampResult = amplifyMedia(audioElement, 2);
717
+ // initialize amplification and set it to ~2x
718
+ ampResult = amplifyMedia(audioElement, 2.0);
719
+ }
720
+ if(!ampResult.enabled) {
539
721
  // enable the amplification
540
722
  ampResult.enable();
541
723
  }
542
724
 
543
- setGain(2.5); // set gain to 2.5x
725
+ updateGainValue(3.5); // try to set gain to ~3.5x
544
726
 
545
- console.log(ampResult.getGain()); // 2.5
727
+ console.log(ampResult.getGain()); // 3.0 (because of the clamp())
546
728
  });
547
729
 
548
730
 
@@ -554,23 +736,6 @@ disableButton.addEventListener("click", () => {
554
736
  ampResult.disable();
555
737
  }
556
738
  });
557
-
558
-
559
- const limiterButton = document.querySelector<HTMLButtonElement>("button#limiter");
560
-
561
- limiterButton.addEventListener("click", () => {
562
- if(ampResult) {
563
- // change the limiter options to a more aggressive setting
564
- // see https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options
565
- ampResult.setLimiterOptions({
566
- threshold: -10,
567
- knee: 20,
568
- ratio: 20,
569
- attack: 0.001,
570
- release: 0.1,
571
- });
572
- }
573
- });
574
739
  ```
575
740
 
576
741
  </details>
@@ -602,6 +767,7 @@ console.log("Element has a vertical scroll bar:", vertical);
602
767
 
603
768
  <br><br>
604
769
 
770
+ <!-- #SECTION Math -->
605
771
  ## Math:
606
772
 
607
773
  ### clamp()
@@ -636,10 +802,10 @@ Usage:
636
802
  ```ts
637
803
  mapRange(
638
804
  value: number,
639
- range_1_min: number,
640
- range_1_max: number,
641
- range_2_min: number,
642
- range_2_max: number
805
+ range1min: number,
806
+ range1max: number,
807
+ range2min: number,
808
+ range2max: number
643
809
  ): number
644
810
  ```
645
811
 
@@ -684,11 +850,39 @@ randRange(10); // 7
684
850
 
685
851
  </details>
686
852
 
853
+ <br>
854
+
855
+ ### randomId()
856
+ Usage:
857
+ ```ts
858
+ randomId(length?: number, radix?: number): string
859
+ ```
860
+
861
+ Generates a cryptographically strong random ID of a given length and [radix (base).](https://en.wikipedia.org/wiki/Radix)
862
+ The default length is 16 and the default radix is 16 (hexadecimal).
863
+ You may change the radix to get digits from different numerical systems.
864
+ Use 2 for binary, 8 for octal, 10 for decimal, 16 for hexadecimal and 36 for alphanumeric.
865
+
866
+ <details><summary><b>Example - click to view</b></summary>
867
+
868
+ ```ts
869
+ import { randomId } from "@sv443-network/userutils";
870
+
871
+ randomId(); // "1bda419a73629d4f" (length 16, radix 16)
872
+ randomId(10); // "f86cd354a4" (length 10, radix 16)
873
+ randomId(10, 2); // "1010001101" (length 10, radix 2)
874
+ randomId(10, 10); // "0183428506" (length 10, radix 10)
875
+ randomId(10, 36); // "z46jfpa37r" (length 10, radix 36)
876
+ ```
877
+
878
+ </details>
879
+
687
880
  <br><br>
688
881
 
882
+ <!-- #SECTION Misc -->
689
883
  ## Misc:
690
884
 
691
- ### ConfigManager()
885
+ ### ConfigManager
692
886
  Usage:
693
887
  ```ts
694
888
  new ConfigManager(options: ConfigManagerOptions)
@@ -711,7 +905,7 @@ The options object has the following properties:
711
905
 
712
906
  <br>
713
907
 
714
- ### Methods:
908
+ #### Methods:
715
909
  `loadData(): Promise<TData>`
716
910
  Asynchronously loads the configuration data from persistent storage and returns it.
717
911
  If no data was saved in persistent storage before, the value of `options.defaultConfig` will be returned and written to persistent storage.
@@ -929,6 +1123,7 @@ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
929
1123
 
930
1124
  <br><br>
931
1125
 
1126
+ <!-- #SECTION Arrays -->
932
1127
  ## Arrays:
933
1128
 
934
1129
  ### randomItem()
@@ -1029,6 +1224,7 @@ console.log(foo); // [1, 2, 3, 4, 5, 6] - original array is not mutated
1029
1224
 
1030
1225
  <br><br>
1031
1226
 
1227
+ <!-- #SECTION Translation -->
1032
1228
  ## Translation:
1033
1229
  This is a very lightweight translation function that can be used to translate simple strings.
1034
1230
  Pluralization is not supported but can be achieved manually by adding variations to the translations, identified by a different suffix. See the example section of [`tr.addLanguage()`](#traddlanguage) for an example on how this might be done.
@@ -1204,6 +1400,7 @@ If no language has been set yet, it will return undefined.
1204
1400
 
1205
1401
  <br><br>
1206
1402
 
1403
+ <!-- #SECTION Utility types -->
1207
1404
  ## Utility types:
1208
1405
  UserUtils also offers some utility types that can be used in TypeScript projects.
1209
1406
  They don't alter the runtime behavior of the code, but they can be used to make the code more readable and to prevent errors.
@@ -1236,13 +1433,41 @@ logSomething(true); // "Log: true"
1236
1433
  logSomething({}); // "Log: [object Object]"
1237
1434
  logSomething(Symbol(1)); // "Log: Symbol(1)"
1238
1435
  logSomething(fooObject); // "Log: hello world"
1239
- logSomething(barObject); // type error
1436
+
1437
+ logSomething(barObject); // Type Error
1438
+ ```
1439
+
1440
+ </details>
1441
+
1442
+ <br>
1443
+
1444
+ ## NonEmptyArray
1445
+ This type describes an array that has at least one item.
1446
+ Use the generic parameter to specify the type of the items in the array.
1447
+
1448
+ <details><summary><b>Example - click to view</b></summary>
1449
+
1450
+ ```ts
1451
+ import type { NonEmptyArray } from "@sv443-network/userutils";
1452
+
1453
+ function logFirstItem(array: NonEmptyArray<string>) {
1454
+ console.log(parseInt(array[0]));
1455
+ }
1456
+
1457
+ function somethingElse(array: NonEmptyArray) {
1458
+ // array is typed as NonEmptyArray<unknown> when not passing a
1459
+ // generic parameter, so this throws a TS error:
1460
+ console.log(parseInt(array[0])); // Argument of type 'unknown' is not assignable to parameter of type 'string'
1461
+ }
1462
+
1463
+ logFirstItem(["04abc", "69"]); // 4
1240
1464
  ```
1241
1465
 
1242
1466
  </details>
1243
1467
 
1244
1468
  <br><br><br><br>
1245
1469
 
1470
+ <!-- #MARKER Footer -->
1246
1471
  <div style="text-align: center;" align="center">
1247
1472
 
1248
1473
  Made with ❤️ by [Sv443](https://github.com/Sv443)