README.md 46 KB

## UserUtils 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. Contains builtin TypeScript declarations. Fully web compatible and supports ESM, CJS and global imports. If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
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)

## Table of Contents: - [**Installation**](#installation) - [**Preamble** (info about the documentation)](#preamble) - [**License**](#license) - [**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 - [insertAfter()](#insertafter) - insert an element as a sibling after another element - [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 - [amplifyMedia()](#amplifymedia) - amplify an audio or video element's volume past the maximum of 100% - [isScrollable()](#isscrollable) - check if an element has a horizontal or vertical scroll bar - [**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 - [randomId()](#randomid) - generate a random ID of a given length and radix - [**Misc:**](#misc) - [ConfigManager](#configmanager) - class that manages persistent userscript configurations, including data migration - [autoPlural()](#autoplural) - automatically pluralize a string - [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time - [debounce()](#debounce) - call a function only once, after 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 - [**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()](#tr) - simple translation of a string to another language - [tr.addLanguage()](#traddlanguage) - add a language and its translations - [tr.setLanguage()](#trsetlanguage) - set the currently active language for translations - [tr.getLanguage()](#trgetlanguage) - returns the currently active language - [**Utility types for TypeScript:**](#utility-types) - [Stringifiable](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly) - [NonEmptyArray](https://github.com/Sv443-Network/UserUtils#nonemptyarray) - any array that should have at least one item

## Installation: - If you are using a bundler like webpack, you can install this package using npm: ``` npm i @sv443-network/userutils ``` Then, import it in your script as usual: ```ts import { addGlobalStyle } from "@sv443-network/userutils"; // or just import everything (not recommended because this doesn't allow for treeshaking): import * as UserUtils from "@sv443-network/userutils"; ``` Shameless plug: I made a [template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly. It also includes this library by default.
- If you are not using a bundler, you can include the latest release from GreasyFork by adding this directive to the userscript header: ``` // @require https://greasyfork.org/scripts/472956-userutils/code/UserUtils.js ``` ``` // @require https://openuserjs.org/src/libs/Sv443/UserUtils.js ``` (in order for your userscript not to break on a major library update, use the versioned URL at the top of the [GreasyFork page](https://greasyfork.org/scripts/472956-userutils)) Then, access the functions on the global variable `UserUtils`: ```ts UserUtils.addGlobalStyle("body { background-color: red; }"); // or using object destructuring: const { clamp } = UserUtils; console.log(clamp(1, 5, 10); // 5 ```

## Preamble: This library is written in TypeScript and contains builtin TypeScript declarations. Each feature has example code that can be expanded by clicking on the text "Example - click to view". The usages and examples are written in TypeScript and use ESM import syntax, but the library can also be used in plain JavaScript after removing the type annotations (and changing the imports if you are using CommonJS or the global declaration). If the usage section contains multiple usages of the function, each occurrence represents an overload and you can choose which one you want to use. Some features require the `@run-at` or `@grant` directives to be tweaked in the userscript header or have other requirements. Their documentation will contain a section marked by a warning emoji (⚠️) that will go into more detail.

## License: This library is licensed under the MIT License. See the [license file](./LICENSE.txt) for details.

## Features:
## DOM: ### SelectorObserver Usage: ```ts new SelectorObserver(baseElement: Element, options?: MutationObserverInit) ``` A class that manages listeners that are called when elements at given selectors are found in the DOM. This is useful for userscripts that need to wait for elements to be added to the DOM at an indeterminate point in time before they can be interacted with. The constructor takes a `baseElement`, which is a parent of the elements you want to observe. If you want to observe the entire document, you can pass `document.body`. The `options` parameter is optional and will be passed to the MutationObserver that is used internally. The default options are `{ childList: true, subtree: true }` - you may see the [MutationObserver.observe() documentation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) for more information and a list of options. For example, if you want to trigger the listeners when certain attributes change, pass `{ attributes: true, attributeFilter: ["class", "data-my-attribute"] }` ⚠️ The instances of this class need to be created after the `baseElement` is available in the DOM (at the earliest when using `@run-at document-end` or after `DOMContentLoaded` has fired).
#### Methods: `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. 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, the listener will not be deregistered after it was called once (defaults to false). If `options.debounce` is set to a number above 0, the 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 the listener will be executed. When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that the listener will return. It will default to HTMLElement if left undefined.
`disable(): void` Disables the observation of the child elements. If selectors are currently being checked, the current selector will be finished before disabling.
`enable(): void` Enables the observation of the child elements if it was disabled before.
`isEnabled(): boolean` Returns whether the observation of the child elements is currently enabled.
`clearListeners(): void` Removes all listeners for all selectors.
`removeAllListeners(selector: string): boolean` Removes all listeners for the given selector.
`removeListener(selector: string, options: SelectorListenerOptions): boolean` Removes a specific listener for the given selector and options.
`getAllListeners(): Map` Returns a Map of all selectors and their listeners.
`getListeners(selector: string): SelectorListenerOptions[] | undefined` Returns all listeners for the given selector or undefined if there are none.
Examples - click to view #### Basic usage: ```ts import { SelectorObserver } from "@sv443-network/userutils"; document.addEventListener("DOMContentLoaded", () => { // adding a single-shot listener: const fooObserver = new SelectorObserver(document.body); fooObserver.addListener("#my-element", { listener: (element) => { console.log("Element found:", element); }, }); // adding custom observer options: const barObserver = new SelectorObserver(document.body, { // only check if the following attributes change: attributeFilter: ["class", "style", "data-whatever"], }); observer.addListener("#my-element", { listener: (element) => { console.log("Element attributes changed:", element); }, }); // using custom listener options: const bazObserver = new SelectorObserver(document.body); // for TypeScript, specify that input elements are returned by the listener: 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) => { console.log("Input elements found:", elements); }, }); // 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); }, }); } }); ```
#### 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); }, }); // 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"; document.addEventListener("DOMContentLoaded", () => { // initialize generic observer that in turn initializes "sub-observers": const fooObserver = new SelectorObserver(document.body); fooObserver.addListener("#my-element", { listener: (element) => { // only initialize the observer once it is actually needed (when #my-element exists): const barObserver = new SelectorObserver(element); barObserver.addListener(".my-child-element", { all: true, continuous: true, listener: (elements) => { console.log("Child elements found:", elements); }, }); }, }); }); ```
### getUnsafeWindow() Usage: ```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.
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); ```

### insertAfter() Usage: ```ts insertAfter(beforeElement: Element, afterElement: Element): Element ``` Inserts the element passed as `afterElement` as a sibling after the passed `beforeElement`. The passed `afterElement` will be returned. ⚠️ 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 { insertAfter } from "@sv443-network/userutils"; // insert a
as a sibling next to an element const beforeElement = document.querySelector("#before"); const afterElement = document.createElement("div"); afterElement.innerText = "After"; insertAfter(beforeElement, afterElement); ```

### addParent() Usage: ```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() Usage: ```ts addGlobalStyle(css: string): void ``` Adds a global style to the page in form of a `