# UserUtils Documentation
## Preamble:
This library is written in TypeScript and contains builtin TypeScript declarations, but it will also work in plain JavaScript after removing the `: type` annotations.
Each feature has example code that can be expanded by clicking on the text "Example - click to view".
The signatures and examples are written in TypeScript and use ESM import syntax to show you which types need to be provided and will be returned.
The library itself supports importing an ESM, CommonJS or global variable definition bundle, depending on your use case.
If the signature section contains multiple signatures of the function, each occurrence represents an overload and you can choose which one you want to use.
They will also be further explained in the description below that section.
Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other specific requirements and limitations.
Those will be listed in a section marked by a warning emoji (⚠️) each.
If you need help with something, please [create a new discussion](https://github.com/Sv443-Network/UserUtils/discussions) or [join my Discord server.](https://dc.sv443.net/)
For submitting bug reports or feature requests, please use the [GitHub issue tracker.](https://github.com/Sv443-Network/UserUtils/issues)
## Table of Contents:
- [**Preamble** (info about the documentation)](#preamble)
- [**Features**](#features)
- [**DOM:**](#dom)
- [`SelectorObserver`](#selectorobserver) - class that manages listeners that are called when selectors are found in the DOM
- [`getUnsafeWindow()`](#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
- [`isDomLoaded()`](#isdomloaded) - check if the DOM has finished loading and can be queried and modified
- [`onDomLoad()`](#ondomload) - run a function or pause async execution until the DOM has finished loading (or immediately if DOM is already loaded)
- [`addParent()`](#addparent) - add a parent element around another element
- [`addGlobalStyle()`](#addglobalstyle) - add a global style to the page
- [`preloadImages()`](#preloadimages) - preload images into the browser cache for faster loading later on
- [`openInNewTab()`](#openinnewtab) - open a link in a new tab
- [`interceptEvent()`](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
- [`interceptWindowEvent()`](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
- [`isScrollable()`](#isscrollable) - check if an element has a horizontal or vertical scroll bar
- [`observeElementProp()`](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
- [`getSiblingsFrame()`](#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
- [`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
- [`probeElementStyle()`](#probeelementstyle) - probe the computed style of a temporary element (get default font size, resolve CSS variables, etc.)
- [**Math:**](#math)
- [`clamp()`](#clamp) - constrain a number between a min and max value
- [`mapRange()`](#maprange) - map a number from one range to the same spot in another range
- [`randRange()`](#randrange) - generate a random number between a min and max boundary
- [`digitCount()`](#digitcount) - calculate the amount of digits in a number
- [`roundFixed()`](#roundfixed) - round a floating-point number at the given amount of decimals, or to the given power of 10
- [`bitSetHas()`](#bitsethas) - check if a bit is set in a [bitset](https://www.geeksforgeeks.org/cpp-bitset-and-its-application/)
- [**Misc:**](#misc)
- [`DataStore`](#datastore) - class that manages a hybrid sync & async persistent JSON database, including data migration
- [`DataStoreSerializer`](#datastoreserializer) - class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrations
- [`Dialog`](#dialog) - class for creating custom modal dialogs with a promise-based API and a generic, default style
- [`NanoEmitter`](#nanoemitter) - tiny event emitter class with a focus on performance and simplicity (based on [nanoevents](https://npmjs.com/package/nanoevents))
- [`Debouncer`](#debouncer) - class for debouncing function calls with a given timeout
- [`debounce()`](#debounce) - function wrapper for the Debouncer class for easier usage
- [`autoPlural()`](#autoplural) - automatically pluralize a string
- [`pauseFor()`](#pausefor) - pause the execution of a function for a given amount of time
- [`fetchAdvanced()`](#fetchadvanced) - wrapper around the fetch API with a timeout option
- [`insertValues()`](#insertvalues) - insert values into a string at specified placeholders
- [`compress()`](#compress) - compress a string with Gzip or Deflate
- [`decompress()`](#decompress) - decompress a previously compressed string
- [`computeHash()`](#computehash) - compute the hash / checksum of a string or ArrayBuffer
- [`randomId()`](#randomid) - generate a random ID of a given length and radix
- [`consumeGen()`](#consumegen) - consumes a ValueGen and returns the value
- [`consumeStringGen()`](#consumestringgen) - consumes a StringGen and returns the string
- [`getListLength()`](#getlistlength) - get the length of any object with a numeric `length`, `count` or `size` property
- [`purifyObj()`](#purifyobj) - removes the prototype chain (all default properties like `toString`, `__proto__`, etc.) from an object
- [**Arrays:**](#arrays)
- [`randomItem()`](#randomitem) - returns a random item from an array
- [`randomItemIndex()`](#randomitemindex) - returns a tuple of a random item and its index from an array
- [`takeRandomItem()`](#takerandomitem) - returns a random item from an array and mutates it to remove the item
- [`randomizeArray()`](#randomizearray) - returns a copy of the array with its items in a random order
- [**Translation:**](#translation)
- [`tr.for()`](#trfor) - translates a key for the specified language
- [`tr.use()`](#truse) - creates a translation function for the specified language
- [`tr.hasKey()`](#trhaskey) - checks if a key exists in the given language
- [`tr.addTranslations()`](#traddtranslations) - add a flat or recursive translation object for a language
- [`tr.getTranslations()`](#trgettranslations) - returns the translation object for a language
- [`tr.deleteTranslations()`](#trdeletetranslations) - delete the translation object for a language
- [`tr.setFallbackLanguage()`](#trsetfallbacklanguage) - set the fallback language used when a key is not found in the given language
- [`tr.getFallbackLanguage()`](#trgetfallbacklanguage) - returns the fallback language
- [`tr.addTransform()`](#traddtransform) - adds a transform function to the translation system for custom argument insertion and much more
- [`tr.deleteTransform()`](#trdeletetransform) - removes a transform function
- [`tr.transforms`](#trtransforms) - predefined transform functions for quickly adding custom argument insertion
- [`TrKeys`](#trkeys) - generic type that extracts all keys from a flat or recursive translation object into a union
- [**Colors:**](#colors)
- [`hexToRgb()`](#hextorgb) - convert a hex color string to an RGB or RGBA value tuple
- [`rgbToHex()`](#rgbtohex) - convert RGB or RGBA values to a hex color string
- [`lightenColor()`](#lightencolor) - lighten a CSS color string (hex, rgb or rgba) by a given percentage
- [`darkenColor()`](#darkencolor) - darken a CSS color string (hex, rgb or rgba) by a given percentage
- [**Utility types for TypeScript:**](#utility-types)
- [`Stringifiable`](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly)
- [`NonEmptyArray`](#nonemptyarray) - any array that should have at least one item
- [`NonEmptyString`](#nonemptystring) - any string that should have at least one character
- [`LooseUnion`](#looseunion) - a union that gives autocomplete in the IDE but also allows any other value of the same type
- [`Prettify`](#prettify) - expands a complex type into a more readable format while keeping functionality the same
- [`ValueGen`](#valuegen) - a "generator" value that allows for super flexible value typing and declaration
- [`StringGen`](#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
- [`ListWithLength`](#listwithlength) - represents an array or object with a numeric `length`, `count` or `size` property
- [**Custom Error classes**](#error-classes)
- [`UUError`](#uuerror) - base class for all custom UserUtils errors - has a custom `date` prop set to the time of creation
- [`ChecksumMismatchError`](#checksummismatcherror) - thrown when a string of data doesn't match its checksum
- [`MigrationError`](#migrationerror) - thrown when a data migration fails
- [`PlatformError`](#platformerror) - thrown when a function is called in an unsupported environment
## Features:
## DOM:
### SelectorObserver
Signatures:
```ts
// using an Element instance:
new SelectorObserver(baseElement: Element, options?: SelectorObserverOptions)
// using selector string:
new SelectorObserver(baseElementSelector: string, options?: SelectorObserverOptions)
```
A class that manages listeners that are called when elements at given selectors are found in the DOM.
It is useful for userscripts that need to wait for elements to be added to the DOM at an indeterminate point in time before they can be interacted with.
By default, it uses the MutationObserver API to observe for any element changes, and as such is highly customizable, but can also be configured to run on a fixed interval.
The constructor takes a `baseElement`, which is a parent of the elements you want to observe.
If a selector string is passed instead, it will be used to find the element.
If you want to observe the entire document, you can pass `document.body` - ⚠️ you should only use this to initialize other SelectorObserver instances, and never run continuous listeners on this instance, as the performance impact can be massive!
The `options` parameter is optional and will be passed to the MutationObserver that is used internally.
The MutationObserver options present by default are `{ childList: true, subtree: true }` - you may see the [MutationObserver.observe() documentation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) for more information and a list of options.
For example, if you want to trigger the listeners when certain attributes change, pass `{ attributeFilter: ["class", "data-my-attribute"] }`
Additionally, there are the following extra options:
- `disableOnNoListeners` - whether to disable the SelectorObserver when there are no listeners left (defaults to false)
- `enableOnAddListener` - whether to enable the SelectorObserver when a new listener is added (defaults to true)
- `defaultDebounce` - if set to a number, this debounce will be applied to every listener that doesn't have a custom debounce set (defaults to 0)
- `defaultDebounceType` - can be set to "immediate" (default), to call the function on the very first call and subsequent times after the given debounce time passed, or "idle", to let through the very last call, after the debounce time passed with no subsequent calls - [see `Debouncer` for more info and a diagram](#debouncer)
- `checkInterval` - if set to a number, the checks will be run on interval instead of on mutation events - in that case all MutationObserverInit props will be ignored
⚠️ Make sure to call `enable()` to actually start observing. This will need to be done after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired) **and** as soon as the `baseElement` or `baseElementSelector` is available.
### Methods:
#### `SelectorObserver.addListener()`
Signature: `SelectorObserver.addListener(selector: string, options: SelectorListenerOptions): void`
Adds a listener (specified in `options.listener`) for the given selector that will be called once the selector exists in the DOM. It will be passed the element(s) that match the selector as the only argument.
The listener will be called immediately if the selector already exists in the DOM.
> `options.listener` is the only required property of the `options` object.
> It is a function that will be called once the selector exists in the DOM.
> It will be passed the found element or NodeList of elements, depending on if `options.all` is set to true or false.
> If `options.all` is set to true, querySelectorAll() will be used instead and the listener will be passed a `NodeList` of matching elements.
> This will also include elements that were already found in a previous listener call.
> If set to false (default), querySelector() will be used and only the first matching element will be returned.
> If `options.continuous` is set to true, this listener will not be deregistered after it was called once (defaults to false).
>
> ⚠️ You should keep usage of this option to a minimum, as it will cause this listener to be called every time the selector is *checked for and found* and this can stack up quite quickly.
> ⚠️ You should try to only use this option on SelectorObserver instances that are scoped really low in the DOM tree to prevent as many selector checks as possible from being triggered.
> ⚠️ I also recommend always setting a debounce time (see constructor or below) if you use this option.
> If `options.debounce` is set to a number above 0, this listener will be debounced by that amount of milliseconds (defaults to 0).
> E.g. if the debounce time is set to 200 and the selector is found twice within 100ms, only the last call of this listener will be executed.
> `options.debounceType` is set to "immediate" by default, which means the listener will be called immediately and then debounced on subsequent calls.
> If set to "idle", the SelectorObserver will wait until the debounce time has passed with no new calls and then calls the listener with the latest element(s) found.
> See the [Debouncer](#debouncer) class for a better explanation.
> When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that this listener will return.
> It will default to HTMLElement if left undefined.
#### `SelectorObserver.enable()`
Signature: `SelectorObserver.enable(immediatelyCheckSelectors?: boolean): boolean`
Enables the observation of the child elements for the first time or if it was disabled before.
`immediatelyCheckSelectors` is set to true by default, which means all previously registered selectors will be checked. Set to false to only check them on the first detected mutation.
Returns true if the observation was enabled, false if it was already enabled or the passed `baseElementSelector` couldn't be found.
#### `SelectorObserver.disable()`
Signature: `SelectorObserver.disable(): void`
Disables the observation of the child elements.
If selectors are currently being checked, the current selector will be finished before disabling.
#### `SelectorObserver.isEnabled()`
Signature: `SelectorObserver.isEnabled(): boolean`
Returns whether the observation of the child elements is currently enabled.
#### `SelectorObserver.clearListeners()`
Signature: `SelectorObserver.clearListeners(): void`
Removes all listeners for all selectors.
#### `SelectorObserver.removeAllListeners()`
Signature: `SelectorObserver.removeAllListeners(selector: string): boolean`
Removes all listeners for the given selector.
#### `SelectorObserver.removeListener()`
Signature: `SelectorObserver.removeListener(selector: string, options: SelectorListenerOptions): boolean`
Removes a specific listener for the given selector and options.
#### `SelectorObserver.getAllListeners()`
Signature: `SelectorObserver.getAllListeners(): Map`
Returns a Map of all selectors and their listeners.
#### `SelectorObserver.getListeners()`
Signature: `SelectorObserver.getListeners(selector: string): SelectorListenerOptions[] | undefined`
Returns all listeners for the given selector or undefined if there are none.
Examples - click to view
#### Basic Signature:
```ts
import { SelectorObserver } from "@sv443-network/userutils";
// adding a single-shot listener before the element exists:
const fooObserver = new SelectorObserver("body");
fooObserver.addListener("#my-element", {
listener: (element) => {
console.log("Element found:", element);
},
});
document.addEventListener("DOMContentLoaded", () => {
// starting observation after the element is available:
fooObserver.enable();
// adding custom observer options:
const barObserver = new SelectorObserver(document.body, {
// only check if the following attributes change:
attributeFilter: ["class", "style", "data-whatever"],
// debounce all listeners by 100ms unless specified otherwise:
defaultDebounce: 100,
// "immediate" means listeners are called immediately and use the debounce as a timeout between subsequent calls - see the Debouncer class for a better explanation
defaultDebounceType: "immediate",
// other settings from the MutationObserver API can be set here too - see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options
});
barObserver.addListener("#my-element", {
listener: (element) => {
console.log("Element's attributes changed:", element);
},
});
barObserver.addListener("#my-other-element", {
// set the debounce higher than provided by the defaultDebounce property:
debounce: 250,
// change the type for this specific listener:
debounceType: "idle",
listener: (element) => {
console.log("Other element's attributes changed:", element);
},
});
barObserver.enable();
// using custom listener options:
const bazObserver = new SelectorObserver(document.body);
// for TypeScript, specify that input elements are returned by the listener:
const unsubscribe = bazObserver.addListener("input", {
all: true, // use querySelectorAll() instead of querySelector()
continuous: true, // don't remove the listener after it was called once
debounce: 50, // debounce the listener by 50ms
listener: (elements) => {
// type of `elements` is NodeListOf
console.log("Input elements found:", elements);
},
});
bazObserver.enable();
window.addEventListener("something", () => {
// remove the listener after the event "something" was dispatched:
unsubscribe();
});
// use a different element as the base:
const myElement = document.querySelector("#my-element");
if(myElement) {
const quxObserver = new SelectorObserver(myElement);
quxObserver.addListener("#my-child-element", {
listener: (element) => {
console.log("Child element found:", element);
},
});
quxObserver.enable();
}
});
```
#### Get and remove listeners:
```ts
import { SelectorObserver } from "@sv443-network/userutils";
document.addEventListener("DOMContentLoaded", () => {
const observer = new SelectorObserver(document.body);
observer.addListener("#my-element-foo", {
continuous: true,
listener: (element) => {
console.log("Element found:", element);
},
});
observer.addListener("#my-element-bar", {
listener: (element) => {
console.log("Element found again:", element);
},
});
observer.enable();
// get all listeners:
console.log(observer.getAllListeners());
// Map(2) {
// '#my-element-foo' => [ { listener: [Function: listener] } ],
// '#my-element-bar' => [ { listener: [Function: listener] } ]
// }
// get listeners for a specific selector:
console.log(observer.getListeners("#my-element-foo"));
// [ { listener: [Function: listener], continuous: true } ]
// remove all listeners for a specific selector:
observer.removeAllListeners("#my-element-foo");
console.log(observer.getAllListeners());
// Map(1) {
// '#my-element-bar' => [ { listener: [Function: listener] } ]
// }
});
```
#### Chaining:
```ts
import { SelectorObserver } from "@sv443-network/userutils";
import type { SelectorObserverOptions } from "@sv443-network/userutils";
// apply a default debounce to all SelectorObserver instances:
const defaultOptions: SelectorObserverOptions = {
defaultDebounce: 100,
};
document.addEventListener("DOMContentLoaded", () => {
// initialize generic observer that in turn initializes "sub-observers":
const fooObserver = new SelectorObserver(document.body, {
...defaultOptions,
// define any other specific options here
});
const myElementSelector = "#my-element";
// this relatively expensive listener (as it is in the full scope) will only fire once:
fooObserver.addListener(myElementSelector, {
listener: (element) => {
// only enable barObserver once its baseElement exists:
barObserver.enable();
},
});
// barObserver is created at the same time as fooObserver, but only enabled once #my-element exists
const barObserver = new SelectorObserver(element, {
...defaultOptions,
// define any other specific options here
});
// this selector will be checked for immediately after `enable()` is called
// and on each subsequent mutation because `continuous` is set to true.
// however it is much less expensive as it is scoped to a lower element which will receive less DOM updates
barObserver.addListener(".my-child-element", {
all: true,
continuous: true,
listener: (elements) => {
console.log("Child elements found:", elements);
},
});
// immediately enable fooObserver as the is available as soon as "DOMContentLoaded" fires:
fooObserver.enable();
});
```
### getUnsafeWindow()
Signature:
```ts
getUnsafeWindow(): Window
```
Returns the unsafeWindow object or falls back to the regular window object if the `@grant unsafeWindow` is not given.
Userscripts are sandboxed and do not have access to the regular window object, so this function is useful for websites that reject some events that were dispatched by the userscript, or userscripts that need to interact with other userscripts, and more.
Example - click to view
```ts
import { getUnsafeWindow } from "@sv443-network/userutils";
// trick the site into thinking the mouse was moved:
const mouseEvent = new MouseEvent("mousemove", {
view: getUnsafeWindow(),
screenY: 69,
screenX: 420,
movementX: 10,
movementY: 0,
});
document.body.dispatchEvent(mouseEvent);
```
### isDomLoaded()
Signature:
```ts
isDomLoaded(): boolean
```
Returns whether or not the DOM has finished loading and can be queried, manipulated, interacted with, etc.
As long as the library is loaded immediately on page load, this function will always return the correct value, even if your runtime is executed after the DOM has finished loading (like when using `@run-at document-end`).
Just make sure to not lazy-load the library, evaluate it on-demand, or anything else that would delay the execution.
Example - click to view
```ts
import { isDomLoaded } from "@sv443-network/userutils";
console.log(isDomLoaded()); // false
document.addEventListener("DOMContentLoaded", () => {
console.log(isDomLoaded()); // true
});
```
### onDomLoad()
Signature:
```ts
onDomLoad(cb?: () => void): Promise
```
Executes a callback and/or resolves the returned Promise when the DOM has finished loading.
Immediately executes/resolves if the DOM is already loaded.
This alleviates the problem of the [`DOMContentLoaded` event](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event) only being fired once and if you missed it, you can't really be certain and have to fall back to something like [`document.readyState === "complete"`](https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState#value), which could happen at a much later point in time than `DOMContentLoaded`.
Example - click to view
```ts
import { onDomLoad } from "@sv443-network/userutils";
// callback gets executed at basically the same time as the `console.log("DOM loaded!")` below:
onDomLoad(() => {
console.log("DOM has finished loading.");
});
document.addEventListener("DOMContentLoaded", async () => {
console.log("DOM loaded!");
// immediately resolves because the DOM is already loaded:
await onDomLoad();
console.log("DOM has finished loading.");
});
```
### addParent()
Signature:
```ts
addParent(element: Element, newParent: Element): Element
```
Adds a parent element around the passed `element` and returns the new parent.
Previously registered event listeners are kept intact.
⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
Example - click to view
```ts
import { addParent } from "@sv443-network/userutils";
// add an around an element
const element = document.querySelector("#element");
const newParent = document.createElement("a");
newParent.href = "https://example.org/";
addParent(element, newParent);
```
### addGlobalStyle()
Signature:
```ts
addGlobalStyle(css: string): HTMLStyleElement
```
Adds a global style to the page in form of a `