@sv443-network/userutils 7.0.1 → 7.2.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,10 +1,10 @@
1
1
  <div style="text-align: center;" align="center">
2
2
 
3
- <!-- #MARKER Description -->
3
+ <!-- #region Description -->
4
4
  ## UserUtils
5
- Zero-dependency library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more.
5
+ Lightweight library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more.
6
6
 
7
- Contains builtin TypeScript declarations. Fully web compatible and supports ESM and CJS imports and global declaration.
7
+ Contains builtin TypeScript declarations. Supports ESM and CJS imports via a bundler and UMD / global declaration via `@require`.
8
8
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
9
9
 
10
10
  <br>
@@ -19,7 +19,7 @@ View the documentation of previous major releases:
19
19
  </div>
20
20
  <br>
21
21
 
22
- <!-- #MARKER Table of Contents -->
22
+ <!-- #region Table of Contents -->
23
23
  ## Table of Contents:
24
24
  - [**Installation**](#installation)
25
25
  - [**Preamble** (info about the documentation)](#preamble)
@@ -37,6 +37,7 @@ View the documentation of previous major releases:
37
37
  - [`isScrollable()`](#isscrollable) - check if an element has a horizontal or vertical scroll bar
38
38
  - [`observeElementProp()`](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
39
39
  - [`getSiblingsFrame()`](#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
40
+ - [`setInnerHtmlUnsafe()`](#setinnerhtmlunsafe) - set the innerHTML of an element using a [Trusted Types policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) without sanitizing or escaping it
40
41
  - [**Math:**](#math)
41
42
  - [`clamp()`](#clamp) - constrain a number between a min and max value
42
43
  - [`mapRange()`](#maprange) - map a number from one range to the same spot in another range
@@ -44,6 +45,8 @@ View the documentation of previous major releases:
44
45
  - [**Misc:**](#misc)
45
46
  - [`DataStore`](#datastore) - class that manages a hybrid sync & async persistent JSON database, including data migration
46
47
  - [`DataStoreSerializer`](#datastoreserializer) - class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrations
48
+ - [`Dialog`](#dialog) - class for creating custom modal dialogs with a promise-based API and a generic, default style
49
+ - [`NanoEmitter`](#nanoemitter) - tiny event emitter class with a focus on performance and simplicity (based on [nanoevents](https://npmjs.com/package/nanoevents))
47
50
  - [`autoPlural()`](#autoplural) - automatically pluralize a string
48
51
  - [`pauseFor()`](#pausefor) - pause the execution of a function for a given amount of time
49
52
  - [`debounce()`](#debounce) - call a function only once in a series of calls, after or before a given timeout
@@ -60,9 +63,14 @@ View the documentation of previous major releases:
60
63
  - [`randomizeArray()`](#randomizearray) - returns a copy of the array with its items in a random order
61
64
  - [**Translation:**](#translation)
62
65
  - [`tr()`](#tr) - simple translation of a string to another language
63
- - [tr.addLanguage()](#traddlanguage) - add a language and its translations
64
- - [tr.setLanguage()](#trsetlanguage) - set the currently active language for translations
65
- - [tr.getLanguage()](#trgetlanguage) - returns the currently active language
66
+ - [`tr.addLanguage()`](#traddlanguage) - add a language and its translations
67
+ - [`tr.setLanguage()`](#trsetlanguage) - set the currently active language for translations
68
+ - [`tr.getLanguage()`](#trgetlanguage) - returns the currently active language
69
+ - [**Colors:**](#colors)
70
+ - [`hexToRgb()`](#hextorgb) - convert a hex color string to an RGB number tuple
71
+ - [`rgbToHex()`](#rgbtohex) - convert RGB numbers to a hex color string
72
+ - [`lightenColor()`](#lightencolor) - lighten a CSS color string (hex, rgb or rgba) by a given percentage
73
+ - [`darkenColor()`](#darkencolor) - darken a CSS color string (hex, rgb or rgba) by a given percentage
66
74
  - [**Utility types for TypeScript:**](#utility-types)
67
75
  - [`Stringifiable`](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly)
68
76
  - [`NonEmptyArray`](#nonemptyarray) - any array that should have at least one item
@@ -71,12 +79,15 @@ View the documentation of previous major releases:
71
79
 
72
80
  <br><br>
73
81
 
74
- <!-- #MARKER Installation -->
82
+ <!-- #region Installation -->
75
83
  ## Installation:
76
- - If you are using a bundler like webpack, you can install this package using npm:
84
+ 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.
85
+
86
+ - If you are using a bundler (like webpack, rollup, vite, etc.), you can install this package using npm:
77
87
  ```
78
88
  npm i @sv443-network/userutils
79
89
  ```
90
+ <sup>For other package managers, check out the respective install command on the [JavaScript Registry](https://jsr.io/@sv443-network/userutils)</sup>
80
91
  Then, import it in your script as usual:
81
92
  ```ts
82
93
  import { addGlobalStyle } from "@sv443-network/userutils";
@@ -85,18 +96,17 @@ View the documentation of previous major releases:
85
96
 
86
97
  import * as UserUtils from "@sv443-network/userutils";
87
98
  ```
88
- 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.
89
99
 
90
100
  <br>
91
101
 
92
- - If you are not using a bundler, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
102
+ - If you are not using a bundler or want to reduce the size of your userscript, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
93
103
  ```
94
104
  // @require https://greasyfork.org/scripts/472956-userutils/code/UserUtils.js
95
105
  ```
96
106
  ```
97
107
  // @require https://openuserjs.org/src/libs/Sv443/UserUtils.js
98
108
  ```
99
- (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))
109
+ (in order for your userscript not to break on a major library update, instead use the versioned URL at the top of the [GreasyFork page](https://greasyfork.org/scripts/472956-userutils))
100
110
 
101
111
  Then, access the functions on the global variable `UserUtils`:
102
112
  ```ts
@@ -107,10 +117,16 @@ View the documentation of previous major releases:
107
117
  const { clamp } = UserUtils;
108
118
  console.log(clamp(1, 5, 10)); // 5
109
119
  ```
120
+ If you're using TypeScript and it complains about the missing global variable `UserUtils`, install the library using the package manager of your choice and add the following inside a `.d.ts` file somewhere in your project:
121
+ ```ts
122
+ declare global {
123
+ const UserUtils: typeof import("@sv443-network/userutils");
124
+ }
125
+ ```
110
126
 
111
127
  <br><br>
112
128
 
113
- <!-- #MARKER Preamble -->
129
+ <!-- #region Preamble -->
114
130
  ## Preamble:
115
131
  This library is written in TypeScript and contains builtin TypeScript declarations.
116
132
 
@@ -123,19 +139,19 @@ Their documentation will contain a section marked by a warning emoji (⚠️) th
123
139
 
124
140
  <br><br>
125
141
 
126
- <!-- #MARKER License -->
142
+ <!-- #region License -->
127
143
  ## License:
128
144
  This library is licensed under the MIT License.
129
145
  See the [license file](./LICENSE.txt) for details.
130
146
 
131
147
  <br><br>
132
148
 
133
- <!-- #MARKER Features -->
149
+ <!-- #region Features -->
134
150
  ## Features:
135
151
 
136
152
  <br>
137
153
 
138
- <!-- #SECTION DOM -->
154
+ <!-- #region DOM -->
139
155
  ## DOM:
140
156
 
141
157
  ### SelectorObserver
@@ -435,10 +451,8 @@ document.addEventListener("DOMContentLoaded", () => {
435
451
  fooObserver.enable();
436
452
  });
437
453
  ```
438
-
439
454
  </details>
440
455
 
441
-
442
456
  <br>
443
457
 
444
458
  ### getUnsafeWindow()
@@ -466,7 +480,6 @@ const mouseEvent = new MouseEvent("mousemove", {
466
480
 
467
481
  document.body.dispatchEvent(mouseEvent);
468
482
  ```
469
-
470
483
  </details>
471
484
 
472
485
  <br>
@@ -494,7 +507,6 @@ newParent.href = "https://example.org/";
494
507
 
495
508
  addParent(element, newParent);
496
509
  ```
497
-
498
510
  </details>
499
511
 
500
512
  <br>
@@ -522,7 +534,6 @@ document.addEventListener("DOMContentLoaded", () => {
522
534
  `);
523
535
  });
524
536
  ```
525
-
526
537
  </details>
527
538
 
528
539
  <br>
@@ -554,7 +565,6 @@ preloadImages([
554
565
  console.error("Couldn't preload all images. Results:", results);
555
566
  });
556
567
  ```
557
-
558
568
  </details>
559
569
 
560
570
  <br>
@@ -581,7 +591,6 @@ document.querySelector("#my-button").addEventListener("click", () => {
581
591
  openInNewTab("https://example.org/", true);
582
592
  });
583
593
  ```
584
-
585
594
  </details>
586
595
 
587
596
  <br>
@@ -616,7 +625,6 @@ interceptEvent(document.body, "click", (event) => {
616
625
  return false; // allow all other click events through
617
626
  });
618
627
  ```
619
-
620
628
  </details>
621
629
 
622
630
  <br>
@@ -646,7 +654,6 @@ import { interceptWindowEvent } from "@sv443-network/userutils";
646
654
  // as no predicate is specified, all events will be discarded by default
647
655
  interceptWindowEvent("beforeunload");
648
656
  ```
649
-
650
657
  </details>
651
658
 
652
659
  <br>
@@ -671,7 +678,6 @@ const { horizontal, vertical } = isScrollable(element);
671
678
  console.log("Element has a horizontal scroll bar:", horizontal);
672
679
  console.log("Element has a vertical scroll bar:", vertical);
673
680
  ```
674
-
675
681
  </details>
676
682
 
677
683
  <br>
@@ -724,7 +730,6 @@ observeElementProp(myInput, "value", (oldValue, newValue) => {
724
730
  console.log("Value changed from", oldValue, "to", newValue);
725
731
  });
726
732
  ```
727
-
728
733
  </details>
729
734
 
730
735
  <br>
@@ -847,12 +852,42 @@ const allBelowExcl = getSiblingsFrame(refElement, Infinity, "bottom", false);
847
852
  // <div>5</div> │ frame
848
853
  // <div>6</div> ◄──┘
849
854
  ```
855
+ </details>
856
+
857
+ <br>
858
+
859
+ ### setInnerHtmlUnsafe()
860
+ Usage:
861
+ ```ts
862
+ setInnerHtmlUnsafe(element: Element, html: string): Element
863
+ ```
864
+
865
+ Sets the innerHTML property of the provided element without any sanitation or validation.
866
+ Makes use of the [Trusted Types API](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to trick the browser into thinking the HTML is safe.
867
+ Use this function if the page makes use of the CSP directive `require-trusted-types-for 'script'` and throws a "This document requires 'TrustedHTML' assignment" error on Chromium-based browsers.
868
+ If the browser doesn't support Trusted Types, this function will fall back to regular innerHTML assignment.
869
+
870
+ ⚠️ This function does not perform any sanitization, it only tricks the browser into thinking the HTML is safe and should thus be used with utmost caution, as it can easily cause XSS vulnerabilities!
871
+ A much better way of doing this is by using the [DOMPurify](https://github.com/cure53/DOMPurify#what-about-dompurify-and-trusted-types) library to create your own Trusted Types policy that *actually* sanitizes the HTML and prevents (most) XSS attack vectors.
872
+ You can also find more info [here.](https://web.dev/articles/trusted-types#library)
873
+
874
+ <details><summary><b>Example - click to view</b></summary>
875
+
876
+ ```ts
877
+ import { setInnerHtmlUnsafe } from "@sv443-network/userutils";
878
+
879
+ const myElement = document.querySelector("#my-element");
880
+ setInnerHtmlUnsafe(myElement, "<img src='https://picsum.photos/100/100' />"); // hardcoded value, so no XSS risk
850
881
 
882
+ const myXssElement = document.querySelector("#my-xss-element");
883
+ const userModifiableVariable = `<img onerror="alert('XSS!')" src="invalid" />`; // let's pretend this came from user input
884
+ setInnerHtmlUnsafe(myXssElement, userModifiableVariable); // <- uses a user-modifiable variable, so big XSS risk!
885
+ ```
851
886
  </details>
852
887
 
853
888
  <br><br>
854
889
 
855
- <!-- #SECTION Math -->
890
+ <!-- #region Math -->
856
891
  ## Math:
857
892
 
858
893
  ### clamp()
@@ -877,7 +912,6 @@ clamp(99999, 0, 10); // 10
877
912
  clamp(-99999, -Infinity, 0); // -99999
878
913
  clamp(99999, 0, Infinity); // 99999
879
914
  ```
880
-
881
915
  </details>
882
916
 
883
917
  <br>
@@ -908,7 +942,6 @@ mapRange(5, 0, 10, 0, 50); // 25
908
942
  // for example, if 4 files of a total of 13 were downloaded:
909
943
  mapRange(4, 0, 13, 0, 100); // 30.76923076923077
910
944
  ```
911
-
912
945
  </details>
913
946
 
914
947
  <br>
@@ -932,12 +965,11 @@ randRange(0, 10); // 4
932
965
  randRange(10, 20); // 17
933
966
  randRange(10); // 7
934
967
  ```
935
-
936
968
  </details>
937
969
 
938
970
  <br><br>
939
971
 
940
- <!-- #SECTION Misc -->
972
+ <!-- #region Misc -->
941
973
  ## Misc:
942
974
 
943
975
  ### DataStore
@@ -1100,7 +1132,6 @@ async function init() {
1100
1132
 
1101
1133
  init();
1102
1134
  ```
1103
-
1104
1135
  </details>
1105
1136
 
1106
1137
  <br>
@@ -1108,7 +1139,7 @@ init();
1108
1139
  ### DataStoreSerializer
1109
1140
  Usage:
1110
1141
  ```ts
1111
- new DataStoreSerializer(stores: DataStore[], options: DataStoreSerializerOptions)
1142
+ new DataStoreSerializer(stores: DataStore[], options?: DataStoreSerializerOptions)
1112
1143
  ```
1113
1144
 
1114
1145
  A class that manages serializing and deserializing (exporting and importing) one to infinite DataStore instances.
@@ -1201,8 +1232,13 @@ const serializer = new DataStoreSerializer([fooStore, barStore], {
1201
1232
  });
1202
1233
 
1203
1234
  async function exportMyDataPls() {
1235
+ // first, make sure the persistent data of the stores is loaded into their caches:
1236
+ await fooStore.loadData();
1237
+ await barStore.loadData();
1238
+
1239
+ // now serialize the data:
1204
1240
  const serializedData = await serializer.serialize();
1205
- // create file and download it:
1241
+ // create a file and download it:
1206
1242
  const blob = new Blob([serializedData], { type: "application/json" });
1207
1243
  const url = URL.createObjectURL(blob);
1208
1244
  const a = document.createElement("a");
@@ -1211,7 +1247,7 @@ async function exportMyDataPls() {
1211
1247
  a.click();
1212
1248
  a.remove();
1213
1249
 
1214
- // This function exports an object like this:
1250
+ // `serialize()` exports a stringified object that looks similar to this:
1215
1251
  // [
1216
1252
  // {
1217
1253
  // "id": "foo-data",
@@ -1246,7 +1282,241 @@ async function importMyDataPls() {
1246
1282
  ```
1247
1283
  </details>
1248
1284
 
1249
- <br><br>
1285
+ <br>
1286
+
1287
+ ### Dialog
1288
+ Usage:
1289
+ ```ts
1290
+ new Dialog(options: DialogOptions)
1291
+ ```
1292
+
1293
+ A class that creates a customizable modal dialog with a title (optional), body and footer (optional).
1294
+ There are tons of options for customization, like changing the close behavior, translating strings and more.
1295
+
1296
+ The options object has the following properties:
1297
+ | Property | Description |
1298
+ | :-- | :-- |
1299
+ | `id: string` | A unique internal identification string for this instance. If two Dialogs share the same ID, they will overwrite each other. |
1300
+ | `width: number` | The target and maximum width of the dialog in pixels. |
1301
+ | `height: number` | The target and maximum height of the dialog in pixels. |
1302
+ | `renderBody: () => HTMLElement \| Promise<HTMLElement>` | Called to render the body of the dialog. |
1303
+ | `renderHeader?: () => HTMLElement \| Promise<HTMLElement>` | (Optional) Called to render the header of the dialog. Leave undefined for a blank header. |
1304
+ | `renderFooter?: () => HTMLElement \| Promise<HTMLElement>` | (Optional) Called to render the footer of the dialog. Leave undefined for no footer. |
1305
+ | `closeOnBgClick?: boolean` | (Optional) Whether the dialog should close when the background is clicked. Defaults to `true`. |
1306
+ | `closeOnEscPress?: boolean` | (Optional) Whether the dialog should close when the escape key is pressed. Defaults to `true`. |
1307
+ | `destroyOnClose?: boolean` | (Optional) Whether the dialog should be destroyed when it's closed. Defaults to `false`. |
1308
+ | `unmountOnClose?: boolean` | (Optional) Whether the dialog should be unmounted when it's closed. Defaults to `true`. Superseded by `destroyOnClose`. |
1309
+ | `removeListenersOnDestroy?: boolean` | (Optional) Whether all listeners should be removed when the dialog is destroyed. Defaults to `true`. |
1310
+ | `small?: boolean` | (Optional) Whether the dialog should have a smaller overall appearance. Defaults to `false`. |
1311
+ | `verticalAlign?: "top" \| "center" \| "bottom"` | (Optional) Where to align or anchor the dialog vertically. Defaults to `"center"`. |
1312
+ | `strings?: Partial<typeof defaultStrings>` | (Optional) Strings used in the dialog (used for translations). Defaults to the default English strings (importable with the name `defaultStrings`). |
1313
+ | `dialogCss?: string` | (Optional) CSS to apply to the dialog. Defaults to the default (importable with the name `defaultDialogCss`). |
1314
+
1315
+ Methods:
1316
+ `open(): Promise<void>`
1317
+ Opens the dialog.
1318
+
1319
+ `close(): void`
1320
+ Closes the dialog.
1321
+
1322
+ `mount(): Promise<void>`
1323
+ Mounts the dialog to the DOM by calling the render functions provided in the options object.
1324
+ Can be done before opening the dialog to avoid a delay.
1325
+
1326
+ `unmount(): void`
1327
+ Unmounts the dialog from the DOM.
1328
+
1329
+ `remount(): Promise<void>`
1330
+ Unmounts and mounts the dialog again.
1331
+ The render functions in the options object will be called again.
1332
+ May cause a flickering effect due to the rendering delay.
1333
+
1334
+ `isOpen(): boolean`
1335
+ Returns `true` if the dialog is open, else `false`.
1336
+
1337
+ `isMounted(): boolean`
1338
+ Returns `true` if the dialog is mounted, else `false`.
1339
+
1340
+ `destroy(): void`
1341
+ Destroys the dialog.
1342
+ Removes all listeners and unmounts the dialog by default.
1343
+
1344
+ `static getCurrentDialogId(): string`
1345
+ Static method that returns the ID of the currently open dialog.
1346
+ Needs to be called without creating an instance of the class.
1347
+
1348
+ `static getOpenDialogs(): string[]`
1349
+ Static method that returns an array of the IDs of all open dialogs.
1350
+ Needs to be called without creating an instance of the class.
1351
+
1352
+ <details><summary><b>Example - click to view</b></summary>
1353
+
1354
+ ```ts
1355
+ import { Dialog } from "@sv443-network/userutils";
1356
+
1357
+ const fooDialog = new Dialog({
1358
+ id: "foo-dialog",
1359
+ width: 400,
1360
+ height: 300,
1361
+ renderHeader() {
1362
+ const header = document.createElement("div");
1363
+ header.textContent = "This is the header";
1364
+ return header;
1365
+ },
1366
+ renderBody() {
1367
+ const body = document.createElement("div");
1368
+ body.textContent = "This is the body";
1369
+ return body;
1370
+ },
1371
+ renderFooter() {
1372
+ const footer = document.createElement("div");
1373
+ footer.textContent = "This is the footer";
1374
+ return footer;
1375
+ },
1376
+ closeOnBgClick: true,
1377
+ closeOnEscPress: true,
1378
+ destroyOnClose: false,
1379
+ unmountOnClose: true,
1380
+ removeListenersOnDestroy: true,
1381
+ small: false,
1382
+ verticalAlign: "center",
1383
+ strings: {
1384
+ closeDialogTooltip: "Click to close",
1385
+ },
1386
+ dialogCss: getMyCustomDialogCss(),
1387
+ });
1388
+
1389
+ fooDialog.on("close", () => {
1390
+ console.log("Dialog closed");
1391
+ });
1392
+
1393
+ fooDialog.open();
1394
+ ```
1395
+ </details>
1396
+
1397
+ <br>
1398
+
1399
+ ### NanoEmitter
1400
+ Usage:
1401
+ ```ts
1402
+ new NanoEmitter<TEventMap = EventsMap>(options?: NanoEmitterOptions): NanoEmitter<TEventMap>
1403
+ ```
1404
+
1405
+ A class that provides a minimalistic event emitter with a tiny footprint powered by [nanoevents.](https://npmjs.com/package/nanoevents)
1406
+ The `TEventMap` generic is used to define the events that can be emitted and listened to.
1407
+
1408
+ The main intention behind this class is to extend it in your own classes to provide a simple event system directly built into the class.
1409
+ However in a functional environment you can also just create instances for use as standalone event emitters throughout your project.
1410
+
1411
+ The options object has the following properties:
1412
+ | Property | Description |
1413
+ | :-- | :-- |
1414
+ | `publicEmit?: boolean` | (Optional) If set to true, allows emitting events through the public method `emit()` (`false` by default). |
1415
+
1416
+ Methods:
1417
+ `on<K extends keyof TEventMap>(event: K, listener: TEventMap[K]): void`
1418
+ Registers a listener function for the given event.
1419
+ May be called multiple times for the same event.
1420
+
1421
+ `once<K extends keyof TEventMap>(event: K, listener: TEventMap[K]): void`
1422
+ Registers a listener function for the given event that will only be called once.
1423
+
1424
+ `emit<K extends keyof TEventMap>(event: K, ...args: Parameters<TEventMap[K]>): boolean`
1425
+ Emits an event with the given arguments from outside the class instance if `publicEmit` is set to `true`.
1426
+ If `publicEmit` is set to `true`, this method will return `true` if the event was emitted.
1427
+ If it is set to `false`, it will always return `false` and you will need to use `this.events.emit()` from inside the class instead.
1428
+
1429
+ `unsubscribeAll(): void`
1430
+ Removes all listeners from all events.
1431
+
1432
+ <br>
1433
+
1434
+ <details><summary><b>Object oriented example - click to view</b></summary>
1435
+
1436
+ ```ts
1437
+ import { NanoEmitter } from "@sv443-network/userutils";
1438
+
1439
+ // map of events for strong typing - the functions always return void
1440
+ interface MyEventMap {
1441
+ foo: (bar: string) => void;
1442
+ baz: (qux: number) => void;
1443
+ }
1444
+
1445
+ class MyClass extends NanoEmitter<MyEventMap> {
1446
+ constructor() {
1447
+ super({
1448
+ // allow emitting events from outside the class
1449
+ publicEmit: true,
1450
+ });
1451
+
1452
+ this.once("baz", (qux) => {
1453
+ console.log("baz event (inside, once):", qux);
1454
+ });
1455
+ }
1456
+
1457
+ public doStuff() {
1458
+ this.emit("foo", "hello");
1459
+ this.emit("baz", 42);
1460
+ this.emit("foo", "world");
1461
+ this.emit("baz", 69);
1462
+ }
1463
+ }
1464
+
1465
+ const myInstance = new MyClass();
1466
+ myInstance.doStuff();
1467
+
1468
+ myInstance.on("foo", (bar) => {
1469
+ console.log("foo event (outside):", bar);
1470
+ });
1471
+
1472
+ myInstance.emit("baz", "hello from the outside");
1473
+
1474
+ myInstance.unsubscribeAll();
1475
+ ```
1476
+ </details>
1477
+
1478
+ <br>
1479
+
1480
+ <details><summary><b>Functional example - click to view</b></summary>
1481
+
1482
+ ```ts
1483
+ import { NanoEmitter } from "@sv443-network/userutils";
1484
+
1485
+ // map of events for strong typing - the functions always return void
1486
+ interface MyEventMap {
1487
+ foo: (bar: string) => void;
1488
+ baz: (qux: number) => void;
1489
+ }
1490
+
1491
+ const myEmitter = new NanoEmitter<MyEventMap>({
1492
+ // allow emitting events from outside the class
1493
+ publicEmit: true,
1494
+ });
1495
+
1496
+ myEmitter.on("foo", (bar) => {
1497
+ console.log("foo event:", bar);
1498
+ });
1499
+
1500
+ myEmitter.once("baz", (qux) => {
1501
+ console.log("baz event (once):", qux);
1502
+ });
1503
+
1504
+ function doStuff() {
1505
+ myEmitter.emit("foo", "hello");
1506
+ myEmitter.emit("baz", 42);
1507
+ myEmitter.emit("foo", "world");
1508
+ myEmitter.emit("baz", 69);
1509
+
1510
+ myEmitter.emit("foo", "hello from the outside");
1511
+
1512
+ myEmitter.unsubscribeAll();
1513
+ }
1514
+
1515
+ doStuff();
1516
+ ```
1517
+ </details>
1518
+
1519
+ <br>
1250
1520
 
1251
1521
  ### autoPlural()
1252
1522
  Usage:
@@ -1272,7 +1542,6 @@ autoPlural("apple", [1, 2]); // "apples"
1272
1542
  const items = [1, 2, 3, 4, "foo", "bar"];
1273
1543
  console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 items"
1274
1544
  ```
1275
-
1276
1545
  </details>
1277
1546
 
1278
1547
  <br>
@@ -1296,7 +1565,6 @@ async function run() {
1296
1565
  console.log("World");
1297
1566
  }
1298
1567
  ```
1299
-
1300
1568
  </details>
1301
1569
 
1302
1570
  <br>
@@ -1342,7 +1610,6 @@ const myFunc = debounce((event) => {
1342
1610
 
1343
1611
  document.body.addEventListener("scroll", myFunc);
1344
1612
  ```
1345
-
1346
1613
  </details>
1347
1614
 
1348
1615
  <br>
@@ -1377,7 +1644,6 @@ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
1377
1644
  console.error("Fetch error:", err);
1378
1645
  });
1379
1646
  ```
1380
-
1381
1647
  </details>
1382
1648
 
1383
1649
  <br>
@@ -1405,7 +1671,6 @@ insertValues("Testing %1", { toString: () => "foo" }); // "Testing foo"
1405
1671
  const values = ["foo", "bar", "baz"];
1406
1672
  insertValues("Testing %1, %2, %3 and %4", ...values); // "Testing foo, bar and baz and %4"
1407
1673
  ```
1408
-
1409
1674
  </details>
1410
1675
 
1411
1676
  <br>
@@ -1446,7 +1711,6 @@ const barDeflate = await compress("Hello, World!".repeat(20), "deflate");
1446
1711
  console.log(fooDeflate); // "eJzzSM3JyddRCM8vyklRBAAfngRq"
1447
1712
  console.log(barDeflate); // "eJzzSM3JyddRCM8vyklR9BiZHAAIEVg1"
1448
1713
  ```
1449
-
1450
1714
  </details>
1451
1715
 
1452
1716
  <br>
@@ -1477,7 +1741,6 @@ const decompressed = await decompress(compressed, "gzip");
1477
1741
 
1478
1742
  console.log(decompressed); // "Hello, World!"
1479
1743
  ```
1480
-
1481
1744
  </details>
1482
1745
 
1483
1746
  <br>
@@ -1544,12 +1807,11 @@ randomId(10, 2); // "1010001101" (length 10, radix 2)
1544
1807
  randomId(10, 10); // "0183428506" (length 10, radix 10)
1545
1808
  randomId(10, 36); // "z46jfpa37r" (length 10, radix 36)
1546
1809
  ```
1547
-
1548
1810
  </details>
1549
1811
 
1550
1812
  <br><br>
1551
1813
 
1552
- <!-- #SECTION Arrays -->
1814
+ <!-- #region Arrays -->
1553
1815
  ## Arrays:
1554
1816
 
1555
1817
  ### randomItem()
@@ -1569,7 +1831,6 @@ import { randomItem } from "@sv443-network/userutils";
1569
1831
  randomItem(["foo", "bar", "baz"]); // "bar"
1570
1832
  randomItem([ ]); // undefined
1571
1833
  ```
1572
-
1573
1834
  </details>
1574
1835
 
1575
1836
  <br>
@@ -1596,7 +1857,6 @@ const [item, index] = randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
1596
1857
  // or if you only want the index:
1597
1858
  const [, index] = randomItemIndex(["foo", "bar", "baz"]); // 1
1598
1859
  ```
1599
-
1600
1860
  </details>
1601
1861
 
1602
1862
  <br>
@@ -1619,7 +1879,6 @@ const arr = ["foo", "bar", "baz"];
1619
1879
  takeRandomItem(arr); // "bar"
1620
1880
  console.log(arr); // ["foo", "baz"]
1621
1881
  ```
1622
-
1623
1882
  </details>
1624
1883
 
1625
1884
  <br>
@@ -1645,12 +1904,11 @@ console.log(randomizeArray(foo)); // [4, 5, 2, 1, 6, 3]
1645
1904
 
1646
1905
  console.log(foo); // [1, 2, 3, 4, 5, 6] - original array is not mutated
1647
1906
  ```
1648
-
1649
1907
  </details>
1650
1908
 
1651
1909
  <br><br>
1652
1910
 
1653
- <!-- #SECTION Translation -->
1911
+ <!-- #region Translation -->
1654
1912
  ## Translation:
1655
1913
  This is a very lightweight translation function that can be used to translate simple strings.
1656
1914
  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.
@@ -1698,7 +1956,6 @@ tr.setLanguage("de");
1698
1956
  console.log(tr("welcome")); // "Willkommen"
1699
1957
  console.log(tr("welcome_name", "John")); // "Willkommen, John"
1700
1958
  ```
1701
-
1702
1959
  </details>
1703
1960
 
1704
1961
  <br>
@@ -1770,34 +2027,33 @@ tr.addLanguage("de-AT", {
1770
2027
  // example for custom pluralization:
1771
2028
 
1772
2029
  tr.addLanguage("en", {
1773
- "items_added-0": "Added %1 items to your cart",
1774
- "items_added-1": "Added %1 item to your cart",
1775
- "items_added-n": "Added all %1 items to your cart",
2030
+ "cart_items_added-0": "No items were added to the cart",
2031
+ "cart_items_added-1": "Added %1 item to the cart",
2032
+ "cart_items_added-n": "Added %1 items to the cart",
1776
2033
  });
1777
2034
 
1778
- /** Returns the custom pluralization identifier for the given number of items (or size of Array/NodeList) */
1779
- function pl(num: number | unknown[] | NodeList) {
2035
+ /** Returns the translation key with a custom pluralization identifier added to it for the given number of items (or size of Array/NodeList or anything else with a `length` property) */
2036
+ function pl(key: string, num: number | Array<unknown> | NodeList | { length: number }) {
1780
2037
  if(typeof num !== "number")
1781
2038
  num = num.length;
1782
2039
 
1783
2040
  if(num === 0)
1784
- return "0";
2041
+ return `${key}-0`;
1785
2042
  else if(num === 1)
1786
- return "1";
2043
+ return `${key}-1`;
1787
2044
  else
1788
- return "n";
2045
+ return `${key}-n`;
1789
2046
  };
1790
2047
 
1791
2048
  const items = [];
1792
- tr(`items_added-${pl(items)}`, items.length); // "Added 0 items to your cart"
2049
+ console.log(tr(pl("cart_items_added", items), items.length)); // "No items were added to the cart"
1793
2050
 
1794
2051
  items.push("foo");
1795
- tr(`items_added-${pl(items)}`, items.length); // "Added 1 item to your cart"
2052
+ console.log(tr(pl("cart_items_added", items), items.length)); // "Added 1 item to the cart"
1796
2053
 
1797
2054
  items.push("bar");
1798
- tr(`items_added-${pl(items)}`, items.length); // "Added all 2 items to your cart"
2055
+ console.log(tr(pl("cart_items_added", items), items.length)); // "Added 2 items to the cart"
1799
2056
  ```
1800
-
1801
2057
  </details>
1802
2058
 
1803
2059
  <br>
@@ -1826,7 +2082,101 @@ If no language has been set yet, it will return undefined.
1826
2082
 
1827
2083
  <br><br>
1828
2084
 
1829
- <!-- #SECTION Utility types -->
2085
+ ## Colors:
2086
+ The color functions are used to manipulate and convert colors in various formats.
2087
+
2088
+ ### hexToRgb()
2089
+ Usage:
2090
+ ```ts
2091
+ hexToRgb(hex: string): [red: number, green: number, blue: number]
2092
+ ```
2093
+
2094
+ Converts a hex color string to an RGB color tuple array.
2095
+ Accepts the formats `#RRGGBB` and `#RGB`, with or without the hash symbol.
2096
+
2097
+ <details><summary><b>Example - click to view</b></summary>
2098
+
2099
+ ```ts
2100
+ import { hexToRgb } from "@sv443-network/userutils";
2101
+
2102
+ hexToRgb("#ff0000"); // [255, 0, 0]
2103
+ hexToRgb("0032ef"); // [0, 50, 239]
2104
+ hexToRgb("#0f0"); // [0, 255, 0]
2105
+ ```
2106
+ </details>
2107
+
2108
+ <br>
2109
+
2110
+ ### rgbToHex()
2111
+ Usage:
2112
+ ```ts
2113
+ rgbToHex(red: number, green: number, blue: number, withHash?: boolean, upperCase?: boolean): string
2114
+ ```
2115
+
2116
+ Converts RGB color values to a hex color string.
2117
+ The `withHash` parameter determines if the hash symbol should be included in the output (true by default).
2118
+ The `upperCase` parameter determines if the output should be in uppercase (false by default).
2119
+
2120
+ <details><summary><b>Example - click to view</b></summary>
2121
+
2122
+ ```ts
2123
+ import { rgbToHex } from "@sv443-network/userutils";
2124
+
2125
+ rgbToHex(255, 0, 0); // "#ff0000"
2126
+ rgbToHex(255, 0, 0, false); // "ff0000"
2127
+ rgbToHex(255, 0, 0, true, true); // "#FF0000"
2128
+ ```
2129
+ </details>
2130
+
2131
+ <br>
2132
+
2133
+ ### lightenColor()
2134
+ Usage:
2135
+ ```ts
2136
+ lightenColor(color: string, percent: number): string
2137
+ ```
2138
+
2139
+ Lightens a CSS color value (in hex, RGB or RGBA format) by a given percentage.
2140
+ Will not exceed the maximum range (00-FF or 0-255).
2141
+ Throws an error if the color format is invalid or not supported.
2142
+
2143
+ <details><summary><b>Example - click to view</b></summary>
2144
+
2145
+ ```ts
2146
+ import { lightenColor } from "@sv443-network/userutils";
2147
+
2148
+ lightenColor("#ff0000", 20); // "#ff3333"
2149
+ lightenColor("rgb(0, 255, 0)", 50); // "rgb(128, 255, 128)"
2150
+ lightenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(128, 255, 128, 0.5)"
2151
+ ```
2152
+ </details>
2153
+
2154
+ <br>
2155
+
2156
+ ### darkenColor()
2157
+ Usage:
2158
+ ```ts
2159
+ darkenColor(color: string, percent: number): string
2160
+ ```
2161
+
2162
+ Darkens a CSS color value (in hex, RGB or RGBA format) by a given percentage.
2163
+ Will not exceed the maximum range (00-FF or 0-255).
2164
+ Throws an error if the color format is invalid or not supported.
2165
+
2166
+ <details><summary><b>Example - click to view</b></summary>
2167
+
2168
+ ```ts
2169
+ import { darkenColor } from "@sv443-network/userutils";
2170
+
2171
+ darkenColor("#ff0000", 20); // "#cc0000"
2172
+ darkenColor("rgb(0, 255, 0)", 50); // "rgb(0, 128, 0)"
2173
+ darkenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(0, 128, 0, 0.5)"
2174
+ ```
2175
+ </details>
2176
+
2177
+ <br><br>
2178
+
2179
+ <!-- #region Utility types -->
1830
2180
  ## Utility types:
1831
2181
  UserUtils also offers some utility types that can be used in TypeScript projects.
1832
2182
  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.
@@ -1862,7 +2212,6 @@ logSomething(fooObject); // "Log: hello world"
1862
2212
 
1863
2213
  logSomething(barObject); // Type error
1864
2214
  ```
1865
-
1866
2215
  </details>
1867
2216
 
1868
2217
  <br>
@@ -1893,7 +2242,6 @@ function somethingElse(array: NonEmptyArray) {
1893
2242
 
1894
2243
  logFirstItem(["04abc", "69"]); // 4
1895
2244
  ```
1896
-
1897
2245
  </details>
1898
2246
 
1899
2247
  <br>
@@ -1918,7 +2266,6 @@ function convertToNumber<T extends string>(str: NonEmptyString<T>) {
1918
2266
  convertToNumber("04abc"); // "4"
1919
2267
  convertToNumber(""); // type error: Argument of type 'string' is not assignable to parameter of type 'never'
1920
2268
  ```
1921
-
1922
2269
  </details>
1923
2270
 
1924
2271
  <br>
@@ -1946,12 +2293,11 @@ foo("a"); // included in autocomplete, no type error
1946
2293
  foo(""); // *not* included in autocomplete, still no type error
1947
2294
  foo(1); // type error: Argument of type '1' is not assignable to parameter of type 'LooseUnion<"a" | "b" | "c">'
1948
2295
  ```
1949
-
1950
2296
  </details>
1951
2297
 
1952
2298
  <br><br><br><br>
1953
2299
 
1954
- <!-- #MARKER Footer -->
2300
+ <!-- #region Footer -->
1955
2301
  <div style="text-align: center;" align="center">
1956
2302
 
1957
2303
  Made with ❤️ by [Sv443](https://github.com/Sv443)