@sv443-network/userutils 1.0.0 → 1.1.1

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,21 @@
1
1
  # @sv443-network/userutils
2
2
 
3
+ ## 1.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 4799a9f: Fix TS error in ConfigManager migration functions
8
+
9
+ ## 1.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - db5cded: Added `isScrollable()` to check whether an element has a horizontal or vertical scroll bar
14
+
15
+ ### Patch Changes
16
+
17
+ - 9e26464: Replaced most occurrences of `HTMLElement` in the docs with `Element` for better compatibility
18
+
3
19
  ## 1.0.0
4
20
 
5
21
  ### Major Changes
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## UserUtils
4
4
  Zero-dependency library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more.
5
+
5
6
  Contains builtin TypeScript declarations. Webpack compatible and supports ESM and CJS.
6
7
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
7
8
 
@@ -26,6 +27,7 @@ If you like using this library, please consider [supporting the development ❤
26
27
  - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
27
28
  - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
28
29
  - [amplifyMedia()](#amplifymedia) - amplify an audio or video element's volume past the maximum of 100%
30
+ - [isScrollable()](#isscrollable) - check if an element has a horizontal or vertical scroll bar
29
31
  - [Math:](#math)
30
32
  - [clamp()](#clamp) - constrain a number between a min and max value
31
33
  - [mapRange()](#maprange) - map a number from one range to the same spot in another range
@@ -80,14 +82,13 @@ If you like using this library, please consider [supporting the development ❤
80
82
 
81
83
  ## Preamble:
82
84
  This library is written in TypeScript and contains builtin TypeScript declarations.
83
- The usages and examples in this readme are written in TypeScript, but the library can also be used in plain JavaScript.
84
-
85
- Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
86
- Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
87
85
 
88
86
  Each feature has example code that can be expanded by clicking on the text "Example - click to view".
87
+ The usages and examples are written in TypeScript, but the library can also be used in plain JavaScript after removing the type annotations (and changing the imports if you are using CommonJS).
88
+ If the usage section contains multiple definitions of the function, each occurrence represents an overload and you can choose which one you want to use.
89
89
 
90
- If the usage section contains multiple definitions of the function, each occurrence represents an overload and you can choose which one you want to use.
90
+ Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements.
91
+ Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.
91
92
 
92
93
  <br><br>
93
94
 
@@ -123,6 +124,7 @@ If set to `false` (default), querySelector() will be used and only the first mat
123
124
  If `continuous` is set to `true`, the listener will not be deregistered after it was called once (defaults to false).
124
125
 
125
126
  When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that the listener will return.
127
+ It will default to `HTMLElement` if left undefined.
126
128
 
127
129
  ⚠️ In order to use this function, [`initOnSelector()`](#initonselector) has to be called as soon as possible.
128
130
  This initialization function has to be called after `DOMContentLoaded` is fired (or immediately if `@run-at document-end` is set).
@@ -132,6 +134,8 @@ Calling onSelector() before `DOMContentLoaded` is fired will not throw an error,
132
134
  <details><summary><h4>Example - click to view</h4></summary>
133
135
 
134
136
  ```ts
137
+ import { initOnSelector, onSelector } from "@sv443-network/userutils";
138
+
135
139
  document.addEventListener("DOMContentLoaded", initOnSelector);
136
140
 
137
141
  // Continuously checks if `div` elements are added to the DOM, then returns all of them (even previously detected ones) in a NodeList
@@ -177,6 +181,8 @@ You may see all options [here](https://developer.mozilla.org/en-US/docs/Web/API/
177
181
  <details><summary><h4>Example - click to view</h4></summary>
178
182
 
179
183
  ```ts
184
+ import { initOnSelector } from "@sv443-network/userutils";
185
+
180
186
  document.addEventListener("DOMContentLoaded", () => {
181
187
  initOnSelector({
182
188
  attributes: true,
@@ -201,6 +207,8 @@ Since multiple listeners can be registered for the same selector, the value of t
201
207
  <details><summary><h4>Example - click to view</h4></summary>
202
208
 
203
209
  ```ts
210
+ import { initOnSelector, onSelector, getSelectorMap } from "@sv443-network/userutils";
211
+
204
212
  document.addEventListener("DOMContentLoaded", initOnSelector);
205
213
 
206
214
  onSelector<HTMLDivElement>("div", {
@@ -244,6 +252,8 @@ Userscripts are sandboxed and do not have access to the regular window object, s
244
252
  <details><summary><h4>Example - click to view</h4></summary>
245
253
 
246
254
  ```ts
255
+ import { getUnsafeWindow } from "@sv443-network/userutils";
256
+
247
257
  // trick the site into thinking the mouse was moved:
248
258
  const mouseEvent = new MouseEvent("mousemove", {
249
259
  view: getUnsafeWindow(),
@@ -252,6 +262,7 @@ const mouseEvent = new MouseEvent("mousemove", {
252
262
  movementX: 10,
253
263
  movementY: 0,
254
264
  });
265
+
255
266
  document.body.dispatchEvent(mouseEvent);
256
267
  ```
257
268
 
@@ -262,7 +273,7 @@ document.body.dispatchEvent(mouseEvent);
262
273
  ### insertAfter()
263
274
  Usage:
264
275
  ```ts
265
- insertAfter(beforeElement: HTMLElement, afterElement: HTMLElement): HTMLElement
276
+ insertAfter(beforeElement: Element, afterElement: Element): Element
266
277
  ```
267
278
 
268
279
  Inserts the element passed as `afterElement` as a sibling after the passed `beforeElement`.
@@ -273,10 +284,13 @@ The passed `afterElement` will be returned.
273
284
  <details><summary><h4>Example - click to view</h4></summary>
274
285
 
275
286
  ```ts
287
+ import { insertAfter } from "@sv443-network/userutils";
288
+
276
289
  // insert a <div> as a sibling next to an element
277
290
  const beforeElement = document.querySelector("#before");
278
291
  const afterElement = document.createElement("div");
279
292
  afterElement.innerText = "After";
293
+
280
294
  insertAfter(beforeElement, afterElement);
281
295
  ```
282
296
 
@@ -287,7 +301,7 @@ insertAfter(beforeElement, afterElement);
287
301
  ### addParent()
288
302
  Usage:
289
303
  ```ts
290
- addParent(element: HTMLElement, newParent: HTMLElement): HTMLElement
304
+ addParent(element: Element, newParent: Element): Element
291
305
  ```
292
306
 
293
307
  Adds a parent element around the passed `element` and returns the new parent.
@@ -298,10 +312,13 @@ Previously registered event listeners are kept intact.
298
312
  <details><summary><h4>Example - click to view</h4></summary>
299
313
 
300
314
  ```ts
315
+ import { addParent } from "@sv443-network/userutils";
316
+
301
317
  // add an <a> around an element
302
318
  const element = document.querySelector("#element");
303
319
  const newParent = document.createElement("a");
304
320
  newParent.href = "https://example.org/";
321
+
305
322
  addParent(element, newParent);
306
323
  ```
307
324
 
@@ -321,6 +338,8 @@ Adds a global style to the page in form of a `<style>` element that's inserted i
321
338
  <details><summary><h4>Example - click to view</h4></summary>
322
339
 
323
340
  ```ts
341
+ import { addGlobalStyle } from "@sv443-network/userutils";
342
+
324
343
  document.addEventListener("DOMContentLoaded", () => {
325
344
  addGlobalStyle(`
326
345
  body {
@@ -347,6 +366,8 @@ The resulting PromiseSettledResult array will contain the image elements if reso
347
366
  <details><summary><h4>Example - click to view</h4></summary>
348
367
 
349
368
  ```ts
369
+ import { preloadImages } from "@sv443-network/userutils";
370
+
350
371
  preloadImages([
351
372
  "https://example.org/image1.png",
352
373
  "https://example.org/image2.png",
@@ -376,6 +397,8 @@ This function has to be run in response to a user interaction event, else the br
376
397
  <details><summary><h4>Example - click to view</h4></summary>
377
398
 
378
399
  ```ts
400
+ import { openInNewTab } from "@sv443-network/userutils";
401
+
379
402
  document.querySelector("#my-button").addEventListener("click", () => {
380
403
  openInNewTab("https://example.org/");
381
404
  });
@@ -391,7 +414,7 @@ Usage:
391
414
  interceptEvent(
392
415
  eventObject: EventTarget,
393
416
  eventName: string,
394
- predicate: () => boolean
417
+ predicate: (event: Event) => boolean
395
418
  ): void
396
419
  ```
397
420
 
@@ -403,7 +426,10 @@ Calling this function will set the `Error.stackTraceLimit` to 1000 (if it's not
403
426
  <details><summary><h4>Example - click to view</h4></summary>
404
427
 
405
428
  ```ts
406
- interceptEvent(document.body, "click", () => {
429
+ import { interceptEvent } from "@sv443-network/userutils";
430
+
431
+ interceptEvent(document.body, "click", (event) => {
432
+ console.log("Intercepting click event:", event);
407
433
  return true; // prevent all click events on the body element
408
434
  });
409
435
  ```
@@ -417,7 +443,7 @@ Usage:
417
443
  ```ts
418
444
  interceptWindowEvent(
419
445
  eventName: string,
420
- predicate: () => boolean
446
+ predicate: (event: Event) => boolean
421
447
  ): void
422
448
  ```
423
449
 
@@ -425,11 +451,14 @@ Intercepts all events dispatched on the `window` object and prevents the listene
425
451
  This is essentially the same as [`interceptEvent()`](#interceptevent), but automatically uses the `unsafeWindow` (or falls back to regular `window`).
426
452
 
427
453
  ⚠️ This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are *attached* after this function is called.
454
+ ⚠️ In order for all events to be interceptable, the directive `@grant unsafeWindow` should be set.
428
455
 
429
456
  <details><summary><h4>Example - click to view</h4></summary>
430
457
 
431
458
  ```ts
432
- interceptWindowEvent("beforeunload", () => {
459
+ import { interceptWindowEvent } from "@sv443-network/userutils";
460
+
461
+ interceptWindowEvent("beforeunload", (event) => {
433
462
  return true; // prevent the pesky "Are you sure you want to leave this page?" popup
434
463
  });
435
464
  ```
@@ -463,6 +492,8 @@ The returned AmplifyMediaResult object has the following properties:
463
492
  <details><summary><h4>Example - click to view</h4></summary>
464
493
 
465
494
  ```ts
495
+ import { amplifyMedia } from "@sv443-network/userutils";
496
+
466
497
  const audio = document.querySelector<HTMLAudioElement>("audio");
467
498
  const button = document.querySelector<HTMLButtonElement>("button");
468
499
 
@@ -485,6 +516,31 @@ button.addEventListener("click", () => {
485
516
 
486
517
  </details>
487
518
 
519
+ <br>
520
+
521
+ ### isScrollable()
522
+ Usage:
523
+ ```ts
524
+ isScrollable(element: Element): { horizontal: boolean, vertical: boolean }
525
+ ```
526
+
527
+ Checks if an element has a horizontal or vertical scroll bar.
528
+ This uses the computed style of the element, so it will also work if the element is hidden.
529
+
530
+ <details><summary><h4>Example - click to view</h4></summary>
531
+
532
+ ```ts
533
+ import { isScrollable } from "@sv443-network/userutils";
534
+
535
+ const element = document.querySelector("#element");
536
+ const { horizontal, vertical } = isScrollable(element);
537
+
538
+ console.log("Element has a horizontal scroll bar:", horizontal);
539
+ console.log("Element has a vertical scroll bar:", vertical);
540
+ ```
541
+
542
+ </details>
543
+
488
544
  <br><br>
489
545
 
490
546
  ## Math:
@@ -500,6 +556,8 @@ Clamps a number between a min and max boundary (inclusive).
500
556
  <details><summary><h4>Example - click to view</h4></summary>
501
557
 
502
558
  ```ts
559
+ import { clamp } from "@sv443-network/userutils";
560
+
503
561
  clamp(7, 0, 10); // 7
504
562
  clamp(-1, 0, 10); // 0
505
563
  clamp(5, -5, 0); // 0
@@ -531,6 +589,8 @@ Maps a number from one range to the spot it would be in another range.
531
589
  <details><summary><h4>Example - click to view</h4></summary>
532
590
 
533
591
  ```ts
592
+ import { mapRange } from "@sv443-network/userutils";
593
+
534
594
  mapRange(5, 0, 10, 0, 100); // 50
535
595
  mapRange(5, 0, 10, 0, 50); // 25
536
596
 
@@ -556,6 +616,8 @@ If only one argument is passed, it will be used as the `max` value and `min` wil
556
616
  <details><summary><h4>Example - click to view</h4></summary>
557
617
 
558
618
  ```ts
619
+ import { randRange } from "@sv443-network/userutils";
620
+
559
621
  randRange(0, 10); // 4
560
622
  randRange(10, 20); // 17
561
623
  randRange(10); // 7
@@ -575,8 +637,9 @@ new ConfigManager(options: ConfigManagerOptions)
575
637
 
576
638
  A class that manages a userscript's configuration that is persistently saved to and loaded from GM storage.
577
639
  Also supports automatic migration of outdated data formats via provided migration functions.
640
+ You may create as many instances as you like as long as they have different IDs.
578
641
 
579
- ⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used.
642
+ ⚠️ The configuration is stored as a JSON string, so only JSON-compatible data can be used. Circular structures and complex objects will throw an error on load and save.
580
643
  ⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required for this to work.
581
644
 
582
645
  The options object has the following properties:
@@ -608,7 +671,8 @@ Writes the default configuration given in `options.defaultConfig` synchronously
608
671
  `deleteConfig(): Promise<void>`
609
672
  Fully deletes the configuration from persistent storage.
610
673
  The internal cache will be left untouched, so any subsequent calls to `getData()` will return the data that was last loaded.
611
- If `loadData()` or `setData()` are called after this, the persistent storage will be populated again.
674
+ If `loadData()` or `setData()` are called after this, the persistent storage will be populated with the value of `options.defaultConfig` again.
675
+ ⚠️ If you want to use this method, the additional directive `@grant GM.deleteValue` is required.
612
676
 
613
677
  <br>
614
678
 
@@ -636,7 +700,7 @@ const formatVersion = 2;
636
700
  /** Functions that migrate outdated data to the latest format - make sure a function exists for every previously used formatVersion and that no numbers are skipped! */
637
701
  const migrations = {
638
702
  // migrate from format version 0 to 1
639
- 1: (oldData: any) => {
703
+ 1: (oldData: Record<string, unknown>) => {
640
704
  return {
641
705
  foo: oldData.foo,
642
706
  bar: oldData.bar,
@@ -644,7 +708,7 @@ const migrations = {
644
708
  };
645
709
  },
646
710
  // asynchronously migrate from format version 1 to 2
647
- 2: async (oldData: any) => {
711
+ 2: async (oldData: Record<string, unknown>) => {
648
712
  // arbitrary async operation required for the new format
649
713
  const qux = JSON.parse(await (await fetch("https://api.example.org/some-data")).text());
650
714
  return {
@@ -656,7 +720,7 @@ const migrations = {
656
720
  },
657
721
  };
658
722
 
659
- const configMgr = new ConfigManager({
723
+ const manager = new ConfigManager({
660
724
  /** A unique ID for this configuration - choose wisely as changing it is not supported yet! */
661
725
  id: "my-userscript",
662
726
  /** Default / fallback configuration data */
@@ -672,7 +736,7 @@ async function init() {
672
736
  // wait for the config to be loaded from persistent storage
673
737
  // if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultConfig will be returned
674
738
  // if the previously saved data needs to be migrated to a newer version, it will happen in this function call
675
- const configData = await configMgr.loadData();
739
+ const configData = await manager.loadData();
676
740
 
677
741
  console.log(configData.foo); // "hello"
678
742
 
@@ -681,12 +745,12 @@ async function init() {
681
745
  configData.bar = 123;
682
746
 
683
747
  // save the updated config - synchronously to the cache and asynchronously to persistent storage
684
- configMgr.saveData(configData).then(() => {
748
+ manager.saveData(configData).then(() => {
685
749
  console.log("Config saved to persistent storage!");
686
750
  });
687
751
 
688
752
  // the internal cache is updated synchronously, so the updated data can be accessed before the Promise resolves:
689
- console.log(configMgr.getData().foo); // "world"
753
+ console.log(manager.getData().foo); // "world"
690
754
  }
691
755
 
692
756
  init();
@@ -708,6 +772,8 @@ If an array or NodeList is passed, the amount of contained items will be used.
708
772
  <details><summary><h4>Example - click to view</h4></summary>
709
773
 
710
774
  ```ts
775
+ import { autoPlural } from "@sv443-network/userutils";
776
+
711
777
  autoPlural("apple", 0); // "apples"
712
778
  autoPlural("apple", 1); // "apple"
713
779
  autoPlural("apple", 2); // "apples"
@@ -734,6 +800,8 @@ Pauses async execution for a given amount of time.
734
800
  <details><summary><h4>Example - click to view</h4></summary>
735
801
 
736
802
  ```ts
803
+ import { pauseFor } from "@sv443-network/userutils";
804
+
737
805
  async function run() {
738
806
  console.log("Hello");
739
807
  await pauseFor(3000); // waits for 3 seconds
@@ -759,6 +827,8 @@ The timeout will default to 300ms if left undefined.
759
827
  <details><summary><h4>Example - click to view</h4></summary>
760
828
 
761
829
  ```ts
830
+ import { debounce } from "@sv443-network/userutils";
831
+
762
832
  window.addEventListener("resize", debounce((event) => {
763
833
  console.log("Window was resized:", event);
764
834
  }, 500)); // 500ms timeout
@@ -783,7 +853,9 @@ The timeout will default to 10 seconds if left undefined.
783
853
  <details><summary><h4>Example - click to view</h4></summary>
784
854
 
785
855
  ```ts
786
- fetchAdvanced("https://api.example.org/data", {
856
+ import { fetchAdvanced } from "@sv443-network/userutils";
857
+
858
+ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
787
859
  timeout: 5000,
788
860
  // also accepts any other fetch options like headers:
789
861
  headers: {
@@ -812,6 +884,8 @@ Returns undefined if the array is empty.
812
884
  <details><summary><h4>Example - click to view</h4></summary>
813
885
 
814
886
  ```ts
887
+ import { randomItem } from "@sv443-network/userutils";
888
+
815
889
  randomItem(["foo", "bar", "baz"]); // "bar"
816
890
  randomItem([ ]); // undefined
817
891
  ```
@@ -832,12 +906,15 @@ If the array is empty, it will return undefined for both values.
832
906
  <details><summary><h4>Example - click to view</h4></summary>
833
907
 
834
908
  ```ts
909
+ import { randomItemIndex } from "@sv443-network/userutils";
910
+
835
911
  randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
836
912
  randomItemIndex([ ]); // [undefined, undefined]
913
+
837
914
  // using array destructuring:
838
- const [item, index] = randomItemIndex(["foo", "bar", "baz"]);
915
+ const [item, index] = randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
839
916
  // or if you only want the index:
840
- const [, index] = randomItemIndex(["foo", "bar", "baz"]);
917
+ const [, index] = randomItemIndex(["foo", "bar", "baz"]); // 1
841
918
  ```
842
919
 
843
920
  </details>
@@ -856,6 +933,8 @@ Returns undefined if the array is empty.
856
933
  <details><summary><h4>Example - click to view</h4></summary>
857
934
 
858
935
  ```ts
936
+ import { takeRandomItem } from "@sv443-network/userutils";
937
+
859
938
  const arr = ["foo", "bar", "baz"];
860
939
  takeRandomItem(arr); // "bar"
861
940
  console.log(arr); // ["foo", "baz"]
@@ -877,7 +956,14 @@ If the array is empty, the originally passed empty array will be returned withou
877
956
  <details><summary><h4>Example - click to view</h4></summary>
878
957
 
879
958
  ```ts
880
- randomizeArray([1, 2, 3, 4, 5, 6]); // [3, 1, 5, 2, 4, 6]
959
+ import { randomizeArray } from "@sv443-network/userutils";
960
+
961
+ const foo = [1, 2, 3, 4, 5, 6];
962
+
963
+ console.log(randomizeArray(foo)); // [3, 1, 5, 2, 4, 6]
964
+ console.log(randomizeArray(foo)); // [4, 5, 2, 1, 6, 3]
965
+
966
+ console.log(foo); // [1, 2, 3, 4, 5, 6] - original array is not mutated
881
967
  ```
882
968
 
883
969
  </details>