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 much, much more
Installations
npm install @sv443-network/userutils
Developer Guide
Typescript
Yes
Module System
ESM
Node Version
22.12.0
NPM Version
10.9.0
Score
74.9
Supply Chain
99.5
Quality
93.4
Maintenance
100
Vulnerability
100
License
Releases
Contributors
Languages
TypeScript (96.27%)
JavaScript (3.05%)
HTML (0.42%)
CSS (0.26%)
Developer
Sv443-Network
Download Statistics
Total Downloads
5,826
Last Day
1
Last Week
152
Last Month
189
Last Year
3,576
GitHub Statistics
9 Stars
422 Commits
1 Forks
3 Branches
1 Contributors
Bundle Size
30.32 kB
Minified
9.19 kB
Minified + Gzipped
Package Meta Information
Latest Version
8.4.0
Package Id
@sv443-network/userutils@8.4.0
Unpacked Size
348.29 kB
Size
89.75 kB
File Count
21
NPM Version
10.9.0
Node Version
22.12.0
Publised On
22 Dec 2024
Total Downloads
Cumulative downloads
Total Downloads
5,826
Last day
-66.7%
1
Compared to previous day
Last week
1,069.2%
152
Compared to previous week
Last month
-58.5%
189
Compared to previous month
Last year
58.9%
3,576
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
UserUtils
Lightweight library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more.
Contains builtin TypeScript declarations. Supports ESM and CJS imports via a bundler and global declaration via @require
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 ❤️
View the documentation of previous major releases:
8.0.0, 7.0.0, 6.0.0, 5.0.0, 4.0.0, 3.0.0, 2.0.0, 1.0.0, 0.5.3
Table of Contents:
- Installation
- Preamble (info about the documentation)
- License
- Features
- DOM:
SelectorObserver
- class that manages listeners that are called when selectors are found in the DOMgetUnsafeWindow()
- get the unsafeWindow object or fall back to the regular window objectaddParent()
- add a parent element around another elementaddGlobalStyle()
- add a global style to the pagepreloadImages()
- preload images into the browser cache for faster loading later onopenInNewTab()
- open a link in a new tabinterceptEvent()
- conditionally intercepts events registered byaddEventListener()
on any given EventTarget objectinterceptWindowEvent()
- conditionally intercepts events registered byaddEventListener()
on the window objectisScrollable()
- check if an element has a horizontal or vertical scroll barobserveElementProp()
- observe changes to an element's property that can't be observed with MutationObservergetSiblingsFrame()
- returns a frame of an element's siblings, with a given alignment and sizesetInnerHtmlUnsafe()
- set the innerHTML of an element using a Trusted Types policy without sanitizing or escaping it
- Math:
clamp()
- constrain a number between a min and max valuemapRange()
- map a number from one range to the same spot in another rangerandRange()
- generate a random number between a min and max boundarydigitCount()
- calculate the amount of digits in a number
- Misc:
DataStore
- class that manages a hybrid sync & async persistent JSON database, including data migrationDataStoreSerializer
- class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrationsDialog
- class for creating custom modal dialogs with a promise-based API and a generic, default styleNanoEmitter
- tiny event emitter class with a focus on performance and simplicity (based on nanoevents)autoPlural()
- automatically pluralize a stringpauseFor()
- pause the execution of a function for a given amount of timedebounce()
- call a function only once in a series of calls, after or before a given timeoutfetchAdvanced()
- wrapper around the fetch API with a timeout optioninsertValues()
- insert values into a string at specified placeholderscompress()
- compress a string with Gzip or Deflatedecompress()
- decompress a previously compressed stringcomputeHash()
- compute the hash / checksum of a string or ArrayBufferrandomId()
- generate a random ID of a given length and radixconsumeGen()
- consumes a ValueGen and returns the valueconsumeStringGen()
- consumes a StringGen and returns the string
- Arrays:
randomItem()
- returns a random item from an arrayrandomItemIndex()
- returns a tuple of a random item and its index from an arraytakeRandomItem()
- returns a random item from an array and mutates it to remove the itemrandomizeArray()
- returns a copy of the array with its items in a random order
- Translation:
tr()
- simple JSON-based translation system with placeholder and nesting supporttr.forLang()
- translate with the specified language instead of the currently active onetr.addLanguage()
- add a language and its translationstr.setLanguage()
- set the currently active language for translationstr.getLanguage()
- returns the currently active languagetr.getTranslations()
- returns the translations for the given language or the currently active one
- Colors:
hexToRgb()
- convert a hex color string to an RGB or RGBA value tuplergbToHex()
- convert RGB or RGBA values to a hex color stringlightenColor()
- lighten a CSS color string (hex, rgb or rgba) by a given percentagedarkenColor()
- darken a CSS color string (hex, rgb or rgba) by a given percentage
- Utility types for TypeScript:
Stringifiable
- any value that is a string or can be converted to one (implicitly or explicitly)NonEmptyArray
- any array that should have at least one itemNonEmptyString
- any string that should have at least one characterLooseUnion
- a union that gives autocomplete in the IDE but also allows any other value of the same typePrettify
- expands a complex type into a more readable format while keeping functionality the sameValueGen
- a "generator" value that allows for super flexible value typing and declarationStringGen
- a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
- DOM:
Installation:
Shameless plug: I made a template for userscripts in TypeScript that you can use to get started quickly. It also includes this library by default.
-
If you are using a bundler (like webpack, rollup, vite, etc.), you can install this package in one of the following ways:
npm i @sv443-network/userutils pnpm i @sv443-network/userutils yarn add @sv443-network/userutils npx jsr install @sv443-network/userutils deno add jsr:@sv443-network/userutils
Then import it in your script as usual:
1import { addGlobalStyle } from "@sv443-network/userutils"; 2 3// or just import everything (not recommended because of worse treeshaking support): 4 5import * as UserUtils from "@sv443-network/userutils";
-
If you are not using a bundler, want to reduce the size of your userscript, or declared the package as external in your bundler, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
Versioned (recommended):
// @require https://cdn.jsdelivr.net/npm/@sv443-network/userutils@INSERT_VERSION/dist/index.global.js // @require https://unpkg.com/@sv443-network/userutils@INSERT_VERSION/dist/index.global.js
Non-versioned (not recommended because auto-updating):
// @require https://update.greasyfork.org/scripts/472956/UserUtils.js // @require https://openuserjs.org/src/libs/Sv443/UserUtils.js
[!NOTE]
In order for your userscript not to break on a major library update, use one the versioned URLs above after replacingINSERT_VERSION
with the desired version (e.g.8.3.2
) or the versioned URL that's shown at the top of the GreasyFork page.
-
Then, access the functions on the global variable
UserUtils
:1UserUtils.addGlobalStyle("body { background-color: red; }"); 2 3// or using object destructuring: 4 5const { clamp } = UserUtils; 6console.log(clamp(1, 5, 10)); // 5
-
If you're using TypeScript and it complains about the missing global variable
UserUtils
, install the library using the package manager of your choice and add the following inside a.d.ts
file somewhere in the directory (or a subdirectory) defined in yourtsconfig.json
'sbaseUrl
option orinclude
array:1declare const UserUtils: typeof import("@sv443-network/userutils"); 2 3declare global { 4 interface Window { 5 UserUtils: typeof UserUtils; 6 } 7}
- If you're using a linter like ESLint, it might complain about the global variable
UserUtils
not being defined. To fix this, add the following to your ESLint configuration file:1"globals": { 2 "UserUtils": "readonly" 3}
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 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 or join my Discord server.
For submitting bug reports or feature requests, please use the GitHub issue tracker.
License:
This library is licensed under the MIT License.
See the license file for details.
Features:
DOM:
SelectorObserver
Usage:
1new SelectorObserver(baseElement: Element, options?: SelectorObserverOptions)
2new 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 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)defaultDebounceEdge
- can be set to "falling" (default) or "rising", to call the function at (rising) on the very first call and subsequent times after the given debounce time or (falling) the very last call after the debounce time passed with no new calls - seedebounce()
for more info and a diagramcheckInterval
- 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()
Usage: SelectorObserver.addListener<TElement = HTMLElement>(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 theoptions
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 ifoptions.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 aNodeList
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.debounceEdge
is set to "falling" by default, which means the debounce timer will start after the last call of this listener.
If set to "rising", the debounce timer will start after the first call of this listener.
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()
Usage: 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()
Usage: 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()
Usage: SelectorObserver.isEnabled(): boolean
Returns whether the observation of the child elements is currently enabled.
SelectorObserver.clearListeners()
Usage: SelectorObserver.clearListeners(): void
Removes all listeners for all selectors.
SelectorObserver.removeAllListeners()
Usage: SelectorObserver.removeAllListeners(selector: string): boolean
Removes all listeners for the given selector.
SelectorObserver.removeListener()
Usage: SelectorObserver.removeListener(selector: string, options: SelectorListenerOptions): boolean
Removes a specific listener for the given selector and options.
SelectorObserver.getAllListeners()
Usage: SelectorObserver.getAllListeners(): Map<string, SelectorListenerOptions[]>
Returns a Map of all selectors and their listeners.
SelectorObserver.getListeners()
Usage: SelectorObserver.getListeners(selector: string): SelectorListenerOptions[] | undefined
Returns all listeners for the given selector or undefined if there are none.
Examples - click to view
Basic usage:
1import { SelectorObserver } from "@sv443-network/userutils";
2
3// adding a single-shot listener before the element exists:
4const fooObserver = new SelectorObserver("body");
5
6fooObserver.addListener("#my-element", {
7 listener: (element) => {
8 console.log("Element found:", element);
9 },
10});
11
12document.addEventListener("DOMContentLoaded", () => {
13 // starting observation after the <body> element is available:
14 fooObserver.enable();
15
16
17 // adding custom observer options:
18
19 const barObserver = new SelectorObserver(document.body, {
20 // only check if the following attributes change:
21 attributeFilter: ["class", "style", "data-whatever"],
22 // debounce all listeners by 100ms unless specified otherwise:
23 defaultDebounce: 100,
24 // "rising" means listeners are called immediately and use the debounce as a timeout between subsequent calls - see the debounce() function for a better explanation
25 defaultDebounceEdge: "rising",
26 // other settings from the MutationObserver API can be set here too - see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options
27 });
28
29 barObserver.addListener("#my-element", {
30 listener: (element) => {
31 console.log("Element's attributes changed:", element);
32 },
33 });
34
35 barObserver.addListener("#my-other-element", {
36 // set the debounce higher than provided by the defaultDebounce property:
37 debounce: 250,
38 // adjust the debounceEdge back to the default "falling" for this specific listener:
39 debounceEdge: "falling",
40 listener: (element) => {
41 console.log("Other element's attributes changed:", element);
42 },
43 });
44
45 barObserver.enable();
46
47
48 // using custom listener options:
49
50 const bazObserver = new SelectorObserver(document.body);
51
52 // for TypeScript, specify that input elements are returned by the listener:
53 const unsubscribe = bazObserver.addListener<HTMLInputElement>("input", {
54 all: true, // use querySelectorAll() instead of querySelector()
55 continuous: true, // don't remove the listener after it was called once
56 debounce: 50, // debounce the listener by 50ms
57 listener: (elements) => {
58 // type of `elements` is NodeListOf<HTMLInputElement>
59 console.log("Input elements found:", elements);
60 },
61 });
62
63 bazObserver.enable();
64
65 window.addEventListener("something", () => {
66 // remove the listener after the event "something" was dispatched:
67 unsubscribe();
68 });
69
70
71 // use a different element as the base:
72
73 const myElement = document.querySelector("#my-element");
74 if(myElement) {
75 const quxObserver = new SelectorObserver(myElement);
76
77 quxObserver.addListener("#my-child-element", {
78 listener: (element) => {
79 console.log("Child element found:", element);
80 },
81 });
82
83 quxObserver.enable();
84 }
85});
Get and remove listeners:
1import { SelectorObserver } from "@sv443-network/userutils"; 2 3document.addEventListener("DOMContentLoaded", () => { 4 const observer = new SelectorObserver(document.body); 5 6 observer.addListener("#my-element-foo", { 7 continuous: true, 8 listener: (element) => { 9 console.log("Element found:", element); 10 }, 11 }); 12 13 observer.addListener("#my-element-bar", { 14 listener: (element) => { 15 console.log("Element found again:", element); 16 }, 17 }); 18 19 observer.enable(); 20 21 22 // get all listeners: 23 24 console.log(observer.getAllListeners()); 25 // Map(2) { 26 // '#my-element-foo' => [ { listener: [Function: listener] } ], 27 // '#my-element-bar' => [ { listener: [Function: listener] } ] 28 // } 29 30 31 // get listeners for a specific selector: 32 33 console.log(observer.getListeners("#my-element-foo")); 34 // [ { listener: [Function: listener], continuous: true } ] 35 36 37 // remove all listeners for a specific selector: 38 39 observer.removeAllListeners("#my-element-foo"); 40 console.log(observer.getAllListeners()); 41 // Map(1) { 42 // '#my-element-bar' => [ { listener: [Function: listener] } ] 43 // } 44});
Chaining:
1import { SelectorObserver } from "@sv443-network/userutils"; 2import type { SelectorObserverOptions } from "@sv443-network/userutils"; 3 4// apply a default debounce to all SelectorObserver instances: 5const defaultOptions: SelectorObserverOptions = { 6 defaultDebounce: 100, 7}; 8 9document.addEventListener("DOMContentLoaded", () => { 10 // initialize generic observer that in turn initializes "sub-observers": 11 const fooObserver = new SelectorObserver(document.body, { 12 ...defaultOptions, 13 // define any other specific options here 14 }); 15 16 const myElementSelector = "#my-element"; 17 18 // this relatively expensive listener (as it is in the full <body> scope) will only fire once: 19 fooObserver.addListener(myElementSelector, { 20 listener: (element) => { 21 // only enable barObserver once its baseElement exists: 22 barObserver.enable(); 23 }, 24 }); 25 26 // barObserver is created at the same time as fooObserver, but only enabled once #my-element exists 27 const barObserver = new SelectorObserver(element, { 28 ...defaultOptions, 29 // define any other specific options here 30 }); 31 32 // this selector will be checked for immediately after `enable()` is called 33 // and on each subsequent mutation because `continuous` is set to true. 34 // however it is much less expensive as it is scoped to a lower element which will receive less DOM updates 35 barObserver.addListener(".my-child-element", { 36 all: true, 37 continuous: true, 38 listener: (elements) => { 39 console.log("Child elements found:", elements); 40 }, 41 }); 42 43 // immediately enable fooObserver as the <body> is available as soon as "DOMContentLoaded" fires: 44 fooObserver.enable(); 45});
getUnsafeWindow()
Usage:
1getUnsafeWindow(): 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
1import { getUnsafeWindow } from "@sv443-network/userutils"; 2 3// trick the site into thinking the mouse was moved: 4const mouseEvent = new MouseEvent("mousemove", { 5 view: getUnsafeWindow(), 6 screenY: 69, 7 screenX: 420, 8 movementX: 10, 9 movementY: 0, 10}); 11 12document.body.dispatchEvent(mouseEvent);
addParent()
Usage:
1addParent(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
1import { addParent } from "@sv443-network/userutils"; 2 3// add an <a> around an element 4const element = document.querySelector("#element"); 5const newParent = document.createElement("a"); 6newParent.href = "https://example.org/"; 7 8addParent(element, newParent);
addGlobalStyle()
Usage:
1addGlobalStyle(css: string): HTMLStyleElement
Adds a global style to the page in form of a <style>
element that's inserted into the <head>
.
Returns the style element that was just created.
⚠️ 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
1import { addGlobalStyle } from "@sv443-network/userutils"; 2 3document.addEventListener("DOMContentLoaded", () => { 4 addGlobalStyle(` 5 body { 6 background-color: red; 7 } 8 `); 9});
preloadImages()
Usage:
1preloadImages(urls: string[], rejects?: boolean): Promise<Array<PromiseSettledResult<HTMLImageElement>>>
Preloads images into browser cache by creating an invisible <img>
element for each URL passed.
The images will be loaded in parallel and the returned Promise will only resolve once all images have been loaded.
The resulting PromiseSettledResult array will contain the image elements if resolved, or an ErrorEvent if rejected, but only if rejects
is set to true.
Example - click to view
1import { preloadImages } from "@sv443-network/userutils"; 2 3preloadImages([ 4 "https://example.org/image1.png", 5 "https://example.org/image2.png", 6 "https://example.org/image3.png", 7], true) 8 .then((results) => { 9 console.log("Images preloaded. Results:", results); 10 }) 11 .catch((results) => { 12 console.error("Couldn't preload all images. Results:", results); 13 });
openInNewTab()
Usage:
1openInNewTab(url: string, background?: boolean): void
Tries to use GM.openInTab
to open the given URL in a new tab, or as a fallback 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.
⚠️ Needs the @grant GM.openInTab
directive, otherwise only the fallback behavior will be used and the warning below is extra important:
⚠️ 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
1import { openInNewTab } from "@sv443-network/userutils"; 2 3document.querySelector("#my-button").addEventListener("click", () => { 4 // open in background: 5 openInNewTab("https://example.org/", true); 6});
interceptEvent()
Usage:
1interceptEvent(
2 eventObject: EventTarget,
3 eventName: string,
4 predicate?: (event: Event) => boolean
5): void
Intercepts all events dispatched on the eventObject
and prevents the listeners from being called as long as the predicate function returns a truthy value.
If no predicate is specified, all events will be discarded.
Calling this function will set the Error.stackTraceLimit
to 100 (if it's 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 attached 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 an error.
Example - click to view
1import { interceptEvent } from "@sv443-network/userutils"; 2 3interceptEvent(document.body, "click", (event) => { 4 // prevent all click events on <a> elements within the entire <body> 5 if(event.target instanceof HTMLAnchorElement) { 6 console.log("Intercepting click event:", event); 7 return true; 8 } 9 return false; // allow all other click events through 10});
interceptWindowEvent()
Usage:
1interceptWindowEvent(
2 eventName: string,
3 predicate?: (event: Event) => boolean
4): void
Intercepts all events dispatched on the window
object and prevents the listeners from being called as long as the 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 falls back to regular window
).
⚠️ This function should be called as soon as possible (I recommend using @run-at document-start
), as it will only intercept events that are attached after this function is called.
⚠️ In order to have the best chance at intercepting events, the directive @grant unsafeWindow
should be set.
⚠️ 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 an error.
Example - click to view
1import { interceptWindowEvent } from "@sv443-network/userutils"; 2 3// prevent the pesky "Are you sure you want to leave this page?" popup 4// as no predicate is specified, all events will be discarded by default 5interceptWindowEvent("beforeunload");
isScrollable()
Usage:
1isScrollable(element: Element): { horizontal: boolean, vertical: boolean }
Checks if an element has a horizontal or vertical scroll bar.
This uses the computed style of the element, so it will also work if the element is hidden.
Example - click to view
1import { isScrollable } from "@sv443-network/userutils"; 2 3const element = document.querySelector("#element"); 4const { horizontal, vertical } = isScrollable(element); 5 6console.log("Element has a horizontal scroll bar:", horizontal); 7console.log("Element has a vertical scroll bar:", vertical);
observeElementProp()
Usage:
1observeElementProp(
2 element: Element,
3 property: string,
4 callback: (oldValue: any, newValue: any) => void
5): void
This function observes changes to the given property of a given element.
While regular HTML attributes can be observed using a MutationObserver, this is not always possible for properties that are assigned on the JS object.
This function shims the setter of the provided property and calls the callback function whenever it is changed through any means.
When using TypeScript, the types for element
, property
and the arguments for callback
will be automatically inferred.
Example - click to view
1import { observeElementProp } from "@sv443-network/userutils"; 2 3const myInput = document.querySelector("input#my-input"); 4 5let value = 0; 6 7setInterval(() => { 8 value += 1; 9 myInput.value = String(value); 10}, 1000); 11 12 13const observer = new MutationObserver((mutations) => { 14 // will never be called: 15 console.log("MutationObserver mutation:", mutations); 16}); 17 18// one would think this should work, but "value" is a JS object *property*, not a DOM *attribute* 19observer.observe(myInput, { 20 attributes: true, 21 attributeFilter: ["value"], 22}); 23 24 25observeElementProp(myInput, "value", (oldValue, newValue) => { 26 // will be called every time the value changes: 27 console.log("Value changed from", oldValue, "to", newValue); 28});
getSiblingsFrame()
Usage:
1getSiblingsFrame< 2 TSiblingType extends Element = HTMLElement 3>( 4 refElement: Element, 5 siblingAmount: number, 6 refElementAlignment: "center-top" | "center-bottom" | "top" | "bottom" = "center-top", 7 includeRef = true 8): TSiblingType[]
Returns a "frame" of the closest siblings of the reference element, based on the passed amount of siblings and element alignment.
The returned type is an array of HTMLElement
by default but can be changed by specifying the TSiblingType
generic in TypeScript.
These are the parameters:
- The
refElement
parameter is the reference element to return the relative closest siblings from. - The
siblingAmount
parameter is the amount of siblings to return in total (including or excluding therefElement
based on theincludeRef
parameter). - The
refElementAlignment
parameter can be set tocenter-top
(default),center-bottom
,top
, orbottom
, which will determine where the relative location of the providedrefElement
is in the returned array.
center-top
(default) will try to keep therefElement
in the center of the returned array, but can shift around by one element. In those cases it will prefer the top spot.
Same goes forcenter-bottom
in reverse.
top
will keep therefElement
at the top of the returned array, andbottom
will keep it at the bottom. - If
includeRef
is set totrue
(default), the providedrefElement
will be included in the returned array at its corresponding position.
Example - click to view
1import { getSiblingsFrame } from "@sv443-network/userutils"; 2 3const refElement = document.querySelector("#ref"); 4// ^ structure of the elements: 5// <div id="parent"> 6// <div>1</div> 7// <div>2</div> 8// <div id="ref">3</div> 9// <div>4</div> 10// <div>5</div> 11// <div>6</div> 12// </div> 13 14// ref element aligned to the top of the frame's center positions and included in the result: 15const siblingsFoo = getSiblingsFrame(refElement, 4, "center-top", true); 16// <div>1</div> 17// <div>2</div> ◄──┐ 18// <div id="ref">3</div> │ returned <(ref is here because refElementAlignment = "center-top") 19// <div>4</div> │ frame 20// <div>5</div> ◄──┘ 21// <div>6</div> 22 23// ref element aligned to the bottom of the frame's center positions and included in the result: 24const siblingsBar = getSiblingsFrame(refElement, 4, "center-bottom", true); 25// <div>1</div> ◄──┐ 26// <div>2</div> │ returned 27// <div id="ref">3</div> │ frame <(ref is here because refElementAlignment = "center-bottom") 28// <div>4</div> ◄──┘ 29// <div>5</div> 30// <div>6</div> 31 32// ref element aligned to the bottom of the frame's center positions, but excluded from the result: 33const siblingsBaz = getSiblingsFrame(refElement, 3, "center-bottom", false); 34// <div>1</div> ◄──┐ 35// <div>2</div> ◄──┘ returned... 36// <div id="ref">3</div> <(skipped because includeRef = false) 37// <div>4</div> ◄─── ...frame 38// <div>5</div> 39// <div>6</div> 40 41// ref element aligned to the top of the frame, but excluded from the result: 42const siblingsQux = getSiblingsFrame(refElement, 3, "top", false); 43// <div>1</div> 44// <div>2</div> 45// <div id="ref">3</div> <(skipped because includeRef = false) 46// <div>4</div> ◄──┐ returned 47// <div>5</div> │ frame 48// <div>6</div> ◄──┘ 49 50// ref element aligned to the top of the frame, but this time included in the result: 51const siblingsQuux = getSiblingsFrame(refElement, 3, "top", true); 52// <div>1</div> 53// <div>2</div> 54// <div id="ref">3</div> ◄──┐ returned <(not skipped because includeRef = true) 55// <div>4</div> │ frame 56// <div>5</div> ◄──┘ 57// <div>6</div>
More useful examples:
1const refElement = document.querySelector("#ref"); 2// ^ structure of the elements: 3// <div id="parent"> 4// <div>1</div> 5// <div>2</div> 6// <div id="ref">3</div> 7// <div>4</div> 8// <div>5</div> 9// <div>6</div> 10// </div> 11 12// get all elements above and include the reference element: 13const allAbove = getSiblingsFrame(refElement, Infinity, "top", true); 14// <div>1</div> ◄──┐ returned 15// <div>2</div> │ frame 16// <div id="ref">3</div> ◄──┘ 17// <div>4</div> 18// <div>5</div> 19// <div>6</div> 20 21// get all elements below and exclude the reference element: 22const allBelowExcl = getSiblingsFrame(refElement, Infinity, "bottom", false); 23// <div>1</div> 24// <div>2</div> 25// <div id="ref">3</div> 26// <div>4</div> ◄──┐ returned 27// <div>5</div> │ frame 28// <div>6</div> ◄──┘
setInnerHtmlUnsafe()
Usage:
1setInnerHtmlUnsafe(element: Element, html: string): Element
Sets the innerHTML property of the provided element without any sanitation or validation.
Makes use of the Trusted Types API to trick the browser into thinking the HTML is safe.
Use this function if the page makes use of the CSP directive require-trusted-types-for 'script'
and throws a "This document requires 'TrustedHTML' assignment" error on Chromium-based browsers.
If the browser doesn't support Trusted Types, this function will fall back to regular innerHTML assignment.
⚠️ This function does not perform any sanitization, it only tricks the browser into thinking the HTML is safe and should thus be used with utmost caution, as it can easily cause XSS vulnerabilities!
A much better way of doing this is by using the DOMPurify library to create your own Trusted Types policy that actually sanitizes the HTML and prevents (most) XSS attack vectors.
You can also find more info here.
Example - click to view
1import { setInnerHtmlUnsafe } from "@sv443-network/userutils"; 2 3const myElement = document.querySelector("#my-element"); 4setInnerHtmlUnsafe(myElement, "<img src='https://picsum.photos/100/100' />"); // hardcoded value, so no XSS risk 5 6const myXssElement = document.querySelector("#my-xss-element"); 7const userModifiableVariable = `<img onerror="alert('XSS!')" src="invalid" />`; // let's pretend this came from user input 8setInnerHtmlUnsafe(myXssElement, userModifiableVariable); // <- uses a user-modifiable variable, so big XSS risk!
Math:
clamp()
Usage:
1clamp(num: number, min: number, max: number): number 2clamp(num: number, max: number): number
Clamps a number between a min and max boundary (inclusive).
If only the num
and max
arguments are passed, the min
boundary will be set to 0.
Example - click to view
1import { clamp } from "@sv443-network/userutils"; 2 3clamp(7, 0, 10); // 7 4clamp(7, 10); // 7 (equivalent to the above) 5clamp(-1, 10); // 0 6clamp(5, -5, 0); // 0 7clamp(99999, 0, 10); // 10 8 9// use Infinity to clamp without a min or max boundary: 10clamp(Number.MAX_SAFE_INTEGER, Infinity); // 9007199254740991 11clamp(Number.MIN_SAFE_INTEGER, -Infinity, 0); // -9007199254740991
mapRange()
Usage:
1mapRange(value: number, range1min: number, range1max: number, range2min: number, range2max: number): number
2mapRange(value: number, range1max: number, range2max: number): number
Maps a number from one range to the spot it would be in another range.
If only the max
arguments are passed, the function will set the min
for both ranges to 0.
Example - click to view
1import { mapRange } from "@sv443-network/userutils"; 2 3mapRange(5, 0, 10, 0, 100); // 50 4mapRange(5, 0, 10, 0, 50); // 25 5mapRange(5, 10, 50); // 25 6 7// to calculate a percentage from arbitrary values, use 0 and 100 as the second range 8// for example, if 4 files of a total of 13 were downloaded: 9mapRange(4, 0, 13, 0, 100); // 30.76923076923077
randRange()
Usages:
1randRange(min: number, max: number, enhancedEntropy?: boolean): number
2randRange(max: number, enhancedEntropy?: boolean): number
Returns a random number between min
and max
(inclusive).
If only one argument is passed, it will be used as the max
value and min
will be set to 0.
If enhancedEntropy
is set to true (false by default), the Web Crypto API is used for generating the random numbers.
Note that this makes the function call take longer, but the generated IDs will have a higher entropy.
Example - click to view
1import { randRange } from "@sv443-network/userutils"; 2 3randRange(0, 10); // 4 4randRange(10, 20); // 17 5randRange(10); // 7 6randRange(0, 10, true); // 4 (the devil is in the details) 7 8 9function benchmark(enhancedEntropy: boolean) { 10 const timestamp = Date.now(); 11 for(let i = 0; i < 100_000; i++) 12 randRange(0, 100, enhancedEntropy); 13 console.log(`Generated 100k in ${Date.now() - timestamp}ms`) 14} 15 16// using Math.random(): 17benchmark(false); // Generated 100k in 90ms 18 19// using crypto.getRandomValues(): 20benchmark(true); // Generated 100k in 461ms 21 22// about a 5x slowdown, but the generated numbers are more entropic
digitCount()
Usage:
1digitCount(num: number | Stringifiable): number
Calculates and returns the amount of digits in the given number.
The given value will be converted by being passed to String()
and then Number()
before the calculation.
Returns NaN
if the number is invalid.
Example - click to view
1import { digitCount } from "@sv443-network/userutils"; 2 3const num1 = 123; 4const num2 = 123456789; 5const num3 = " 123456789 "; 6const num4 = Number.MAX_SAFE_INTEGER; 7const num5 = "a123b456c789d"; 8const num6 = parseInt("0x123456789abcdef", 16); 9 10digitCount(num1); // 3 11digitCount(num2); // 9 12digitCount(num3); // 9 13digitCount(num4); // 16 14digitCount(num5); // NaN (because hex conversion has to be done through parseInt(str, 16)), like below: 15digitCount(num6); // 17
Misc:
DataStore
Usage:
1new DataStore(options: DataStoreOptions)
A class that manages a sync & async JSON database that is persistently saved to and loaded from GM storage, localStorage or sessionStorage.
Also supports automatic migration of outdated data formats via provided migration functions.
You may create as many instances as you like as long as they have different IDs.
The class' internal methods are all declared as protected, so you can extend this class and override them if you need to add your own functionality, like changing the location data is stored.
If you have multiple DataStore instances and you want to be able to easily and safely export and import their data, take a look at the DataStoreSerializer class.
It combines the data of multiple DataStore instances into a single object that can be exported and imported as a whole by the end user.
⚠️ The data is stored as a JSON string, so only JSON-compatible data can be used. Circular structures and complex objects will throw an error on load and save or cause otherwise unexpected behavior.
⚠️ The directives @grant GM.getValue
and @grant GM.setValue
are required if the storageMethod is left as the default of "GM"
The options object has the following properties:
Property | Description |
---|---|
id | A unique internal identification string for this instance. If two DataStores share the same ID, they will overwrite each other's data, so it is recommended that you use a prefix that is unique to your project. |
defaultData | The default data to use if no data is saved in persistent storage yet. Until the data is loaded from persistent storage, this will be the data returned by getData() . For TypeScript, the type of the data passed here is what will be used for all other methods of the instance. |
formatVersion | An incremental version of the data format. If the format of the data is changed in any way, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively. Never decrement this number or skip numbers. |
migrations? | (Optional) A dictionary of functions that can be used to migrate data from older versions of the data to newer ones. The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value. The values should be functions that take the data in the old format and return the data in the new format. The functions will be run in order from the oldest to the newest version. If the current format version is not in the dictionary, no migrations will be run. |
migrateIds? | (Optional) A string or array of strings that migrate from one or more old IDs to the ID set in the constructor. If no data exist for the old ID(s), nothing will be done, but some time may still pass trying to fetch the non-existent data. The ID migration will be done once per session in the call to loadData() . |
storageMethod? | (Optional) The method that is used to store the data. Can be "GM" (default), "localStorage" or "sessionStorage" . If you want to store the data in a different way, you can override the methods of the DataStore class. |
encodeData? | (Optional, but required when decodeData is set) Function that encodes the data before saving - you can use compress() here to save space at the cost of a little bit of performance |
decodeData? | (Optional, but required when encodeData is set) Function that decodes the data when loading - you can use decompress() here to decode data that was previously compressed with compress() |
Methods:
DataStore.loadData()
Usage: loadData(): Promise<TData>
Asynchronously loads the data from persistent storage and returns it.
If no data was saved in persistent storage before, the value of options.defaultData
will be returned and also written to persistent storage before resolving.
If the options.migrateIds
property is present and this is the first time calling this function in this session, the data will be migrated from the old ID(s) to the current one.
Then, if the formatVersion
of the saved data is lower than the current one and the options.migrations
property is present, the instance will try to migrate the data to the latest format before resolving, updating the in-memory cache and persistent storage.
DataStore.getData()
Usage: getData(): TData
Synchronously returns the current data that is stored in the internal cache.
If no data was loaded from persistent storage yet using loadData()
, the value of options.defaultData
will be returned.
DataStore.setData()
Usage: setData(data: TData): Promise<void>
Writes the given data synchronously to the internal cache and asynchronously to persistent storage.
DataStore.saveDefaultData()
Usage: saveDefaultData(): Promise<void>
Writes the default data given in options.defaultData
synchronously to the internal cache and asynchronously to persistent storage.
DataStore.deleteData()
Usage: deleteData(): Promise<void>
Fully deletes the data from persistent storage only.
The internal cache will be left untouched, so any subsequent calls to getData()
will return the data that was last loaded.
If loadData()
or setData()
are called after this, the persistent storage will be populated with the value of options.defaultData
again.
This is why you should either immediately repopulate the cache and persistent storage or the page should probably be reloaded or closed after this method is called.
⚠️ If you want to use this method, the additional directive @grant GM.deleteValue
is required.
DataStore.runMigrations()
Usage: runMigrations(oldData: any, oldFmtVer: number, resetOnError?: boolean): Promise<TData>
Runs all necessary migration functions to migrate the given oldData
to the latest format.
If resetOnError
is set to false
, the migration will be aborted if an error is thrown and no data will be committed. If it is set to true
(default) and an error is encountered, it will be suppressed and the defaultData
will be saved to persistent storage and returned.
DataStore.migrateId()
Usage: migrateId(oldIds: string | string[]): Promise<void>
Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.
If no data exist for the old ID(s), nothing will be done, but some time may still pass trying to fetch the non-existent data.
Instead of calling this manually, consider setting the migrateIds
property in the constructor to automatically migrate the data once per session in the call to loadData()
, unless you know that you need to migrate the ID(s) manually.
DataStore.encodingEnabled()
Usage: encodingEnabled(): boolean
Returns true
if both options.encodeData
and options.decodeData
are set, else false
.
Uses TypeScript's type guard notation for easier use in conditional statements.
Example - click to view
1import { DataStore, compress, decompress } from "@sv443-network/userutils";
2
3/** Example: Userscript configuration data */
4interface MyConfig {
5 foo: string;
6 bar: number;
7 baz: string;
8 qux: string;
9}
10
11/** Default data returned by getData() calls until setData() is used and also fallback data if something goes wrong */
12const defaultData: MyConfig = {
13 foo: "hello",
14 bar: 42,
15 baz: "xyz",
16 qux: "something",
17};
18/** If any properties are added to, removed from, or renamed in the MyConfig type, increment this number */
19const formatVersion = 2;
20/** These are functions that migrate outdated data to the latest format - make sure a function exists for every previously used formatVersion and that no numbers are skipped! */
21const migrations = {
22 // migrate from format version 0 to 1
23 1: (oldData: Record<string, unknown>) => {
24 return {
25 foo: oldData.foo,
26 bar: oldData.bar,
27 baz: "world",
28 };
29 },
30 // asynchronously migrate from format version 1 to 2
31 2: async (oldData: Record<string, unknown>) => {
32 // using arbitrary async operations for the new format:
33 const qux = await grabQuxDataAsync();
34 return {
35 foo: oldData.foo,
36 bar: oldData.bar,
37 baz: oldData.baz,
38 qux,
39 };
40 },
41};
42
43// You probably want to export this instance (or helper functions) so you can use it anywhere in your script:
44export const manager = new DataStore({
45 /** A unique ID for this instance */
46 id: "my-userscript-config",
47 /** Default, initial and fallback data */
48 defaultData,
49 /** The current version of the data format - should be a whole number that is only ever incremented */
50 formatVersion,
51 /** Data format migration functions called when the formatVersion is increased */
52 migrations,
53 /** If the data was saved under different ID(s) before, providing them here will make sure the data is migrated to the current ID when `loadData()` is called */
54 migrateIds: ["my-data", "config"],
55 /**
56 * Where the data should be stored.
57 * For example, you could use `"sessionStorage"` to make the data be automatically deleted after the browser session is finished, or use `"localStorage"` if you don't have access to GM storage for some reason.
58 */
59 storageMethod: "localStorage",
60
61 // Compression example:
62 // Adding the following will save space at the cost of a little bit of performance (only for the initial loading and every time new data is saved)
63 // Feel free to use your own functions here, as long as they take in the stringified JSON and return another string, either synchronously or asynchronously
64 // Either both of these properties or none of them should be set
65
66 /** Compresses the data using the "deflate" algorithm and digests it as a string */
67 encodeData: (data) => compress(data, "deflate", "string"),
68 /** Decompresses the "deflate" encoded data as a string */
69 decodeData: (data) => decompress(data, "deflate", "string"),
70});
71
72/** Entrypoint of the userscript */
73async function init() {
74 // wait for the data to be loaded from persistent storage
75 // if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultData will be returned
76 // if the previously saved data needs to be migrated to a newer version, it will happen inside this function call
77 const configData = await manager.loadData();
78
79 console.log(configData.foo); // "hello"
80
81 // update the data
82 configData.foo = "world";
83 configData.bar = 123;
84
85 // save the updated data - synchronously to the cache and asynchronously to persistent storage
86 manager.saveData(configData).then(() => {
87 console.log("Data saved to persistent storage!");
88 });
89
90 // the internal cache is updated synchronously, so the updated data can be accessed before the Promise resolves:
91 console.log(manager.getData().foo); // "world"
92}
93
94init();
DataStoreSerializer
Usage:
1new DataStoreSerializer(stores: DataStore[], options?: DataStoreSerializerOptions)
A class that manages serializing and deserializing (exporting and importing) one to infinite DataStore instances.
The serialized data is a JSON string that can be saved to a file, copied to the clipboard, or stored in any other way.
Each DataStore instance's settings like data encoding are respected and saved next to the exported data.
Also, by default a checksum is calculated and importing data with a mismatching checksum will throw an error.
The class' internal methods are all declared as protected, so you can extend this class and override them if you need to add your own functionality.
⚠️ Needs to run in a secure context (HTTPS) due to the use of the SubtleCrypto API.
The options object has the following properties:
Property | Description |
---|---|
addChecksum? | (Optional) If set to true (default), a SHA-256 checksum will be calculated and saved with the serialized data. If set to false , no checksum will be calculated and saved. |
ensureIntegrity? | (Optional) If set to true (default), the checksum will be checked when importing data and an error will be thrown if it doesn't match. If set to false , the checksum will not be checked and no error will be thrown. If no checksum property exists on the imported data (for example because it wasn't enabled in a previous data format version), the checksum check will be skipped regardless of this setting. |
Methods:
DataStoreSerializer.serialize()
Usage: serialize(): Promise<string>
Serializes all DataStore instances passed in the constructor and returns the serialized data as a JSON string.
Click to view the structure of the returned data.
1[ 2 { 3 "id": "foo-data", // the ID property given to the DataStore instance 4 "data": "eJyrVkrKTFeyUkrOKM1LLy1WqgUAMvAF6g==", // serialized data (may be compressed / encoded or not) 5 "formatVersion": 2, // the format version of the data 6 "encoded": true, // only set to true if both encodeData and decodeData are set in the DataStore instance 7 "checksum": "420deadbeef69", // property will be missing if addChecksum is set to false 8 }, 9 { 10 // ... 11 } 12]
DataStoreSerializer.deserialize()
Usage: deserialize(data: string): Promise<void>
Deserializes the given string that was created with serialize()
and imports the contained data each DataStore instance.
In the process of importing the data, the migrations will be run, if the formatVersion
property is lower than the one set on the DataStore instance.
If ensureIntegrity
is set to true
and the checksum doesn't match, an error will be thrown.
If ensureIntegrity
is set to false
, the checksum check will be skipped entirely.
If the checksum
property is missing on the imported data, the checksum check will also be skipped.
If encoded
is set to true
, the data will be decoded using the decodeData
function set on the DataStore instance.
DataStoreSerializer.loadStoresData()
Usage: loadStoresData(): PromiseSettledResult<{ id: string, data: object }>[];
Loads the persistent data of the DataStore instances into the in-memory cache of each DataStore instance.
Also triggers the migration process if the data format has changed.
See the DataStore.loadData()
method for more information.
Click to view the structure of the returned data.
1[ 2 { 3 "status": "fulfilled", 4 "value": { 5 "id": "foo-data", 6 "data": { 7 "foo": "hello", 8 "bar": "world" 9 } 10 } 11 }, 12 { 13 "status": "rejected", 14 "reason": "Checksum mismatch for DataStore with ID \"bar-data\"!\nExpected: 69beefdead420\nHas: abcdef42" 15 } 16]
DataStoreSerializer.resetStoresData()
Usage: resetStoresData(): PromiseSettledResult[];
Resets the persistent data of the DataStore instances to their default values.
This affects both the in-memory cache and the persistent storage.
Any call to serialize()
will then use the value of options.defaultData
of the respective DataStore instance.
DataStoreSerializer.deleteStoresData()
Usage: deleteStoresData(): PromiseSettledResult[];
Deletes the persistent data of the DataStore instances from the set storage method.
Leaves the in-memory cache of the DataStore instances untouched.
Any call to setData()
on the instances will recreate their own persistent storage data.
Example - click to view
1import { DataStore, DataStoreSerializer, compress, decompress } from "@sv443-network/userutils"; 2 3/** This store doesn't have migrations to run and also has no encodeData and decodeData functions */ 4const fooStore = new DataStore({ 5 id: "foo-data", 6 defaultData: { 7 foo: "hello", 8 }, 9 formatVersion: 1, 10}); 11 12/** This store has migrations to run and also has encodeData and decodeData functions */ 13const barStore = new DataStore({ 14 id: "bar-data", 15 defaultData: { 16 foo: "hello", 17 }, 18 formatVersion: 2, 19 migrations: { 20 2: (oldData) => ({ 21 ...oldData, 22 bar: "world", 23 }), 24 }, 25 encodeData: (data) => compress(data, "deflate", "string"), 26 decodeData: (data) => decompress(data, "deflate", "string"), 27}); 28 29const serializer = new DataStoreSerializer([fooStore, barStore], { 30 addChecksum: true, 31 ensureIntegrity: true, 32}); 33 34async function exportMyDataPls() { 35 // first, make sure the persistent data of all stores is loaded into their caches: 36 await serializer.loadStoresData(); 37 38 // now serialize the data: 39 const serializedData = await serializer.serialize(); 40 // create a file and download it: 41 const blob = new Blob([serializedData], { type: "application/json" }); 42 const url = URL.createObjectURL(blob); 43 const a = document.createElement("a"); 44 a.href = url; 45 a.download = `data_export-${new Date().toISOString()}.json`; 46 a.click(); 47 a.remove(); 48 49 // `serialize()` exports a stringified object that looks similar to this: 50 // [ 51 // { 52 // "id": "foo-data", 53 // "data": "{\"foo\":\"hello\"}", // not compressed or encoded because encodeData and decodeData are not set 54 // "formatVersion": 1, 55 // "encoded": false, 56 // "checksum": "420deadbeef69" 57 // }, 58 // { 59 // "id": "bar-data", 60 // "data": "eJyrVkrKTFeyUkrOKM1LLy1WqgUAMvAF6g==", // compressed because encodeData and decodeData are set 61 // "formatVersion": 2, 62 // "encoded": true, 63 // "checksum": "69beefdead420" 64 // } 65 // ] 66} 67 68async function importMyDataPls() { 69 // grab the data from the file by using the system file picker or a text field or something similar 70 const data = await getDataFromSomewhere(); 71 72 try { 73 // import the data and run migrations if necessary 74 await serializer.deserialize(data); 75 } 76 catch(err) { 77 console.error(err); 78 alert(`Data import failed: ${err}`); 79 } 80} 81 82async function resetMyDataPls() { 83 // reset the data of all stores in both the cache and the persistent storage 84 await serializer.resetStoresData(); 85}
Dialog
Usage:
1new 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.
The options object has the following properties:
Property | Description |
---|---|
id: string | A unique internal identification string for this instance. If two Dialogs share the same ID, they will overwrite each other. |
width: number | The target and maximum width of the dialog in pixels. |
height: number | The target and maximum height of the dialog in pixels. |
renderBody: () => HTMLElement | Promise<HTMLElement> | Called to render the body of the dialog. |
renderHeader?: () => HTMLElement | Promise<HTMLElement> | (Optional) Called to render the header of the dialog. Leave undefined for a blank header. |
renderFooter?: () => HTMLElement | Promise<HTMLElement> | (Optional) Called to render the footer of the dialog. Leave undefined for no footer. |
closeOnBgClick?: boolean | (Optional) Whether the dialog should close when the background is clicked. Defaults to true . |
closeOnEscPress?: boolean | (Optional) Whether the dialog should close when the escape key is pressed. Defaults to true . |
destroyOnClose?: boolean | (Optional) Whether the dialog should be destroyed when it's closed. Defaults to false . |
unmountOnClose?: boolean | (Optional) Whether the dialog should be unmounted when it's closed. Defaults to true . Superseded by destroyOnClose . |
`removeListenersO |
No vulnerabilities found.
No security vulnerabilities found.