Skip to content

Latest commit

 

History

History
1932 lines (1486 loc) · 89.1 KB

File metadata and controls

1932 lines (1486 loc) · 89.1 KB

UserUtils Documentation

General purpose DOM/GreaseMonkey library that allows you to register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more.
Contains builtin TypeScript declarations. Supports ESM and CJS imports via a bundler and global declaration via @require or <script>
The library works in any DOM environment with or without the GreaseMonkey API, but some features will be unavailable or limited.

You may want to check out my template for userscripts in TypeScript that you can use to get started quickly. It also includes this library by default.
If you like using this library, please consider supporting the development ❤️


Preamble:

This library is written in TypeScript with builtin TypeScript declarations, but it works just as well in plain JavaScript after removing the : type annotations from the example code snippets.
The library supports importing an ESM, CommonJS or global variable definition bundle, depending on your use case.

The signatures and example snippets use TypeScript with ESM import syntax to show which types need to be provided and will be returned.
If the signature section contains multiple signatures, each one represents an overload. They will be further explained in the description below that section.

Each feature's example code snippet can be expanded by clicking on the text ▷ Example - click to view below its description.

Some features require the @run-at or @grant directives to be tweaked in the userscript header, or have other specific requirements and limitations. These will be listed in a section marked by a warning emoji (⚠️) each.

Note

In version 10.0.0, many of the platform-agnostic features were moved to the CoreUtils library.
Everything in CoreUtils is re-exported by UserUtils for backwards compatibility, so installing both at the same time isn't usually necessary.
Beware that when both are installed, class inheritance between the two libraries will only work if the installed version of CoreUtils matches the version of CoreUtils that is included in UserUtils (refer to package.json), so that the final bundler is able to deduplicate them correctly. See also const versions

Tip

If you need help with something, please create a new discussion or join my Discord server.
For bug reports or feature requests, please use the GitHub issue tracker.


Table of Contents:

Note

🟣 = function
🟧 = class
🔷 = type
🟩 = const




Features:


DOM:

class Dialog

Signature:

class Dialog extends NanoEmitter;

Usage:

const dialog = new Dialog(options: DialogOptions);

A class that creates a customizable modal dialog with a title (optional), body and footer (optional).
There are tons of options for customization, like changing the close behavior, translating strings and more.

To see all available options, refer to the DialogOptions type.

  • ⚠️ Each instance should have a unique ID, else the elements will conflict with each other in the DOM.
Example - click to view
import { Dialog } from "@sv443-network/userutils";

const myDialog = new Dialog({
  id: "my-unique-dialog-id",
  width: 450,
  height: 250,
  closeOnBgClick: true,
  closeOnEscPress: true,
  destroyOnClose: false,
  unmountOnClose: false,
  small: true,
  verticalAlign: "top",
  renderHeader: () => {
    const header = document.createElement("div");
    header.textContent = "My Custom Dialog";
    return header;
  },
  renderBody: () => {
    const body = document.createElement("div");
    body.textContent = "This is the body of the dialog.";
    return body;
  },
  renderFooter: () => {
    const footer = document.createElement("div");
    const closeButton = document.createElement("button");
    closeButton.textContent = "Close";
    closeButton.addEventListener("click", () => myDialog.close());
    footer.appendChild(closeButton);
    return footer;
  },
});

// register some event listeners:
myDialog.on("open", () => {
  console.log("Dialog opened!");
});

myDialog.on("close", () => {
  console.log("Dialog closed!");
});

myDialog.on("destroy", () => {
  console.log("Dialog destroyed!");
});

// open the dialog:
await myDialog.open();

// pause async execution until the dialog is closed:
await myDialog.once("close");

// destroy the dialog when done:
myDialog.destroy();

Events

The Dialog class inherits from NanoEmitter, so you can use all of its inherited methods to listen to the following events:

Event Arguments Description
close - Emitted just after the dialog is closed
open - Emitted just after the dialog is opened
render - Emitted just after the dialog contents are rendered
clear - Emitted just after the dialog contents are cleared
destroy - Emitted just after the dialog is destroyed and all listeners are removed

Methods

Dialog.mount()

Signature:

public async mount(): Promise<HTMLElement | void>;

Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM.


Dialog.unmount()

Signature:

public unmount(): void;

Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call.


Dialog.remount()

Signature:

public async remount(): Promise<void>;

Clears the DOM of the dialog and then renders it again.
This can be used to call the rendering functions again to update the dialog contents.


Dialog.open()

Signature:

public async open(e?: MouseEvent | KeyboardEvent): Promise<HTMLElement | void>;

Opens the dialog - also mounts it if it hasn't been mounted yet.
Prevents default action and immediate propagation of the passed event.


Dialog.close()

Signature:

public close(e?: MouseEvent | KeyboardEvent): void;

Closes the dialog - prevents default action and immediate propagation of the passed event.


Dialog.isOpen()

Signature:

public isOpen(): boolean;

Returns true if the dialog is currently open.


Dialog.isMounted()

Signature:

public isMounted(): boolean;

Returns true if the dialog is currently mounted.


Dialog.destroy()

Signature:

public destroy(): void;

Clears the DOM of the dialog and removes all event listeners.


Dialog.getCurrentDialogId()

Signature:

public static getCurrentDialogId(): string | null;

Returns the ID of the top-most dialog (the dialog that has been opened last).


Dialog.getOpenDialogs()

Signature:

public static getOpenDialogs(): string[];

Returns the IDs of all currently open dialogs, top-most first.


Types

type DialogOptions

The options object for the Dialog class.
These are the properties:

Property Type Description
id string ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules!
width number Target and max width of the dialog in pixels
height number Target and max height of the dialog in pixels
closeOnBgClick? boolean | undefined Whether the dialog should close when the background is clicked - defaults to true
closeOnEscPress? boolean | undefined Whether the dialog should close when the escape key is pressed - defaults to true
destroyOnClose? boolean | undefined Whether the dialog should be destroyed when it's closed - defaults to false
unmountOnClose? boolean | undefined Whether the dialog should be unmounted when it's closed - defaults to true - superseded by destroyOnClose
removeListenersOnDestroy? boolean | undefined Whether all listeners should be removed when the dialog is destroyed - defaults to true
small? boolean | undefined Whether the dialog should have a smaller overall appearance - defaults to false
verticalAlign? "top" | "center" | "bottom" | undefined Where to align or anchor the dialog vertically - defaults to "center"
strings? Partial<typeof defaultStrings> | undefined Strings used in the dialog (used for translations) - defaults to the default English strings exported as defaultStrings
dialogCss? string | undefined CSS to apply to the dialog - defaults to the exported constant defaultDialogCss
renderBody () => HTMLElement | Promise<HTMLElement> Called to render the body of the dialog
renderHeader? (() => HTMLElement | Promise<HTMLElement>) | undefined Called to render the header of the dialog - leave undefined for a blank header
renderFooter? (() => HTMLElement | Promise<HTMLElement>) | undefined Called to render the footer of the dialog - leave undefined for no footer
renderCloseBtn? (() => HTMLElement | Promise<HTMLElement>) | undefined Called to render the close button of the dialog - leave undefined for no close button



class SelectorObserver

Signature:

class SelectorObserver;

Usage:

// using a valid, mounted Element as the base element:
new SelectorObserver(baseElement: Element, options?: SelectorObserverConstructorOptions);
// using a selector string to find the base element when needed:
new SelectorObserver(baseElementSelector: string, options?: SelectorObserverConstructorOptions);

A class that manages listeners that are called when elements at given CSS 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 as soon as observation is enabled.
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 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"] }

⚠️ 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.

Example - click to view
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 <body> 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,
    defaultDebounceType: "immediate",
  });

  barObserver.addListener("#my-element", {
    listener: (element) => {
      console.log("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<HTMLInputElement>("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<HTMLInputElement>
      console.log("Input elements found:", elements);
    },
  });

  bazObserver.enable();

  window.addEventListener("something", () => {
    // remove the listener after the event "something" was dispatched:
    unsubscribe();
  });
});

Methods

SelectorObserver.addListener()

Signature:

public addListener<TElem extends Element = HTMLElement>(
  selector: string,
  options: SelectorListenerOptions<TElem>
): UnsubscribeFunction;

Starts observing the children of the base element for changes to the given selector according to the set options.
Returns a function that can be called to remove this listener.

The options object has the following properties:

Property Type Description
listener (element: TElem) => void or (elements: NodeListOf<TElem>) => void Gets called whenever the selector is found in the DOM
all? boolean | undefined Whether to use querySelectorAll() instead - defaults to false
continuous? boolean | undefined Whether to call the listener continuously instead of just once - defaults to false
debounce? number | undefined Whether to debounce the listener to reduce calls - set undefined or <=0 to disable (default)
debounceType? DebouncerType | undefined The edge type of the debouncer - defaults to "immediate"

SelectorObserver.enable()

Signature:

public enable(immediatelyCheckSelectors?: boolean): boolean;

Enables or reenables the observation of the child elements.
immediatelyCheckSelectors defaults to true, which means all previously registered selectors will be checked.
Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found).


SelectorObserver.disable()

Signature:

public disable(): void;

Disables the observation of the child elements.


SelectorObserver.isEnabled()

Signature:

public isEnabled(): boolean;

Returns whether the observation of the child elements is currently enabled.


SelectorObserver.clearListeners()

Signature:

public clearListeners(): void;

Removes all listeners that have been registered with addListener().


SelectorObserver.removeAllListeners()

Signature:

public removeAllListeners(selector: string): boolean;

Removes all listeners for the given selector.
Returns true when all listeners for the associated selector were found and removed, false otherwise.


SelectorObserver.removeListener()

Signature:

public removeListener(selector: string, options: SelectorListenerOptions): boolean;

Removes a single listener for the given selector and options.
Returns true when the listener was found and removed, false otherwise.


SelectorObserver.getAllListeners()

Signature:

public getAllListeners(): Map<string, SelectorListenerOptions<HTMLElement>[]>;

Returns all listeners that have been registered with addListener().


SelectorObserver.getListeners()

Signature:

public getListeners(selector: string): SelectorListenerOptions<HTMLElement>[] | undefined;

Returns all listeners for the given selector or undefined if there are none.


Types

type SelectorObserverConstructorOptions

Options object passed to the SelectorObserver constructor.
Extends MutationObserverInit with the following additional properties:

Property Type Description
defaultDebounce? number | undefined If set, applies this debounce in milliseconds to all listeners that don't have their own debounce set
defaultDebounceType? DebouncerType | undefined If set, applies this debounce edge type to all listeners that don't have their own set - defaults to "immediate"
disableOnNoListeners? boolean | undefined Whether to disable the observer when no listeners are present - defaults to false
enableOnAddListener? boolean | undefined Whether to ensure the observer is enabled when a new listener is added - defaults to true
checkInterval? number | undefined If set to a number, the checks will be run on interval instead of on mutation events - all MutationObserverInit props will be ignored

type SelectorListenerOptions

Options object passed to SelectorObserver.addListener().
See the table in that section for more details.



function getUnsafeWindow()

Signature:

function 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
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);



function isDomLoaded()

Signature:

function isDomLoaded(): boolean;

Returns whether or not the DOM has finished loading and can be queried and modified.

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).

Example - click to view
import { isDomLoaded } from "@sv443-network/userutils";

console.log(isDomLoaded()); // false

document.addEventListener("DOMContentLoaded", () => {
  console.log(isDomLoaded()); // true
});



function onDomLoad()

Signature:

function onDomLoad(cb?: () => void): Promise<void>;

Executes a callback and/or resolves the returned Promise when the DOM has finished loading.
Immediately executes/resolves if the DOM is already loaded.

Example - click to view
import { onDomLoad } from "@sv443-network/userutils";

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.");
});



function addParent()

Signature:

function addParent<TElem extends Element, TParentElem extends Element>(
  element: TElem,
  newParent: TParentElem
): TParentElem;

Adds a parent container around the provided element and returns the new parent element.
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
import { addParent } from "@sv443-network/userutils";

const element = document.querySelector("#element");
const newParent = document.createElement("a");
newParent.href = "https://example.org/";

addParent(element, newParent);



function addGlobalStyle()

Signature:

function addGlobalStyle(style: string): HTMLStyleElement;

Adds global CSS style in the form of a <style> element in the document's <head>.
Returns the created style element.

⚠️ 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
import { addGlobalStyle } from "@sv443-network/userutils";

document.addEventListener("DOMContentLoaded", () => {
  addGlobalStyle(`
    body {
      background-color: red;
    }
  `);
});



function preloadImages()

Signature:

function preloadImages(srcUrls: string[], rejects?: boolean): Promise<PromiseSettledResult<HTMLImageElement>[]>;

Preloads an array of image URLs so they can be loaded instantly from the browser cache later on.
The rejects parameter defaults to false. If set to true, the returned PromiseSettledResults will contain rejections for any of the images that failed to load.
Each resolved result will contain the loaded image element, while each rejected result will contain an ErrorEvent.

Example - click to view
import { preloadImages } from "@sv443-network/userutils";

preloadImages([
  "https://example.org/image1.png",
  "https://example.org/image2.png",
  "https://example.org/image3.png",
], true)
  .then((results) => {
    console.log("Images preloaded. Results:", results);
  });



function openInNewTab()

Signature:

function openInNewTab(href: string, background?: boolean, additionalProps?: Partial<HTMLAnchorElement>): void;

Tries to use GM.openInTab to open the given URL in a new tab, otherwise if the grant is not given, creates an invisible anchor element and clicks it.
If background is set to true, the tab will be opened in the background. Leave undefined to use the browser's default behavior.
If additionalProps is set and GM.openInTab is not available, the given properties will be added or overwritten on the created anchor element.

⚠️ You should add the @grant GM.openInTab directive, otherwise only the fallback behavior will be used.
⚠️ For the fallback to work, this function needs to be run in response to a user interaction event, else the browser might reject it.

Example - click to view
import { openInNewTab } from "@sv443-network/userutils";

document.querySelector("#my-button").addEventListener("click", () => {
  openInNewTab("https://example.org/", true);
});



function interceptEvent()

Signature:

function interceptEvent<
  TEvtObj extends EventTarget,
  TPredicateEvt extends Event,
>(
  eventObject: TEvtObj,
  eventName: Parameters<TEvtObj["addEventListener"]>[0],
  predicate?: (event: TPredicateEvt) => boolean
): void;

Intercepts the specified event on the passed object and prevents it from being called if the called predicate function returns a truthy value.
If no predicate is specified, all events will be discarded.
Calling this function will set Error.stackTraceLimit = 100 (if not already higher) to ensure the stack trace is preserved.

⚠️ This function should be called as soon as possible (I recommend using @run-at document-start), as it will only intercept events that are added after this function is called.
⚠️ Due to this function modifying the addEventListener prototype, it might break execution of the page's main script if the userscript is running in an isolated context (like it does in FireMonkey). In that case, calling this function will throw a PlatformError.

Example - click to view
import { interceptEvent } from "@sv443-network/userutils";

interceptEvent(document.body, "click", (event) => {
  // prevent all click events on <a> elements within the entire <body>
  if(event.target instanceof HTMLAnchorElement) {
    console.log("Intercepting click event:", event);
    return true;
  }
  return false; // allow all other click events through
});



function interceptWindowEvent()

Signature:

function interceptWindowEvent<TEvtKey extends keyof WindowEventMap>(
  eventName: TEvtKey,
  predicate?: (event: WindowEventMap[TEvtKey]) => boolean
): void;

Intercepts the specified event on the unsafeWindow (if available) or window object and prevents it from being called if the called predicate function returns a truthy value.
If no predicate is specified, all events will be discarded.
This is essentially the same as interceptEvent(), but automatically uses the unsafeWindow or window, depending on availability.

⚠️ This function should be called as soon as possible (I recommend using @run-at document-start), as it will only intercept events that are added after this function is called.
⚠️ In order to have the best chance at intercepting events in a userscript, the directive @grant unsafeWindow should be set.

Example - click to view
import { interceptWindowEvent } from "@sv443-network/userutils";

// prevent the "Are you sure you want to leave this page?" popup:
interceptWindowEvent("beforeunload");

// discard all context menu commands that are not within #my-element:
interceptWindowEvent("contextmenu", (event) =>
  event.target instanceof HTMLElement && !event.target.closest("#my-element")
);



function isScrollable()

Signature:

function isScrollable(element: Element): Record<"vertical" | "horizontal", boolean>;

Checks if an element has a horizontal or vertical scroll bar.
Uses the computed style of the element, so it has a high chance of working even if the element is hidden.

⚠️ The element needs to be mounted in the DOM so the CSS engine evaluates it, otherwise no scroll bars can be detected.

Example - click to view
import { isScrollable } from "@sv443-network/userutils";

const element = document.querySelector("#element");
const { horizontal, vertical } = isScrollable(element);

console.log("Element has a horizontal scroll bar:", horizontal);
console.log("Element has a vertical scroll bar:", vertical);



function observeElementProp()

Signature:

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;

Executes the callback when the passed element's property changes.
Contrary to an element's attributes, properties can usually not be observed with a MutationObserver.
This function shims the getter and setter of the property to invoke the callback.

When using TypeScript, the types for element, property and the arguments for callback will be automatically inferred.

Example - click to view
import { observeElementProp } from "@sv443-network/userutils";

const myInput = document.querySelector("input#my-input");

observeElementProp(myInput, "value", (oldValue, newValue) => {
  console.log("Value changed from", oldValue, "to", newValue);
});



function getSiblingsFrame()

Signature:

function getSiblingsFrame<TSibling extends Element = HTMLElement>(
  refElement: Element,
  siblingAmount: number,
  refElementAlignment?: "center-top" | "center-bottom" | "top" | "bottom",
  includeRef?: boolean
): TSibling[];

Returns a "frame" of the closest siblings of the refElement, based on the passed amount of siblings and refElementAlignment.

These are the parameters:

  • refElement - The reference element to return the relative closest siblings from.
  • siblingAmount - The amount of siblings to return in total.
  • refElementAlignment - Can be set to center-top (default), center-bottom, top, or bottom, which will determine where the relative location of the provided refElement is in the returned array.
  • includeRef - If set to true (default), the provided refElement will be included in the returned array at its corresponding position.
Example - click to view
import { getSiblingsFrame } from "@sv443-network/userutils";

const refElement = document.querySelector("#ref");
// ^ structure of the elements:
// <div id="parent">
//     <div>1</div>
//     <div>2</div>
//     <div id="ref">3</div>
//     <div>4</div>
//     <div>5</div>
//     <div>6</div>
// </div>

// ref element aligned to the top, included in the result:
const siblings = getSiblingsFrame(refElement, 3, "top", true);
// [<div id="ref">3</div>, <div>4</div>, <div>5</div>]



function setInnerHtmlUnsafe()

Signature:

function setInnerHtmlUnsafe<TElement extends Element = HTMLElement>(
  element: TElement,
  html: string
): TElement;

Sets the innerHTML property of the provided element without any sanitization or validation.
Uses a Trusted Types policy on Chromium-based browsers to trick the browser into thinking the HTML is safe.
Returns the element that was passed for chaining.

⚠️ This function does not perform any sanitization and should thus be used with utmost caution, as it can easily lead to XSS vulnerabilities when used with untrusted input!
⚠️ Only use this function when absolutely necessary, prefer using element.textContent = "foo" or other safer alternatives like the DOMPurify library whenever possible.

Example - click to view
import { setInnerHtmlUnsafe } from "@sv443-network/userutils";

const myElement = document.querySelector("#my-element");
setInnerHtmlUnsafe(myElement, "<img src='https://picsum.photos/100/100' />");



function probeElementStyle()

Signature:

function probeElementStyle<
  TValue,
  TElem extends HTMLElement = HTMLSpanElement,
>(
  probeStyle: (style: CSSStyleDeclaration, element: TElem) => TValue,
  element?: TElem | (() => TElem),
  hideOffscreen?: boolean,
  parentElement?: HTMLElement
): TValue;

Creates an invisible temporary element to probe its rendered computed style.
This might be useful for resolving the value behind a CSS variable, getting the browser's default font size, etc.

⚠️ This function can only be called after the DOM has loaded (when using @run-at document-end or after DOMContentLoaded has fired).

  • probeStyle - Function to probe the element's style. First argument is the element's style object, second argument is the element itself.
  • element - The element to probe, or a function that creates and returns the element. All probe elements will have the class _uu_probe_element added. Defaults to a <span> element.
  • hideOffscreen - Whether to hide the element offscreen (default: true). Disable if you want to probe position style properties.
  • parentElement - The parent element to append the probe element to (default: document.body).
Example - click to view
import { probeElementStyle } from "@sv443-network/userutils";

document.addEventListener("DOMContentLoaded", () => {
  const probedCol = probeElementStyle(
    (style) => style.backgroundColor,
    () => {
      const elem = document.createElement("span");
      elem.style.backgroundColor = "var(--my-cool-color, #000)";
      return elem;
    },
    true,
  );

  console.log("Resolved:", probedCol);
});



Misc:

class GMStorageEngine

Signature:

class GMStorageEngine<TData extends object> extends DataStoreEngine<TData>;

Usage:

const engine = new GMStorageEngine(options?: GMStorageEngineOptions);

Storage engine for the DataStore class that uses GreaseMonkey's GM.getValue and GM.setValue functions.
This class can also be used standalone for an abstracted, uniform interface to GM storage, but is primarily intended to be used as a storage engine for DataStore.
Refer to the DataStore documentation for more information on how to use DataStore and storage engines.

  • ⚠️ Requires the grants GM.getValue, GM.setValue, GM.deleteValue, and GM.listValues in your userscript metadata.
  • ⚠️ To avoid having to specify index signatures for the data type, the template generic TData is constrained to object. However, only values serializable by JSON.stringify() and storable in GM storage (e.g. no functions, symbols, BigInts, etc.) are supported.
  • ⚠️ Don't reuse engine instances, always create a new one for each DataStore instance.
Example - click to view
import { DataStore, GMStorageEngine } from "@sv443-network/userutils";

const myStore = new DataStore({
  id: "my-data",
  defaultData: { foo: "bar" },
  formatVersion: 1,
  engine: new GMStorageEngine(),
});

await myStore.loadData();
console.log(myStore.getData()); // { foo: "bar" }

Methods

GMStorageEngine.getValue()

Signature:

public async getValue<TValue extends SerializableVal = string>(name: string, defaultValue: TValue): Promise<string | TValue>;

Fetches a value from persistent GM storage.


GMStorageEngine.setValue()

Signature:

public async setValue<TValue extends SerializableVal = string>(name: string, value: TValue): Promise<void>;

Sets a value in persistent GM storage.


GMStorageEngine.deleteValue()

Signature:

public async deleteValue(name: string): Promise<void>;

Deletes a value from persistent GM storage.


GMStorageEngine.deleteStorage()

Signature:

public async deleteStorage(): Promise<void>;

Deletes all values from the GM storage.


Types

type GMStorageEngineOptions

Options for the GMStorageEngine class.

Property Type Description
dataStoreOptions? DataStoreEngineDSOptions<TData extends object> | undefined Specifies the necessary options for storing data - ⚠️ Only specify this if you are using this instance standalone! The parent DataStore will set this automatically.



class Mixins

Signature:

class Mixins<
  TMixinMap extends Record<string, (arg: any, ctx?: any) => any>,
  TMixinKey extends Extract<keyof TMixinMap, string> = Extract<keyof TMixinMap, string>,
>;

Usage:

const mixins = new Mixins<TMixinMap>(config?: Partial<MixinsConstructorConfig>);

A class for creating mixin functions that allow multiple sources to modify a target value in a highly flexible way.
Mixins are identified via their string key and can be added with add().
When calling resolve(), all registered mixin functions with the same key will be applied to the input value in the order of their priority.
If a mixin function has its stopPropagation flag set, no further mixin functions will be applied after it.

The TMixinMap template generic defines the mixin functions. Keys are the mixin names and values are functions that take the value as the first argument and an optional context object as the second, and return the modified value.
Important: the first argument and return type need to be the same. Also, if a context object is defined, it must be passed as the third argument in resolve().

Example - click to view
import { Mixins } from "@sv443-network/userutils";

const myMixins = new Mixins<{
  myValue: (val: number, ctx: { factor: number }) => Promise<number>;
}>({
  autoIncrementPriority: true,
});

myMixins.add("myValue", (val, { factor }) => val * factor);
myMixins.add("myValue", (val) => Promise.resolve(val + 1));
myMixins.add("myValue", (val) => val * 2, 1);

const result = await myMixins.resolve("myValue", 10, { factor: 0.75 });
// order of operations:
// 1. 10 * 2 = 20     (priority 1)
// 2. 20 * 0.75 = 15  (priority 0, index 0)
// 3. 15 + 1 = 16     (priority 0, index 1)
// result = 16

Methods

Mixins.add()

Signature:

public add<TKey extends TMixinKey, TArg, TCtx>(
  mixinKey: TKey,
  mixinFn: (arg: TArg, ...ctx: TCtx extends undefined ? [void] : [TCtx]) => ReturnType<TMixinMap[TKey]>,
  config?: Partial<MixinConfig> | number
): () => void;

Registers a mixin function for the given key.
If a number is passed as config, it will be treated as the priority.
Returns a cleanup function that removes this mixin when called.

Mixins with the highest priority will be applied first. If two or more mixins share the exact same priority, they will be executed in order of registration (first come, first serve).


Mixins.resolve()

Signature:

public resolve<TKey extends TMixinKey, TArg, TCtx>(
  mixinKey: TKey,
  inputValue: TArg,
  ...inputCtx: TCtx extends undefined ? [void] : [TCtx]
): ReturnType<TMixinMap[TKey]>;

Applies all mixins with the given key to the input value, respecting the priority and stopPropagation settings.
If some of the mixins are async, the method will also return a Promise.


Mixins.list()

Signature:

public list(): ({ key: string } & MixinConfig)[];

Returns an array of objects that contain the mixin keys and their configuration objects, but not the mixin functions themselves.


Types

type MixinsConstructorConfig

Configuration object for the Mixins class.

Property Type Description
autoIncrementPriority boolean If true, an auto-incrementing integer priority will be used when none is specified (unique per mixin key). Defaults to false.
defaultPriority number The default priority for mixins that do not specify one. Defaults to 0.
defaultStopPropagation boolean The default stopPropagation value. Defaults to false.
defaultSignal? AbortSignal | undefined The default AbortSignal for mixins that do not specify one.

type MixinConfig

Configuration object for an individual mixin function.

Property Type Description
priority number The higher, the earlier the mixin will be applied. Supports floating-point and negative numbers. Defaults to 0.
stopPropagation boolean If true, no further mixins will be applied after this one.
signal? AbortSignal | undefined If set, the mixin will only be applied if the given signal is not aborted.



const versions

An object containing the current version of the library and its re-exported dependency CoreUtils.
These versions are semver-compliant, without any prefix like v or range specifiers like ^, but might still contain suffixes like -beta.1 for pre-release versions.

  • ⚠️ If you want to install both libraries at the same time, make sure to use this object to check that your installed version of CoreUtils matches the one that UserUtils is re-exporting, to avoid potential compatibility issues like broken class inheritance or feature mismatches. For most use cases, it should suffice to just use the re-exported CoreUtils features.
{
  UserUtils: string; // semver-compliant version of this library
  CoreUtils: string; // semver-compliant version of the re-exported CoreUtils library
}



Translation:

UserUtils' translation system is simpler than other industry standard libraries, but still powerful and flexible. It features support for nested keys, pattern-based transformation functions (with some predefined ones included), and various utility functions for runtime translation management, as well as full TypeScript type safety and autocomplete for translation keys.

The main translation functions are tr.for() and tr.use(), but there are also utility functions like tr.hasKey(), tr.addTranslations(), tr.getTranslations(), and more.
For fallbacks, tr.setFallbackLanguage() can be used to default to a specific language when a translation key is not found for the requested language.

Use the type TrKeys for creating a TS union type out of the keys of a translation object, which also works with nested keys, to provide better autocomplete and type safety when using tr.for() and tr.use().
The type TrObject defines the shape of the translation objects that are registered with tr.addTranslations().


function tr.for()

Signature:

function tr.for<TTrKey extends string = string>(
  language: string,
  key: TTrKey,
  ...args: (Stringifiable | Record<string, Stringifiable>)[]
): string;

Returns the translated text for the specified key in the specified language.
If the key is not found in the specified previously registered translation, the key itself is returned.

  • ⚠️ Remember to register a language with tr.addTranslations() before using this function, otherwise it will always return the key itself.
  • By default, translation strings are returned as they are, but using the function tr.addTransform() you can add functions that modify the translation string in various ways.
    UserUtils comes with some predefined transforms out of the box, but custom ones are also easy to create for any other use case. Refer to the type TransformTuple for details.
Example - click to view
import { tr, type TrObject, type TrKeys } from "@sv443-network/userutils";

// create translation object:
const transEn = {
  hello: "Hello, World!",
  nested: {
    key: "This is a nested key",
  },
  foo: "Foo: %1",
} as const satisfies TrObject;
// ^ `as const satisfies` ensures that the literal structural type is the one used by TS, while also ensuring it still matches `TrObject`

// create union type of all translation keys, including nested ones:
type KeysEn = TrKeys<typeof transEn>; // "hello" | "nested.key" | "foo"
// ^ it's recommended to create a type out of the keys of the most complete translation object (usually the same as the fallback language) and use that type for all calls to `tr.for()` and `tr.use()`, even for other languages that might have fewer keys.

// add translations object for "en" language:
tr.addTranslations("en", transEn);

// register the %n positional argument transform:
tr.addTransform(tr.transforms.percent);

// use tr.for() with autocomplete for the keys:
tr.for<KeysEn>("en", "hello");      // "Hello, World!"
tr.for<KeysEn>("en", "nested.key"); // "This is a nested key"
tr.for<KeysEn>("en", "foo", "bar"); // "Foo: bar" (using the predefined positional argument transform)

// using fallback language:
tr.for<KeysEn>("de", "hello"); // "hello" (key not found, returns key itself)
tr.setFallbackLanguage("en");
tr.for<KeysEn>("de", "hello"); // "Hello, World!" (fallback to English)



function tr.use()

Signature:

function tr.use<TTrKey extends string = string>(
  language: string
): (key: TTrKey, ...args: (Stringifiable | Record<string, Stringifiable>)[]) => string;

Creates a translation function for the specified language, allowing you to translate multiple strings without repeating the language parameter.
The returned function works exactly like tr.for(), minus the language parameter.

  • ⚠️ Remember to register a language with tr.addTranslations() before using this function, otherwise it will always return the key itself.
  • By default, translation strings are returned as they are, but using the function tr.addTransform() you can add functions that modify the translation string in various ways.
    UserUtils comes with some predefined transforms out of the box, but custom ones are also easy to create for any other use case. Refer to the type TransformTuple for details.
Example - click to view
import { tr, type TrObject, type TrKeys } from "@sv443-network/userutils";

// create translation object:
const transEn = {
  hello: "Hello, World!",
  nested: {
    key: "This is a nested key",
  },
  foo: "Foo: %1",
} as const satisfies TrObject;
// ^ `as const satisfies` ensures that the literal structural type is the one used by TS, while also ensuring it still matches `TrObject`

// create union type of all translation keys, including nested ones:
type KeysEn = TrKeys<typeof transEn>; // "hello" | "nested.key" | "foo"
// ^ it's recommended to create a type out of the keys of the most complete translation object (usually the same as the fallback language) and use that type for all calls to `tr.for()` and `tr.use()`, even for other languages that might have fewer keys.

// add translations object for "en" language:
tr.addTranslations("en", transEn);

// register the %n positional argument transform:
tr.addTransform(tr.transforms.percent);

// create a translation function for "en" language with autocomplete for the keys:
const t = tr.use<KeysEn>("en");

t("hello");      // "Hello, World!"
t("nested.key"); // "This is a nested key"
t("foo", "bar"); // "Foo: bar" (using the predefined positional argument transform)



function tr.hasKey()

Signature:

function tr.hasKey<TTrKey extends string = string>(
  language?: string,
  key: TTrKey
): boolean;

Checks if a translation key exists in the specified language or the set fallback language.
Returns false if the given language was not registered with tr.addTranslations().

Example - click to view
import { tr } from "@sv443-network/userutils";

tr.addTranslations("en", { hello: "Hello, World!" });

tr.hasKey("en", "hello");   // true
tr.hasKey("en", "goodbye"); // false



function tr.addTranslations()

Signature:

function tr.addTranslations(language: string, translations: TrObject): void;

Registers a new language and its translations. If the language already exists, it will be overwritten.
The translations can be a flat key-value object or infinitely nested objects, resulting in a dot-separated key.

Refer to the type TrObject for more details on the expected shape of the translations object.
In general, the value can either be a string, or another object that follows the same rules, allowing for infinite nesting.
When using nested objects, the keys will be concatenated with dots to form the final translation key. For example, the translation object { nested: { key: "foo" } } will create a translation key of nested.key with the value "foo".

Example - click to view
import { tr, type TrObject, type TrKeys } from "@sv443-network/userutils";

// create translation object:
const transEn = {
  hello: "Hello, World!",
  nested: {
    key: "This is a nested key",
  },
  foo: "Foo: %1",
} as const satisfies TrObject;
// ^ `as const satisfies` ensures that the literal structural type is the one used by TS, while also ensuring it still matches `TrObject`

// create union type of all translation keys, including nested ones:
type KeysEn = TrKeys<typeof transEn>; // "hello" | "nested.key" | "foo"
// ^ it's recommended to create a type out of the keys of the most complete translation object (usually the same as the fallback language) and use that type for all calls to `tr.for()` and `tr.use()`, even for other languages that might have fewer keys.

// add translations object for "en" language:
tr.addTranslations("en", transEn);

// register the %n positional argument transform:
tr.addTransform(tr.transforms.percent);

// create a translation function for "en" language with autocomplete for the keys:
const t = tr.use<KeysEn>("en");

t("hello");      // "Hello, World!"
t("nested.key"); // "This is a nested key"
t("foo", "bar"); // "Foo: bar" (using the predefined positional argument transform)



function tr.getTranslations()

Signature:

function tr.getTranslations(language?: string): TrObject | undefined;

Returns the translation object for the specified language, or undefined if the language is not registered.
If no language is provided, defaults to the fallback language.

Example - click to view
import { tr } from "@sv443-network/userutils";

tr.addTranslations("en", { hello: "Hello, World!" });

tr.getTranslations("en"); // { hello: "Hello, World!" }
tr.getTranslations("de"); // undefined



function tr.getAllTranslations()

Signature:

getAllTranslations(asCopy = true): Record<string, TrObject>;

Returns an object containing all registered translations, where keys are the language codes and values are the translation objects.
If asCopy is set to true (default), the returned object and all nested translation objects will be cloned using JSON.parse(JSON.stringify()) to prevent external mutation. If set to false, the actual internal translation objects will be returned, so any changes to them will affect the translations used by the library and can be used as an alternative to tr.addTranslations() for modifying translations.

Example - click to view
import { tr } from "@sv443-network/userutils";

tr.addTranslations("en", { hello: "Hello, World!" });
tr.addTranslations("de", { hello: "Hallo, Welt!" });

tr.getAllTranslations(); // { en: { hello: "Hello, World!" }, de: { hello: "Hallo, Welt!" } }

const translationsMutable = tr.getAllTranslations(false);
translationsMutable.en.hello = "Hi, World!";

tr.for("en", "hello"); // "Hi, World!"



function tr.deleteTranslations()

Signature:

function tr.deleteTranslations(language: string): boolean;

Deletes the translations for the specified language from memory.
Returns true if the translations were found and deleted, false otherwise.

Example - click to view
import { tr } from "@sv443-network/userutils";

tr.addTranslations("en", { hello: "Hello, World!" });
tr.deleteTranslations("en"); // true
tr.deleteTranslations("de"); // false



function tr.setFallbackLanguage()

Signature:

function tr.setFallbackLanguage(fallbackLanguage?: string): void;

Sets the fallback language to use when a translation key is not found in the given language.
Pass undefined to disable fallbacks and just return the translation key if translations are not found.

Example - click to view
import { tr } from "@sv443-network/userutils";

tr.addTranslations("en", { hello: "Hello!", goodbye: "Goodbye!" });
tr.addTranslations("de", { hello: "Hallo!" });

tr.setFallbackLanguage("en");

const t = tr.use("de");

t("hello");   // "Hallo!"
t("goodbye"); // "Goodbye!" (falls back to "en")



function tr.getFallbackLanguage()

Signature:

function tr.getFallbackLanguage(): string | undefined;

Returns the currently set fallback language, or undefined if no fallback language was set.

Example - click to view
import { tr } from "@sv443-network/userutils";

tr.getFallbackLanguage(); // undefined

tr.setFallbackLanguage("en");

tr.getFallbackLanguage(); // "en"



function tr.addTransform()

Signature:

function tr.addTransform<TTrKey extends string = string>(
  transform: TransformTuple<TTrKey>
): void;

Adds a transform function to the translation system.
Use this to enable dynamic values in translations, for example to insert custom values or to denote a section that could be encapsulated by rich text.
The transform argument is a tuple of [pattern: RegExp, callback: TransformFn]. See the type TransformTuple for more details.

  • Transforms are applied after resolving the initial translation string for any language, in the order they were added.
  • Only when the given RegExp pattern was found inside a translation value, the corresponding TransformFn callback will be executed. As long as that function then correctly modifies and returns the currentValue property (which all the default ones do), it won't matter if transforms are mixed and matched across different translation values. Just make sure that you only use one type of interpolation pattern per translation value to avoid potential conflicts between positional and keyed arguments.
  • ⚠️ If a transform function throws an error, it will propagate up through the translation functions (tr.for(), tr.use(), etc.), so make sure to either handle errors within the transform function itself or wrap translation calls in try/catch blocks.

The TransformFn receives a single parameter which is an object with the following properties:

Property Type Description
currentValue string Current value, possibly in-between transformations. Should be modified and returned by the transform function.
matches RegExpExecArray[] All matches as returned by RegExp.exec()
language string The current or fallback language code.
trKey TTrKey The translation key for which the transform is currently being applied.
trValue string Translation value before any transformations. Use with caution to avoid conflicts with other transforms.
trArgs (Stringifiable | Record<string, Stringifiable>)[] Array of all arguments passed to the translation function.
Example - click to view
import { tr } from "@sv443-network/userutils";

// >> using predefined transforms:
// (scroll down for custom transform example)

tr.addTranslations("en", {
  // uses the templateLiteral transform:
  greeting: "Hello, ${name}!",
  // uses the i18n transform:
  notifications: "You have {{notifs}} notifications.",
  // uses the percent transform:
  status: "Status: %1",
});

// add multiple predefined transforms:
tr.addTransform(tr.transforms.templateLiteral);
tr.addTransform(tr.transforms.i18n);
tr.addTransform(tr.transforms.percent);

const t = tr.use("en");

// the transforms that are both positional and keyed can be used via object or positional arguments:
// templateLiteral transform:
t("greeting", { name: "John" }); // "Hello, John!"
t("greeting", "John");           // "Hello, John!"

// i18n transform:
t("notifications", { notifs: 42 }); // "You have 42 notifications."
t("notifications", 42);             // "You have 42 notifications."

// transforms that only support positional arguments (like the percent transform) will try to stringify all arguments:
// percent transform:
t("status", "Online");                     // "Status: Online"
t("status", { status: "Online" });         // "Status: [object Object]"
t("status", { toString: () => "Online" }); // "Status: Online"


// >> custom transform example:

// creating a custom transform that resolves '<c #hex>text</c>' to '<span style="color: #hex;">text</span>':
tr.addTransform([
  // use g and m flags to match and replace all occurrences:
  /<c\s+#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\s?>(.*?)<\/c>/gm,
  // grab values from the regex groups and return the transformed string:
  ({ matches }) => `<span style="color: #${matches[1]}};">${matches[2]}</span>`,
]);

// add a new translation key while not overwriting the existing ones:
tr.addTranslations("en", {
  ...tr.getTranslations("en"),
  colored: "<c #f00>This is red</c> and <c #0000ff>this is blue</c>.",
});

t("colored"); // "<span style="color: #f00;">This is red</span> and <span style="color: #0000ff;">this is blue</span>."



function tr.deleteTransform()

Signature:

function tr.deleteTransform(patternOrFn: RegExp | TransformFn): boolean;

Removes a transform function from the list of registered transform functions.
Returns true if the transform was found and deleted, false otherwise.

Example - click to view
import { tr, type TransformTuple } from "@sv443-network/userutils";

const myTransform: TransformTuple = [
  /\$\{([a-zA-Z0-9$_-]+)\}/gm,
  ({ matches }) => matches[1] ?? "",
];

tr.deleteTransform(myTransform[0]); // false
tr.addTransform(myTransform);
tr.deleteTransform(myTransform[0]); // true
tr.deleteTransform(myTransform[0]); // false

const tr.transforms

Predefined transform functions for quickly adding custom argument interpolation.
Enable them by passing them to tr.addTransform().

Currently available transforms:

Key Pattern Type(s)
templateLiteral ${key} Keyed / Positional
i18n {{key}} Keyed / Positional
percent %n Positional
Example - click to view
import { tr } from "@sv443-network/userutils";

tr.addTranslations("en", {
  greeting: "Hello, ${name}! You have ${notifs} notifications.",
  message: "Hello, %1! You have %2 notifications.",
});

tr.addTransform(tr.transforms.templateLiteral);
tr.addTransform(tr.transforms.percent);

const t = tr.use("en");

// templateLiteral supports both keyed and positional:
t("greeting", { name: "John", notifs: 42 }); // "Hello, John! You have 42 notifications."
t("greeting", "John", 42);                   // "Hello, John! You have 42 notifications."

// percent is positional only:
t("message", "John", 42); // "Hello, John! You have 42 notifications."

Types

type TrKeys

Signature:

type TrKeys<TTrObj, P extends string = "">;

Generic type that extracts all keys from a flat or recursive translation object into a union type.
Nested keys will be joined with a dot (.).

Example - click to view
import { tr, type TrKeys } from "@sv443-network/userutils";

const trEn = {
  hello: "Hello, World!",
  nested: {
    key: "This is a nested key",
  },
} as const;

tr.addTranslations("en", trEn);

type MyKeys = TrKeys<typeof trEn>; // "hello" | "nested.key"

const t = tr.use<MyKeys>("en");

type TrObject

interface TrObject {
  [key: string]: string | TrObject;
}

Translation object to pass to tr.addTranslations().
Can be a flat object of identifier keys and translation text values, or an infinitely nestable object containing the same.


type TransformFn

type TransformFn<TTrKey extends string = string> = (props: TransformFnProps<TTrKey>) => Stringifiable;

Function that transforms a matched translation string into another string.
It is passed as the second item in the type TransformTuple to the function tr.addTransform().
When the function gets called, it receives a single object parameter. Refer to the function tr.addTransform() documentation for the properties of this object and other important notes on using transform functions.


type TransformTuple

type TransformTuple<TTrKey extends string = string> = [RegExp, TransformFn<TTrKey>];

Translation transform pattern and function in tuple form, passed to tr.addTransform().
Used when creating custom transforms that are not included in the predefined transforms.

The first item in the tuple is a RegExp pattern that is used to find matches in translation values.
Use groups (...) to capture values for later use in the TransformFn via the matches property. (You can also prevent capturing with (?:...) if you just need the parens for the pattern but don't need the values.)
Make sure to use the g and m flags so that the pattern can match multiple occurrences in a single translation value, including translation values that contain \n line breaks.
You can use a website like regex101.com to test and debug your RegExp patterns. Just make sure the flavor is set to ECMAScript (JavaScript) and to include the appropriate flags.

The second item is the TransformFn that transforms the matched translation string into another string.
It is a function that takes a single object parameter. Refer to the function tr.addTransform() documentation for the properties of this object and other important notes on using transform functions.

Example - click to view
import { tr, type TransformTuple } from "@sv443-network/userutils";

// simple transform that turns '[icon:name]' into '<i class="icon" data-icon="name"></i>':
const iconTransform: TransformTuple = [
  /\[icon:([a-zA-Z0-9_-]+)\]/gm,
  ({ matches }) => `<i class="icon" data-icon="${matches[1]}"></i>`,
];

// add the custom transform and the predefined templateLiteral transform:
tr.addTransform(iconTransform);
tr.addTransform(tr.transforms.templateLiteral);

tr.addTranslations("en", {
  warning: "[icon:warning] Warning: ${message}",
});

const t = tr.use("en");

console.log(t("warning", { message: "This is a warning message!" }));
// Output: "<i class="icon" data-icon="warning"></i> Warning: This is a warning message!"



Error classes:

class PlatformError

Signature:

class PlatformError extends DatedError;

Usage:

throw new PlatformError(message: string, options?: ErrorOptions);

Thrown when the current platform doesn't support a certain feature, like calling a DOM function in a non-DOM environment.
Extends from DatedError, which has a date property that contains the date and time when the error was created.

Example - click to view
import { PlatformError } from "@sv443-network/userutils";

if(typeof document === "undefined")
  throw new PlatformError("This feature requires a DOM environment");





Made with ❤️ by Sv443
If you like this library, please consider supporting development