@sv443-network/userutils 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @sv443-network/userutils
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 885323d: Added function `observeElementProp` to allow observing element property changes
8
+
9
+ ## 4.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - dae5450: Removed `amplifyMedia` function due to massive inconsistencies in sound quality
14
+
15
+ ### Minor Changes
16
+
17
+ - 168c2aa: Added functions `compress` and `decompress` to compress and decompress strings using gzip or deflate
18
+ - 49bc85e: Added utility types `NonEmptyString` and `LooseUnion`
19
+
20
+ ### Patch Changes
21
+
22
+ - 2ae665d: fixed wrong TS type for SelectorObserver options in constructor
23
+
3
24
  ## 3.0.0
4
25
 
5
26
  ### Major Changes
package/README.md CHANGED
@@ -4,13 +4,15 @@
4
4
  ## UserUtils
5
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.
6
6
 
7
- Contains builtin TypeScript declarations. Fully web compatible and supports ESM, CJS and global imports.
7
+ Contains builtin TypeScript declarations. Fully web compatible and supports ESM and CJS imports and global declaration.
8
8
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
9
9
 
10
10
  <br>
11
+ <sub>
11
12
 
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
+ View the documentation of previous major releases: [3.0.0](https://github.com/Sv443-Network/UserUtils/blob/v3.0.0/README.md), [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
14
 
15
+ </sub>
14
16
  </div>
15
17
  <br>
16
18
 
@@ -30,8 +32,8 @@ View the documentation of previous major releases: [2.0.1](https://github.com/Sv
30
32
  - [openInNewTab()](#openinnewtab) - open a link in a new tab
31
33
  - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
32
34
  - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
33
- - [amplifyMedia()](#amplifymedia) - amplify an audio or video element's volume past the maximum of 100%
34
35
  - [isScrollable()](#isscrollable) - check if an element has a horizontal or vertical scroll bar
36
+ - [observeElementProp()](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
35
37
  - [**Math:**](#math)
36
38
  - [clamp()](#clamp) - constrain a number between a min and max value
37
39
  - [mapRange()](#maprange) - map a number from one range to the same spot in another range
@@ -44,6 +46,8 @@ View the documentation of previous major releases: [2.0.1](https://github.com/Sv
44
46
  - [debounce()](#debounce) - call a function only once, after a given amount of time
45
47
  - [fetchAdvanced()](#fetchadvanced) - wrapper around the fetch API with a timeout option
46
48
  - [insertValues()](#insertvalues) - insert values into a string at specified placeholders
49
+ - [compress()](#compress) - compress a string with Gzip or Deflate
50
+ - [decompress()](#decompress) - decompress a previously compressed string
47
51
  - [**Arrays:**](#arrays)
48
52
  - [randomItem()](#randomitem) - returns a random item from an array
49
53
  - [randomItemIndex()](#randomitemindex) - returns a tuple of a random item and its index from an array
@@ -56,7 +60,9 @@ View the documentation of previous major releases: [2.0.1](https://github.com/Sv
56
60
  - [tr.getLanguage()](#trgetlanguage) - returns the currently active language
57
61
  - [**Utility types for TypeScript:**](#utility-types)
58
62
  - [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
63
+ - [NonEmptyArray](#nonemptyarray) - any array that should have at least one item
64
+ - [NonEmptyString](#nonemptystring) - any string that should have at least one character
65
+ - [LooseUnion](#looseunion) - a union that gives autocomplete in the IDE but also allows any other value of the same type
60
66
 
61
67
  <br><br>
62
68
 
@@ -647,120 +653,78 @@ interceptWindowEvent("beforeunload");
647
653
 
648
654
  <br>
649
655
 
650
- ### amplifyMedia()
656
+ ### isScrollable()
651
657
  Usage:
652
658
  ```ts
653
- amplifyMedia(mediaElement: HTMLMediaElement, initialGain?: number): AmplifyMediaResult
659
+ isScrollable(element: Element): { horizontal: boolean, vertical: boolean }
654
660
  ```
655
661
 
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.
659
-
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.
662
-
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`
667
-
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.
669
- ⚠️ Make sure to call the returned function `enable()` after calling this function to actually enable the amplification.
670
-
671
- The returned object of the type `AmplifyMediaResult` has the following properties:
672
- **Important properties:**
673
- | Property | Description |
674
- | :-- | :-- |
675
- | `enable()` | Call to enable the amplification for the first time or re-enable it if it was disabled before |
676
- | `disable()` | Call to disable amplification |
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 |
662
+ Checks if an element has a horizontal or vertical scroll bar.
663
+ This uses the computed style of the element, so it will also work if the element is hidden.
687
664
 
688
- <br>
689
-
690
665
  <details><summary><b>Example - click to view</b></summary>
691
666
 
692
667
  ```ts
693
- import { amplifyMedia, clamp } from "@sv443-network/userutils";
694
- import type { AmplifyMediaResult } from "@sv443-network/userutils";
695
-
696
- const audioElement = document.querySelector<HTMLAudioElement>("audio");
697
-
698
- let ampResult: AmplifyMediaResult | undefined;
699
-
700
- function updateGainValue(gainValue: number) {
701
- if(!ampResult)
702
- return;
703
- // constrain the value to between 0 and 3 for safety
704
- ampResult.setGain(clamp(gainValue, 0, 3));
705
-
706
- console.log("Gain set to", ampResult.getGain());
707
- }
708
-
709
-
710
- const amplifyButton = document.querySelector<HTMLButtonElement>("button#amplify");
711
-
712
- // amplifyMedia() needs to be called in response to a user interaction event:
713
- amplifyButton.addEventListener("click", () => {
714
- // only needs to be initialized once, afterwards the returned object
715
- // can be used to change settings and enable/disable the amplification
716
- if(!ampResult) {
717
- // initialize amplification and set it to ~2x
718
- ampResult = amplifyMedia(audioElement, 2.0);
719
- }
720
- if(!ampResult.enabled) {
721
- // enable the amplification
722
- ampResult.enable();
723
- }
724
-
725
- updateGainValue(3.5); // try to set gain to ~3.5x
726
-
727
- console.log(ampResult.getGain()); // 3.0 (because of the clamp())
728
- });
729
-
668
+ import { isScrollable } from "@sv443-network/userutils";
730
669
 
731
- const disableButton = document.querySelector<HTMLButtonElement>("button#disable");
670
+ const element = document.querySelector("#element");
671
+ const { horizontal, vertical } = isScrollable(element);
732
672
 
733
- disableButton.addEventListener("click", () => {
734
- if(ampResult) {
735
- // disable the amplification
736
- ampResult.disable();
737
- }
738
- });
673
+ console.log("Element has a horizontal scroll bar:", horizontal);
674
+ console.log("Element has a vertical scroll bar:", vertical);
739
675
  ```
740
676
 
741
677
  </details>
742
678
 
743
679
  <br>
744
680
 
745
- ### isScrollable()
681
+ ### observeElementProp()
746
682
  Usage:
747
683
  ```ts
748
- isScrollable(element: Element): { horizontal: boolean, vertical: boolean }
684
+ observeElementProp(
685
+ element: Element,
686
+ property: string,
687
+ callback: (oldValue: any, newValue: any) => void
688
+ ): void
749
689
  ```
750
690
 
751
- Checks if an element has a horizontal or vertical scroll bar.
752
- This uses the computed style of the element, so it will also work if the element is hidden.
691
+ This function observes changes to the given property of a given element.
692
+ While regular HTML attributes can be observed using a MutationObserver, this is not always possible for properties that are assigned on the JS object.
693
+ This function shims the setter of the provided property and calls the callback function whenever it is changed through any means.
694
+
695
+ When using TypeScript, the types for `element`, `property` and the arguments for `callback` will be automatically inferred.
753
696
 
754
697
  <details><summary><b>Example - click to view</b></summary>
755
698
 
756
699
  ```ts
757
- import { isScrollable } from "@sv443-network/userutils";
700
+ import { observeElementProp } from "@sv443-network/userutils";
758
701
 
759
- const element = document.querySelector("#element");
760
- const { horizontal, vertical } = isScrollable(element);
702
+ const myInput = document.querySelector("input#my-input");
761
703
 
762
- console.log("Element has a horizontal scroll bar:", horizontal);
763
- console.log("Element has a vertical scroll bar:", vertical);
704
+ let value = 0;
705
+
706
+ setInterval(() => {
707
+ value += 1;
708
+ myInput.value = String(value);
709
+ }, 1000);
710
+
711
+
712
+ const observer = new MutationObserver((mutations) => {
713
+ // will never be called:
714
+ console.log("MutationObserver mutation:", mutations);
715
+ });
716
+
717
+ // one would think this should work, but "value" is a JS object *property*, not a DOM *attribute*
718
+ observer.observe(myInput, {
719
+ attributes: true,
720
+ attributeFilter: ["value"],
721
+ });
722
+
723
+
724
+ observeElementProp(myInput, "value", (oldValue, newValue) => {
725
+ // will be called every time the value changes:
726
+ console.log("Value changed from", oldValue, "to", newValue);
727
+ });
764
728
  ```
765
729
 
766
730
  </details>
@@ -1121,6 +1085,106 @@ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
1121
1085
 
1122
1086
  </details>
1123
1087
 
1088
+ <br>
1089
+
1090
+ ### insertValues()
1091
+ Usage:
1092
+ ```ts
1093
+ insertValues(input: string, ...values: Stringifiable[]): string
1094
+ ```
1095
+
1096
+ Inserts values into a string in the format `%n`, where `n` is the number of the value, starting at 1.
1097
+ The values will be stringified using `toString()` (see [Stringifiable](#stringifiable)) before being inserted into the input string.
1098
+ If not enough values are passed, the remaining placeholders will be left untouched.
1099
+
1100
+ <details><summary><b>Example - click to view</b></summary>
1101
+
1102
+ ```ts
1103
+ import { insertValues } from "@sv443-network/userutils";
1104
+
1105
+ insertValues("Hello, %1!", "World"); // "Hello, World!"
1106
+ insertValues("Hello, %1! My name is %2.", "World", "John"); // "Hello, World! My name is John."
1107
+ insertValues("Testing %1", { toString: () => "foo" }); // "Testing foo"
1108
+
1109
+ // using an array for the values and not passing enough arguments:
1110
+ const values = ["foo", "bar", "baz"];
1111
+ insertValues("Testing %1, %2, %3 and %4", ...values); // "Testing foo, bar and baz and %4"
1112
+ ```
1113
+
1114
+ </details>
1115
+
1116
+ <br>
1117
+
1118
+ ### compress()
1119
+ Usage:
1120
+ ```ts
1121
+ compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "base64"): Promise<string>
1122
+ compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
1123
+ ```
1124
+
1125
+ Compresses a string or ArrayBuffer using the specified compression format. Most browsers should support at least `gzip` and `deflate`
1126
+ The `outputType` dictates which format the output will be in. It will default to `base64` if left undefined.
1127
+
1128
+ ⚠️ You need to provide the `@grant unsafeWindow` directive if you are using the `base64` output type or you will get a TypeError.
1129
+ ⚠️ Not all browsers might support compression. Please check [on this page](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream#browser_compatibility) for compatibility and supported compression formats.
1130
+
1131
+ <details><summary><b>Example - click to view</b></summary>
1132
+
1133
+ ```ts
1134
+ import { compress } from "@sv443-network/userutils";
1135
+
1136
+ // using gzip:
1137
+
1138
+ const fooGz = await compress("Hello, World!", "gzip");
1139
+ const barGz = await compress("Hello, World!".repeat(20), "gzip");
1140
+
1141
+ // not as efficient with short strings but can save quite a lot of space with larger strings:
1142
+ console.log(fooGz); // "H4sIAAAAAAAAE/NIzcnJ11EIzy/KSVEEANDDSuwNAAAA"
1143
+ console.log(barGz); // "H4sIAAAAAAAAE/NIzcnJ11EIzy/KSVH0GJkcAKOPcmYEAQAA"
1144
+
1145
+ // depending on the type of data you might want to use a different compression format like deflate:
1146
+
1147
+ const fooDeflate = await compress("Hello, World!", "deflate");
1148
+ const barDeflate = await compress("Hello, World!".repeat(20), "deflate");
1149
+
1150
+ // again, it's not as efficient initially but gets better with longer inputs:
1151
+ console.log(fooDeflate); // "eJzzSM3JyddRCM8vyklRBAAfngRq"
1152
+ console.log(barDeflate); // "eJzzSM3JyddRCM8vyklR9BiZHAAIEVg1"
1153
+ ```
1154
+
1155
+ </details>
1156
+
1157
+ <br>
1158
+
1159
+ ### decompress()
1160
+ Usage:
1161
+ ```ts
1162
+ decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>
1163
+ decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>
1164
+ ```
1165
+
1166
+ Decompresses a string or ArrayBuffer that has been previously [compressed](#compress) using the specified compression format. Most browsers should support at least `gzip` and `deflate`
1167
+ The `outputType` dictates which format the output will be in. It will default to `string` if left undefined.
1168
+
1169
+ ⚠️ You need to provide the `@grant unsafeWindow` directive if you are using the `string` output type or you will get a TypeError.
1170
+ ⚠️ Not all browsers might support decompression. Please check [on this page](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream#browser_compatibility) for compatibility and supported compression formats.
1171
+
1172
+ <details><summary><b>Example - click to view</b></summary>
1173
+
1174
+ ```ts
1175
+ import { compress, decompress } from "@sv443-network/userutils";
1176
+
1177
+ const compressed = await compress("Hello, World!".repeat(20), "gzip");
1178
+
1179
+ console.log(compressed); // "H4sIAAAAAAAAE/NIzcnJ11EIzy/KSVH0GJkcAKOPcmYEAQAA"
1180
+
1181
+ const decompressed = await decompress(compressed, "gzip");
1182
+
1183
+ console.log(decompressed); // "Hello, World!"
1184
+ ```
1185
+
1186
+ </details>
1187
+
1124
1188
  <br><br>
1125
1189
 
1126
1190
  <!-- #SECTION Arrays -->
@@ -1442,6 +1506,11 @@ logSomething(barObject); // Type Error
1442
1506
  <br>
1443
1507
 
1444
1508
  ## NonEmptyArray
1509
+ Usage:
1510
+ ```ts
1511
+ NonEmptyArray<TItem = unknown>
1512
+ ```
1513
+
1445
1514
  This type describes an array that has at least one item.
1446
1515
  Use the generic parameter to specify the type of the items in the array.
1447
1516
 
@@ -1465,6 +1534,59 @@ logFirstItem(["04abc", "69"]); // 4
1465
1534
 
1466
1535
  </details>
1467
1536
 
1537
+ <br>
1538
+
1539
+ ## NonEmptyString
1540
+ Usage:
1541
+ ```ts
1542
+ NonEmptyString<TString extends string>
1543
+ ```
1544
+
1545
+ This type describes a string that has at least one character.
1546
+
1547
+ <details><summary><b>Example - click to view</b></summary>
1548
+
1549
+ ```ts
1550
+ import type { NonEmptyString } from "@sv443-network/userutils";
1551
+
1552
+ function convertToNumber<T extends string>(str: NonEmptyString<T>) {
1553
+ console.log(parseInt(str));
1554
+ }
1555
+
1556
+ convertToNumber("04abc"); // "4"
1557
+ convertToNumber(""); // type error: Argument of type 'string' is not assignable to parameter of type 'never'
1558
+ ```
1559
+
1560
+ </details>
1561
+
1562
+ <br>
1563
+
1564
+ ## LooseUnion
1565
+ Usage:
1566
+ ```ts
1567
+ LooseUnion<TUnion extends string | number | object>
1568
+ ```
1569
+
1570
+ A type that offers autocomplete in the IDE for the passed union but also allows any value of the same type to be passed.
1571
+ Supports unions of strings, numbers and objects.
1572
+
1573
+ <details><summary><b>Example - click to view</b></summary>
1574
+
1575
+ ```ts
1576
+ function foo(bar: LooseUnion<"a" | "b" | "c">) {
1577
+ console.log(bar);
1578
+ }
1579
+
1580
+ // when typing the following, autocomplete suggests "a", "b" and "c"
1581
+ // foo("
1582
+
1583
+ foo("a"); // included in autocomplete, no type error
1584
+ foo(""); // *not* included in autocomplete, still no type error
1585
+ foo(1); // type error: Argument of type '1' is not assignable to parameter of type 'LooseUnion<"a" | "b" | "c">'
1586
+ ```
1587
+
1588
+ </details>
1589
+
1468
1590
  <br><br><br><br>
1469
1591
 
1470
1592
  <!-- #MARKER Footer -->
@@ -9,7 +9,7 @@
9
9
  // ==UserLibrary==
10
10
  // @name UserUtils
11
11
  // @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
12
- // @version 3.0.0
12
+ // @version 4.1.0
13
13
  // @license MIT
14
14
  // @copyright Sv443 (https://github.com/Sv443)
15
15
 
@@ -326,43 +326,6 @@ var UserUtils = (function (exports) {
326
326
  function interceptWindowEvent(eventName, predicate = () => true) {
327
327
  return interceptEvent(getUnsafeWindow(), eventName, predicate);
328
328
  }
329
- function amplifyMedia(mediaElement, initialGain = 1) {
330
- const context = new (window.AudioContext || window.webkitAudioContext)();
331
- const props = {
332
- context,
333
- sourceNode: context.createMediaElementSource(mediaElement),
334
- gainNode: context.createGain(),
335
- /** Sets the gain of the amplifying GainNode */
336
- setGain(gain) {
337
- props.gainNode.gain.value = gain;
338
- },
339
- /** Returns the current gain of the amplifying GainNode */
340
- getGain() {
341
- return props.gainNode.gain.value;
342
- },
343
- /** Whether the amplification is currently enabled */
344
- enabled: false,
345
- /** Enable the amplification for the first time or if it was disabled before */
346
- enable() {
347
- if (props.enabled)
348
- return;
349
- props.enabled = true;
350
- props.sourceNode.connect(props.gainNode);
351
- props.gainNode.connect(props.context.destination);
352
- },
353
- /** Disable the amplification */
354
- disable() {
355
- if (!props.enabled)
356
- return;
357
- props.enabled = false;
358
- props.sourceNode.disconnect(props.gainNode);
359
- props.gainNode.disconnect(props.context.destination);
360
- props.sourceNode.connect(props.context.destination);
361
- }
362
- };
363
- props.setGain(initialGain);
364
- return props;
365
- }
366
329
  function isScrollable(element) {
367
330
  const { overflowX, overflowY } = getComputedStyle(element);
368
331
  return {
@@ -370,6 +333,28 @@ var UserUtils = (function (exports) {
370
333
  horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
371
334
  };
372
335
  }
336
+ function observeElementProp(element, property, callback) {
337
+ const elementPrototype = Object.getPrototypeOf(element);
338
+ if (elementPrototype.hasOwnProperty(property)) {
339
+ const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
340
+ Object.defineProperty(element, property, {
341
+ get: function() {
342
+ var _a;
343
+ return (_a = descriptor == null ? void 0 : descriptor.get) == null ? void 0 : _a.apply(this, arguments);
344
+ },
345
+ set: function() {
346
+ var _a;
347
+ const oldValue = this[property];
348
+ (_a = descriptor == null ? void 0 : descriptor.set) == null ? void 0 : _a.apply(this, arguments);
349
+ const newValue = this[property];
350
+ if (typeof callback === "function") {
351
+ callback.bind(this, oldValue, newValue);
352
+ }
353
+ return newValue;
354
+ }
355
+ });
356
+ }
357
+ }
373
358
 
374
359
  // lib/misc.ts
375
360
  function autoPlural(word, num) {
@@ -401,13 +386,43 @@ var UserUtils = (function (exports) {
401
386
  return res;
402
387
  });
403
388
  }
404
- function insertValues(str, ...values) {
405
- return str.replace(/%\d/gm, (match) => {
389
+ function insertValues(input, ...values) {
390
+ return input.replace(/%\d/gm, (match) => {
406
391
  var _a, _b;
407
392
  const argIndex = Number(match.substring(1)) - 1;
408
393
  return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
409
394
  });
410
395
  }
396
+ function compress(input, compressionFormat, outputType = "base64") {
397
+ return __async(this, null, function* () {
398
+ const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
399
+ const comp = new CompressionStream(compressionFormat);
400
+ const writer = comp.writable.getWriter();
401
+ writer.write(byteArray);
402
+ writer.close();
403
+ const buf = yield new Response(comp.readable).arrayBuffer();
404
+ return outputType === "arrayBuffer" ? buf : ab2str(buf);
405
+ });
406
+ }
407
+ function decompress(input, compressionFormat, outputType = "string") {
408
+ return __async(this, null, function* () {
409
+ const byteArray = typeof input === "string" ? str2ab(input) : input;
410
+ const decomp = new DecompressionStream(compressionFormat);
411
+ const writer = decomp.writable.getWriter();
412
+ writer.write(byteArray);
413
+ writer.close();
414
+ const buf = yield new Response(decomp.readable).arrayBuffer();
415
+ return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
416
+ });
417
+ }
418
+ function ab2str(buf) {
419
+ return getUnsafeWindow().btoa(
420
+ new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
421
+ );
422
+ }
423
+ function str2ab(str) {
424
+ return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
425
+ }
411
426
 
412
427
  // lib/SelectorObserver.ts
413
428
  var SelectorObserver = class {
@@ -579,10 +594,11 @@ var UserUtils = (function (exports) {
579
594
  exports.SelectorObserver = SelectorObserver;
580
595
  exports.addGlobalStyle = addGlobalStyle;
581
596
  exports.addParent = addParent;
582
- exports.amplifyMedia = amplifyMedia;
583
597
  exports.autoPlural = autoPlural;
584
598
  exports.clamp = clamp;
599
+ exports.compress = compress;
585
600
  exports.debounce = debounce;
601
+ exports.decompress = decompress;
586
602
  exports.fetchAdvanced = fetchAdvanced;
587
603
  exports.getUnsafeWindow = getUnsafeWindow;
588
604
  exports.insertAfter = insertAfter;
@@ -591,6 +607,7 @@ var UserUtils = (function (exports) {
591
607
  exports.interceptWindowEvent = interceptWindowEvent;
592
608
  exports.isScrollable = isScrollable;
593
609
  exports.mapRange = mapRange;
610
+ exports.observeElementProp = observeElementProp;
594
611
  exports.openInNewTab = openInNewTab;
595
612
  exports.pauseFor = pauseFor;
596
613
  exports.preloadImages = preloadImages;
package/dist/index.js CHANGED
@@ -305,43 +305,6 @@ function interceptEvent(eventObject, eventName, predicate = () => true) {
305
305
  function interceptWindowEvent(eventName, predicate = () => true) {
306
306
  return interceptEvent(getUnsafeWindow(), eventName, predicate);
307
307
  }
308
- function amplifyMedia(mediaElement, initialGain = 1) {
309
- const context = new (window.AudioContext || window.webkitAudioContext)();
310
- const props = {
311
- context,
312
- sourceNode: context.createMediaElementSource(mediaElement),
313
- gainNode: context.createGain(),
314
- /** Sets the gain of the amplifying GainNode */
315
- setGain(gain) {
316
- props.gainNode.gain.value = gain;
317
- },
318
- /** Returns the current gain of the amplifying GainNode */
319
- getGain() {
320
- return props.gainNode.gain.value;
321
- },
322
- /** Whether the amplification is currently enabled */
323
- enabled: false,
324
- /** Enable the amplification for the first time or if it was disabled before */
325
- enable() {
326
- if (props.enabled)
327
- return;
328
- props.enabled = true;
329
- props.sourceNode.connect(props.gainNode);
330
- props.gainNode.connect(props.context.destination);
331
- },
332
- /** Disable the amplification */
333
- disable() {
334
- if (!props.enabled)
335
- return;
336
- props.enabled = false;
337
- props.sourceNode.disconnect(props.gainNode);
338
- props.gainNode.disconnect(props.context.destination);
339
- props.sourceNode.connect(props.context.destination);
340
- }
341
- };
342
- props.setGain(initialGain);
343
- return props;
344
- }
345
308
  function isScrollable(element) {
346
309
  const { overflowX, overflowY } = getComputedStyle(element);
347
310
  return {
@@ -349,6 +312,28 @@ function isScrollable(element) {
349
312
  horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
350
313
  };
351
314
  }
315
+ function observeElementProp(element, property, callback) {
316
+ const elementPrototype = Object.getPrototypeOf(element);
317
+ if (elementPrototype.hasOwnProperty(property)) {
318
+ const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
319
+ Object.defineProperty(element, property, {
320
+ get: function() {
321
+ var _a;
322
+ return (_a = descriptor == null ? void 0 : descriptor.get) == null ? void 0 : _a.apply(this, arguments);
323
+ },
324
+ set: function() {
325
+ var _a;
326
+ const oldValue = this[property];
327
+ (_a = descriptor == null ? void 0 : descriptor.set) == null ? void 0 : _a.apply(this, arguments);
328
+ const newValue = this[property];
329
+ if (typeof callback === "function") {
330
+ callback.bind(this, oldValue, newValue);
331
+ }
332
+ return newValue;
333
+ }
334
+ });
335
+ }
336
+ }
352
337
 
353
338
  // lib/misc.ts
354
339
  function autoPlural(word, num) {
@@ -380,13 +365,43 @@ function fetchAdvanced(_0) {
380
365
  return res;
381
366
  });
382
367
  }
383
- function insertValues(str, ...values) {
384
- return str.replace(/%\d/gm, (match) => {
368
+ function insertValues(input, ...values) {
369
+ return input.replace(/%\d/gm, (match) => {
385
370
  var _a, _b;
386
371
  const argIndex = Number(match.substring(1)) - 1;
387
372
  return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
388
373
  });
389
374
  }
375
+ function compress(input, compressionFormat, outputType = "base64") {
376
+ return __async(this, null, function* () {
377
+ const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
378
+ const comp = new CompressionStream(compressionFormat);
379
+ const writer = comp.writable.getWriter();
380
+ writer.write(byteArray);
381
+ writer.close();
382
+ const buf = yield new Response(comp.readable).arrayBuffer();
383
+ return outputType === "arrayBuffer" ? buf : ab2str(buf);
384
+ });
385
+ }
386
+ function decompress(input, compressionFormat, outputType = "string") {
387
+ return __async(this, null, function* () {
388
+ const byteArray = typeof input === "string" ? str2ab(input) : input;
389
+ const decomp = new DecompressionStream(compressionFormat);
390
+ const writer = decomp.writable.getWriter();
391
+ writer.write(byteArray);
392
+ writer.close();
393
+ const buf = yield new Response(decomp.readable).arrayBuffer();
394
+ return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
395
+ });
396
+ }
397
+ function ab2str(buf) {
398
+ return getUnsafeWindow().btoa(
399
+ new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
400
+ );
401
+ }
402
+ function str2ab(str) {
403
+ return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
404
+ }
390
405
 
391
406
  // lib/SelectorObserver.ts
392
407
  var SelectorObserver = class {
@@ -558,10 +573,11 @@ exports.ConfigManager = ConfigManager;
558
573
  exports.SelectorObserver = SelectorObserver;
559
574
  exports.addGlobalStyle = addGlobalStyle;
560
575
  exports.addParent = addParent;
561
- exports.amplifyMedia = amplifyMedia;
562
576
  exports.autoPlural = autoPlural;
563
577
  exports.clamp = clamp;
578
+ exports.compress = compress;
564
579
  exports.debounce = debounce;
580
+ exports.decompress = decompress;
565
581
  exports.fetchAdvanced = fetchAdvanced;
566
582
  exports.getUnsafeWindow = getUnsafeWindow;
567
583
  exports.insertAfter = insertAfter;
@@ -570,6 +586,7 @@ exports.interceptEvent = interceptEvent;
570
586
  exports.interceptWindowEvent = interceptWindowEvent;
571
587
  exports.isScrollable = isScrollable;
572
588
  exports.mapRange = mapRange;
589
+ exports.observeElementProp = observeElementProp;
573
590
  exports.openInNewTab = openInNewTab;
574
591
  exports.pauseFor = pauseFor;
575
592
  exports.preloadImages = preloadImages;
package/dist/index.mjs CHANGED
@@ -303,43 +303,6 @@ function interceptEvent(eventObject, eventName, predicate = () => true) {
303
303
  function interceptWindowEvent(eventName, predicate = () => true) {
304
304
  return interceptEvent(getUnsafeWindow(), eventName, predicate);
305
305
  }
306
- function amplifyMedia(mediaElement, initialGain = 1) {
307
- const context = new (window.AudioContext || window.webkitAudioContext)();
308
- const props = {
309
- context,
310
- sourceNode: context.createMediaElementSource(mediaElement),
311
- gainNode: context.createGain(),
312
- /** Sets the gain of the amplifying GainNode */
313
- setGain(gain) {
314
- props.gainNode.gain.value = gain;
315
- },
316
- /** Returns the current gain of the amplifying GainNode */
317
- getGain() {
318
- return props.gainNode.gain.value;
319
- },
320
- /** Whether the amplification is currently enabled */
321
- enabled: false,
322
- /** Enable the amplification for the first time or if it was disabled before */
323
- enable() {
324
- if (props.enabled)
325
- return;
326
- props.enabled = true;
327
- props.sourceNode.connect(props.gainNode);
328
- props.gainNode.connect(props.context.destination);
329
- },
330
- /** Disable the amplification */
331
- disable() {
332
- if (!props.enabled)
333
- return;
334
- props.enabled = false;
335
- props.sourceNode.disconnect(props.gainNode);
336
- props.gainNode.disconnect(props.context.destination);
337
- props.sourceNode.connect(props.context.destination);
338
- }
339
- };
340
- props.setGain(initialGain);
341
- return props;
342
- }
343
306
  function isScrollable(element) {
344
307
  const { overflowX, overflowY } = getComputedStyle(element);
345
308
  return {
@@ -347,6 +310,28 @@ function isScrollable(element) {
347
310
  horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
348
311
  };
349
312
  }
313
+ function observeElementProp(element, property, callback) {
314
+ const elementPrototype = Object.getPrototypeOf(element);
315
+ if (elementPrototype.hasOwnProperty(property)) {
316
+ const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
317
+ Object.defineProperty(element, property, {
318
+ get: function() {
319
+ var _a;
320
+ return (_a = descriptor == null ? void 0 : descriptor.get) == null ? void 0 : _a.apply(this, arguments);
321
+ },
322
+ set: function() {
323
+ var _a;
324
+ const oldValue = this[property];
325
+ (_a = descriptor == null ? void 0 : descriptor.set) == null ? void 0 : _a.apply(this, arguments);
326
+ const newValue = this[property];
327
+ if (typeof callback === "function") {
328
+ callback.bind(this, oldValue, newValue);
329
+ }
330
+ return newValue;
331
+ }
332
+ });
333
+ }
334
+ }
350
335
 
351
336
  // lib/misc.ts
352
337
  function autoPlural(word, num) {
@@ -378,13 +363,43 @@ function fetchAdvanced(_0) {
378
363
  return res;
379
364
  });
380
365
  }
381
- function insertValues(str, ...values) {
382
- return str.replace(/%\d/gm, (match) => {
366
+ function insertValues(input, ...values) {
367
+ return input.replace(/%\d/gm, (match) => {
383
368
  var _a, _b;
384
369
  const argIndex = Number(match.substring(1)) - 1;
385
370
  return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
386
371
  });
387
372
  }
373
+ function compress(input, compressionFormat, outputType = "base64") {
374
+ return __async(this, null, function* () {
375
+ const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
376
+ const comp = new CompressionStream(compressionFormat);
377
+ const writer = comp.writable.getWriter();
378
+ writer.write(byteArray);
379
+ writer.close();
380
+ const buf = yield new Response(comp.readable).arrayBuffer();
381
+ return outputType === "arrayBuffer" ? buf : ab2str(buf);
382
+ });
383
+ }
384
+ function decompress(input, compressionFormat, outputType = "string") {
385
+ return __async(this, null, function* () {
386
+ const byteArray = typeof input === "string" ? str2ab(input) : input;
387
+ const decomp = new DecompressionStream(compressionFormat);
388
+ const writer = decomp.writable.getWriter();
389
+ writer.write(byteArray);
390
+ writer.close();
391
+ const buf = yield new Response(decomp.readable).arrayBuffer();
392
+ return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
393
+ });
394
+ }
395
+ function ab2str(buf) {
396
+ return getUnsafeWindow().btoa(
397
+ new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
398
+ );
399
+ }
400
+ function str2ab(str) {
401
+ return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
402
+ }
388
403
 
389
404
  // lib/SelectorObserver.ts
390
405
  var SelectorObserver = class {
@@ -552,4 +567,4 @@ tr.getLanguage = () => {
552
567
  return curLang;
553
568
  };
554
569
 
555
- export { ConfigManager, SelectorObserver, addGlobalStyle, addParent, amplifyMedia, autoPlural, clamp, debounce, fetchAdvanced, getUnsafeWindow, insertAfter, insertValues, interceptEvent, interceptWindowEvent, isScrollable, mapRange, openInNewTab, pauseFor, preloadImages, randRange, randomId, randomItem, randomItemIndex, randomizeArray, takeRandomItem, tr };
570
+ export { ConfigManager, SelectorObserver, addGlobalStyle, addParent, autoPlural, clamp, compress, debounce, decompress, fetchAdvanced, getUnsafeWindow, insertAfter, insertValues, interceptEvent, interceptWindowEvent, isScrollable, mapRange, observeElementProp, openInNewTab, pauseFor, preloadImages, randRange, randomId, randomItem, randomItemIndex, randomizeArray, takeRandomItem, tr };
@@ -34,13 +34,13 @@ export declare class SelectorObserver {
34
34
  * @param baseElementSelector The selector of the element to observe
35
35
  * @param options Fine-tune what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
36
36
  */
37
- constructor(baseElementSelector: string, options: SelectorObserverOptions);
37
+ constructor(baseElementSelector: string, options?: SelectorObserverOptions);
38
38
  /**
39
39
  * Creates a new SelectorObserver that will observe the children of the given base element for changes (only creation and deletion of elements by default)
40
40
  * @param baseElement The element to observe
41
41
  * @param options Fine-tune what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
42
42
  */
43
- constructor(baseElement: Element, options: SelectorObserverOptions);
43
+ constructor(baseElement: Element, options?: SelectorObserverOptions);
44
44
  private checkAllSelectors;
45
45
  private checkSelector;
46
46
  private debounce;
@@ -1,13 +1,13 @@
1
1
  /** Describes an array with at least one item */
2
- export type NonEmptyArray<T = unknown> = [T, ...T[]];
2
+ export type NonEmptyArray<TArray = unknown> = [TArray, ...TArray[]];
3
3
  /** Returns a random item from the passed array */
4
- export declare function randomItem<T = unknown>(array: T[]): T | undefined;
4
+ export declare function randomItem<TItem = unknown>(array: TItem[]): TItem | undefined;
5
5
  /**
6
6
  * Returns a tuple of a random item and its index from the passed array
7
7
  * Returns `[undefined, undefined]` if the passed array is empty
8
8
  */
9
- export declare function randomItemIndex<T = unknown>(array: T[]): [item?: T, index?: number];
9
+ export declare function randomItemIndex<TItem = unknown>(array: TItem[]): [item?: TItem, index?: number];
10
10
  /** Returns a random item from the passed array and mutates the array to remove the item */
11
- export declare function takeRandomItem<T = unknown>(arr: T[]): T | undefined;
11
+ export declare function takeRandomItem<TItem = unknown>(arr: TItem[]): TItem | undefined;
12
12
  /** Returns a copy of the array with its items in a random order */
13
- export declare function randomizeArray<T = unknown>(array: T[]): T[];
13
+ export declare function randomizeArray<TItem = unknown>(array: TItem[]): TItem[];
package/dist/lib/dom.d.ts CHANGED
@@ -45,55 +45,18 @@ export declare function interceptEvent<TEvtObj extends EventTarget, TPredicateEv
45
45
  * Calling this function will set the `Error.stackTraceLimit` to 1000 to ensure the stack trace is preserved.
46
46
  */
47
47
  export declare function interceptWindowEvent<TEvtKey extends keyof WindowEventMap>(eventName: TEvtKey, predicate?: (event: WindowEventMap[TEvtKey]) => boolean): void;
48
- /** An object which contains the results of {@linkcode amplifyMedia()} */
49
- export type AmplifyMediaResult = ReturnType<typeof amplifyMedia>;
50
- /**
51
- * Amplifies the gain of the passed media element's audio by the specified value.
52
- * This function supports any MediaElement instance like `<audio>` or `<video>`
53
- *
54
- * This is the audio processing workflow:
55
- * `MediaElement (source)` => `GainNode (amplification)` => `destination`
56
- *
57
- * ⚠️ 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.
58
- * ⚠️ Make sure to call the returned function `enable()` after calling this function to actually enable the amplification.
59
- * ⚠️ You should implement a safety limit by using the [`clamp()`](https://github.com/Sv443-Network/UserUtils#clamp) function to prevent any accidental bleeding eardrums.
60
- *
61
- * @param mediaElement The media element to amplify (e.g. `<audio>` or `<video>`)
62
- * @param initialGain The initial gain to apply to the GainNode responsible for volume amplification (floating point number, default is `1.0`)
63
- * @returns Returns an object with the following properties:
64
- * **Important properties:**
65
- * | Property | Description |
66
- * | :-- | :-- |
67
- * | `enable()` | Call to enable the amplification for the first time or re-enable it if it was disabled before |
68
- * | `disable()` | Call to disable amplification |
69
- * | `enabled` | Whether the amplification is currently enabled |
70
- * | `setGain()` | Used to change the gain value from the default given by the parameter {@linkcode initialGain} |
71
- * | `getGain()` | Returns the current gain value |
72
- *
73
- * **Other properties:**
74
- * | Property | Description |
75
- * | :-- | :-- |
76
- * | `context` | The AudioContext instance |
77
- * | `sourceNode` | A MediaElementSourceNode instance created from the passed {@linkcode mediaElement} |
78
- * | `gainNode` | The GainNode instance used for volume amplification |
79
- */
80
- export declare function amplifyMedia<TElem extends HTMLMediaElement>(mediaElement: TElem, initialGain?: number): {
81
- context: AudioContext;
82
- sourceNode: MediaElementAudioSourceNode;
83
- gainNode: GainNode;
84
- /** Sets the gain of the amplifying GainNode */
85
- setGain(gain: number): void;
86
- /** Returns the current gain of the amplifying GainNode */
87
- getGain(): number;
88
- /** Whether the amplification is currently enabled */
89
- enabled: boolean;
90
- /** Enable the amplification for the first time or if it was disabled before */
91
- enable(): void;
92
- /** Disable the amplification */
93
- disable(): void;
94
- };
95
48
  /** Checks if an element is scrollable in the horizontal and vertical directions */
96
49
  export declare function isScrollable(element: Element): {
97
50
  vertical: boolean;
98
51
  horizontal: boolean;
99
52
  };
53
+ /**
54
+ * Executes the callback when the passed element's property changes.
55
+ * Contrary to an element's attributes, properties can usually not be observed with a MutationObserver.
56
+ * This function shims the getter and setter of the property to invoke the callback.
57
+ *
58
+ * [Source](https://stackoverflow.com/a/61975440)
59
+ * @param property The name of the property to observe
60
+ * @param callback Callback to execute when the value is changed
61
+ */
62
+ export declare function observeElementProp<TElem extends Element = HTMLElement, TPropKey extends keyof TElem = keyof TElem>(element: TElem, property: TPropKey, callback: (oldVal: TElem[TPropKey], newVal: TElem[TPropKey]) => void): void;
@@ -2,6 +2,19 @@
2
2
  export type Stringifiable = string | {
3
3
  toString(): string;
4
4
  };
5
+ /**
6
+ * A type that offers autocomplete for the passed union but also allows any arbitrary value of the same type to be passed.
7
+ * Supports unions of strings, numbers and objects.
8
+ */
9
+ export type LooseUnion<TUnion extends string | number | object> = (TUnion) | (TUnion extends string ? (string & {}) : (TUnion extends number ? (number & {}) : (TUnion extends Record<keyof any, unknown> ? (object & {}) : never)));
10
+ /**
11
+ * A type that allows all strings except for empty ones
12
+ * @example
13
+ * function foo<T extends string>(bar: NonEmptyString<T>) {
14
+ * console.log(bar);
15
+ * }
16
+ */
17
+ export type NonEmptyString<TString extends string> = TString extends "" ? never : TString;
5
18
  /**
6
19
  * Automatically appends an `s` to the passed {@linkcode word}, if {@linkcode num} is not equal to 1
7
20
  * @param word A word in singular form, to auto-convert to plural
@@ -25,7 +38,15 @@ export declare function fetchAdvanced(url: string, options?: FetchAdvancedOpts):
25
38
  /**
26
39
  * Inserts the passed values into a string at the respective placeholders.
27
40
  * The placeholder format is `%n`, where `n` is the 1-indexed argument number.
28
- * @param str The string to insert the values into
41
+ * @param input The string to insert the values into
29
42
  * @param values The values to insert, in order, starting at `%1`
30
43
  */
31
- export declare function insertValues(str: string, ...values: Stringifiable[]): string;
44
+ export declare function insertValues(input: string, ...values: Stringifiable[]): string;
45
+ /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as a base64 string */
46
+ export declare function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "base64"): Promise<string>;
47
+ /** Compresses a string or an ArrayBuffer using the provided {@linkcode compressionFormat} and returns it as an ArrayBuffer */
48
+ export declare function compress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>;
49
+ /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to a string */
50
+ export declare function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType?: "string"): Promise<string>;
51
+ /** Decompresses a previously compressed base64 string or ArrayBuffer, with the format passed by {@linkcode compressionFormat}, converted to an ArrayBuffer */
52
+ export declare function decompress(input: string | ArrayBuffer, compressionFormat: CompressionFormat, outputType: "arrayBuffer"): Promise<ArrayBuffer>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sv443-network/userutils",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
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",