소스 검색

feat: add array + math funcs & refactor

Sven 1 년 전
부모
커밋
8368e3cff6
6개의 변경된 파일404개의 추가작업 그리고 179개의 파일을 삭제
  1. 260 125
      README.md
  2. 47 0
      lib/array.ts
  3. 0 53
      lib/dom.ts
  4. 3 1
      lib/index.ts
  5. 47 0
      lib/math.ts
  6. 47 0
      lib/misc.ts

+ 260 - 125
README.md

@@ -1,28 +1,41 @@
+<div style="text-align: center;" align="center">
+
 ## UserUtils
 Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, modify the DOM more easily and more.  
-Contains builtin TypeScript declarations.
+Contains builtin TypeScript declarations. Webpack compatible and supports ESM and CJS.
 
+</div>
 <br>
 
 ## Table of Contents:
 - [Installation](#installation)
 - [Features](#features)
-  - [onSelector()](#onselector) - call a listener once a selector is found in the DOM
-  - [initOnSelector()](#initonselector) - needs to be called once to be able to use `onSelector()`
-  - [getSelectorMap()](#getselectormap) - returns all currently registered selectors, listeners and options
-  - [autoPlural()](#autoplural) - automatically pluralize a string
-  - [clamp()](#clamp) - clamp a number between a min and max value
-  - [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time
-  - [debounce()](#debounce) - call a function only once, after a given amount of time
-  - [getUnsafeWindow()](#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
-  - [insertAfter()](#insertafter) - insert an element as a sibling after another element
-  - [addParent()](#addparent) - add a parent element around another element
-  - [addGlobalStyle()](#addglobalstyle) - add a global style to the page
-  - [preloadImages()](#preloadimages) - preload images into the browser cache for faster loading later on
-  - [fetchAdvanced()](#fetchadvanced) - wrapper around the fetch API with a timeout option
-  - [openInNewTab()](#openinnewtab) - open a link in a new tab
-  - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
-  - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
+  - [DOM:](#dom)
+    - [onSelector()](#onselector) - call a listener once a selector is found in the DOM
+    - [initOnSelector()](#initonselector) - needs to be called once to be able to use `onSelector()`
+    - [getSelectorMap()](#getselectormap) - returns all currently registered selectors, listeners and options
+    - [getUnsafeWindow()](#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
+    - [insertAfter()](#insertafter) - insert an element as a sibling after another element
+    - [addParent()](#addparent) - add a parent element around another element
+    - [addGlobalStyle()](#addglobalstyle) - add a global style to the page
+    - [preloadImages()](#preloadimages) - preload images into the browser cache for faster loading later on
+    - [openInNewTab()](#openinnewtab) - open a link in a new tab
+    - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
+    - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
+  - [Math:](#math)
+    - [clamp()](#clamp) - clamp a number between a min and max value
+    - [mapRange()](#maprange) - map a number from one range to the same spot in another range
+    - [randRange()](#randrange) - generate a random number between a min and max boundary
+  - [Misc:](#misc)
+    - [autoPlural()](#autoplural) - automatically pluralize a string
+    - [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time
+    - [debounce()](#debounce) - call a function only once, after a given amount of time
+    - [fetchAdvanced()](#fetchadvanced) - wrapper around the fetch API with a timeout option
+  - [Arrays:](#arrays)
+    - [randomItem()](#randomitem) - Returns a random item from an array
+    - [randomItemIndex()](#randomitemindex) - Returns a tuple of a random item and its index from an array
+    - [takeRandomItem()](#takerandomitem) - Returns a random item from an array and mutates it to remove the item
+    - [randomizeArray()](#randomizearray) - Returns a copy of the array with its items in a random order
 - [License](#license)
 
 <br><br>
@@ -55,6 +68,8 @@ If you like using this library, please consider [supporting development](https:/
 
 ## Features:
 
+## DOM:
+
 ### onSelector()
 Usage:  
 ```ts
@@ -181,86 +196,6 @@ const selectorMap = getSelectorMap();
 
 <br>
 
-### autoPlural()
-Usage: `autoPlural(str: string, num: number | Array | NodeList): string`  
-  
-Automatically pluralizes a string if the given number is not 1.  
-If an array or NodeList is passed, the length of it will be used.  
-  
-<details><summary><b>Example - click to view</b></summary>
-
-```ts
-autoPlural("apple", 0); // "apples"
-autoPlural("apple", 1); // "apple"
-autoPlural("apple", 2); // "apples"
-
-autoPlural("apple", [1]);    // "apple"
-autoPlural("apple", [1, 2]); // "apples"
-
-const items = [1, 2, 3, 4, "foo", "bar"];
-console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 items"
-```
-
-</details>
-
-<br>
-
-### clamp()
-Usage: `clamp(num: number, min: number, max: number): number`  
-  
-Clamps a number between a min and max value.  
-  
-<details><summary><b>Example - click to view</b></summary>
-
-```ts
-clamp(5, 0, 10);        // 5
-clamp(-1, 0, 10);       // 0
-clamp(7, 0, 10);        // 7
-clamp(Infinity, 0, 10); // 10
-```
-
-</details>
-
-<br>
-
-### pauseFor()
-Usage: `pauseFor(ms: number): Promise<void>`  
-  
-Pauses async execution for a given amount of time.  
-  
-<details><summary><b>Example - click to view</b></summary>
-
-```ts
-async function run() {
-  console.log("Hello");
-  await pauseFor(3000); // waits for 3 seconds
-  console.log("World");
-}
-```
-
-</details>
-
-<br>
-
-### debounce()
-Usage: `debounce(func: Function, timeout?: number): Function`  
-  
-Debounces a function, meaning that it will only be called once after a given amount of time.  
-This is very useful for functions that are called repeatedly, like event listeners, to remove extraneous calls.  
-The timeout will default to 300ms if left undefined.  
-  
-<details><summary><b>Example - click to view</b></summary>
-
-```ts
-window.addEventListener("resize", debounce((event) => {
-  console.log("Window was resized:", event);
-}, 500)); // 500ms timeout
-```
-
-</details>
-
-<br>
-
 ### getUnsafeWindow()
 Usage: `getUnsafeWindow(): Window`  
   
@@ -375,6 +310,191 @@ preloadImages([
 
 <br>
 
+### openInNewTab()
+Usage: `openInNewTab(url: string): void`  
+  
+Creates an invisible anchor with a `_blank` target and clicks it.  
+Contrary to `window.open()`, this has a lesser chance to get blocked by the browser's popup blocker and doesn't open the URL as a new window.  
+This function has to be run in relatively quick succession in response to a user interaction event, else the browser might reject it.  
+  
+⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+document.querySelector("#my-button").addEventListener("click", () => {
+  openInNewTab("https://example.org/");
+});
+```
+
+</details>
+
+<br>
+
+### interceptEvent()
+Usage: `interceptEvent(eventObject: EventTarget, eventName: string, predicate: () => boolean): 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.  
+Calling this function will set the `Error.stackTraceLimit` to 1000 (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.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+interceptEvent(document.body, "click", () => {
+  return true; // prevent all click events on the body element
+});
+```
+
+</details>
+
+<br>
+
+### interceptWindowEvent()
+Usage: `interceptWindowEvent(eventName: string, predicate: () => boolean): 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.  
+This is essentially the same as [`interceptEvent()`](#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.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+interceptWindowEvent("beforeunload", () => {
+  return true; // prevent the pesky "Are you sure you want to leave this page?" popup
+});
+```
+
+</details>
+
+<br><br>
+
+## Math:
+
+### clamp()
+Usage: `clamp(num: number, min: number, max: number): number`  
+  
+Clamps a number between a min and max value.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+clamp(5, 0, 10);        // 5
+clamp(-1, 0, 10);       // 0
+clamp(7, 0, 10);        // 7
+clamp(Infinity, 0, 10); // 10
+```
+
+</details>
+
+<br>
+
+### mapRange()
+Usage: `mapRange(value: number, range_1_min: number, range_1_max: number, range_2_min: number, range_2_max: number): number`  
+  
+Maps a number from one range to the spot it would be in another range.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+mapRange(5, 0, 10, 0, 100); // 50
+mapRange(5, 0, 10, 0, 50);  // 25
+// to calculate a percentage from arbitrary values, use 0 and 100 as the second range:
+mapRange(4, 0, 13, 0, 100); // 30.76923076923077
+```
+
+</details>
+
+<br>
+
+### randRange()
+Usages:  
+```ts
+randRange(min: number, max: number): number
+randRange(max: number): 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.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+randRange(0, 10);  // 4
+randRange(10, 20); // 17
+randRange(10);     // 7
+```
+
+</details>
+
+<br><br>
+
+## Misc:
+
+### autoPlural()
+Usage: `autoPlural(str: string, num: number | Array | NodeList): string`  
+  
+Automatically pluralizes a string if the given number is not 1.  
+If an array or NodeList is passed, the length of it will be used.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+autoPlural("apple", 0); // "apples"
+autoPlural("apple", 1); // "apple"
+autoPlural("apple", 2); // "apples"
+
+autoPlural("apple", [1]);    // "apple"
+autoPlural("apple", [1, 2]); // "apples"
+
+const items = [1, 2, 3, 4, "foo", "bar"];
+console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 items"
+```
+
+</details>
+
+<br>
+
+### pauseFor()
+Usage: `pauseFor(ms: number): Promise<void>`  
+  
+Pauses async execution for a given amount of time.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+async function run() {
+  console.log("Hello");
+  await pauseFor(3000); // waits for 3 seconds
+  console.log("World");
+}
+```
+
+</details>
+
+<br>
+
+### debounce()
+Usage: `debounce(func: Function, timeout?: number): Function`  
+  
+Debounces a function, meaning that it will only be called once after a given amount of time.  
+This is very useful for functions that are called repeatedly, like event listeners, to remove extraneous calls.  
+The timeout will default to 300ms if left undefined.  
+  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+window.addEventListener("resize", debounce((event) => {
+  console.log("Window was resized:", event);
+}, 500)); // 500ms timeout
+```
+
+</details>
+
+<br>
+
 ### fetchAdvanced()
 Usage:  
 ```ts
@@ -403,67 +523,82 @@ fetchAdvanced("https://api.example.org/data", {
 
 </details>
 
-<br>
+<br><br>
 
-### openInNewTab()
-Usage: `openInNewTab(url: string): void`  
-  
-Creates an invisible anchor with a `_blank` target and clicks it.  
-Contrary to `window.open()`, this has a lesser chance to get blocked by the browser's popup blocker and doesn't open the URL as a new window.  
-This function has to be run in relatively quick succession in response to a user interaction event, else the browser might reject it.  
+## Arrays:
+
+### randomItem()
+Usage: `randomItem(array: Array): any`  
   
-⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).  
+Returns a random item from an array.  
+Returns undefined if the array is empty.  
   
 <details><summary><b>Example - click to view</b></summary>
 
 ```ts
-document.querySelector("#my-button").addEventListener("click", () => {
-  openInNewTab("https://example.org/");
-});
+randomItem(["foo", "bar", "baz"]); // "bar"
+randomItem([ ]);                   // undefined
 ```
 
 </details>
 
 <br>
 
-### interceptEvent()
-Usage: `interceptEvent(eventObject: EventTarget, eventName: string, predicate: () => boolean): void`  
+### randomItemIndex()
+Usage: `randomItemIndex(array: Array): [item: any, index: number]`  
   
-Intercepts all events dispatched on the `eventObject` and prevents the listeners from being called as long as the predicate function returns a truthy value.  
-Calling this function will set the `Error.stackTraceLimit` to 1000 (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.  
+Returns a tuple of a random item and its index from an array.  
+If the array is empty, it will return undefined for both values.  
   
 <details><summary><b>Example - click to view</b></summary>
 
 ```ts
-interceptEvent(document.body, "click", () => {
-  return true; // prevent all click events on the body element
-});
+randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
+randomItemIndex([ ]);                   // [undefined, undefined]
+// using array destructuring:
+const [item, index] = randomItemIndex(["foo", "bar", "baz"]);
+// or if you only want the index:
+const [, index] = randomItemIndex(["foo", "bar", "baz"]);
 ```
 
 </details>
 
 <br>
 
-### interceptWindowEvent()
-Usage: `interceptWindowEvent(eventName: string, predicate: () => boolean): void`  
+### takeRandomItem()
+Usage: `takeRandomItem(array: Array): any`  
   
-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.  
-This is essentially the same as [`interceptEvent()`](#interceptevent), but automatically uses the `unsafeWindow` (or falls back to regular `window`).  
+Returns a random item from an array and mutates the array by removing the item.  
+Returns undefined if the array is empty.  
   
-⚠️ 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.  
+<details><summary><b>Example - click to view</b></summary>
+
+```ts
+const arr = ["foo", "bar", "baz"];
+takeRandomItem(arr); // "bar"
+console.log(arr);    // ["foo", "baz"]
+```
+
+</details>
+
+<br>
+
+### randomizeArray()
+Usage: `randomizeArray(array: Array): Array`  
+  
+Returns a copy of the array with its items in a random order.  
+If the array is empty, the originally passed array will be returned.  
   
 <details><summary><b>Example - click to view</b></summary>
 
 ```ts
-interceptWindowEvent("beforeunload", () => {
-  return true; // prevent the pesky "Are you sure you want to leave this page?" popup
-});
+randomizeArray([1, 2, 3, 4, 5, 6]); // [3, 1, 5, 2, 4, 6]
 ```
 
 </details>
 
+<br>
+
 
 <br><br>
 

+ 47 - 0
lib/array.ts

@@ -0,0 +1,47 @@
+import { randRange } from "./math";
+
+/** Returns a random item from the passed array */
+export function randomItem<T = unknown>(array: T[]) {
+  return randomItemIndex<T>(array)[0];
+}
+
+/**
+ * Returns a tuple of a random item and its index from the passed array  
+ * Returns `[undefined, undefined]` if the passed array is empty
+ */
+export function randomItemIndex<T = unknown>(array: T[]): [item?: T, index?: number] {
+  if(array.length === 0)
+    return [undefined, undefined];
+
+  const idx = randRange(array.length - 1);
+
+  return [array[idx]!, idx];
+}
+
+/** Returns a random item from the passed array and mutates the array to remove the item */
+export function takeRandomItem<T = unknown>(arr: T[]) {
+  const [itm, idx] = randomItemIndex<T>(arr);
+
+  if(idx === undefined)
+    return undefined;
+
+  arr.splice(idx, 1);
+  return itm as T;
+}
+
+/** Returns a copy of the array with its items in a random order */
+export function randomizeArray<T = unknown>(array: T[]) {
+  const retArray = [...array]; // so array and retArray don't point to the same memory address
+
+  if(array.length === 0)
+    return array;
+
+  // shamelessly stolen from https://javascript.info/task/shuffle
+  for(let i = retArray.length - 1; i > 0; i--) {
+    const j = Math.floor((randRange(0, 10000) / 10000) * (i + 1));
+    // @ts-ignore
+    [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
+  }
+
+  return retArray;
+}

+ 0 - 53
lib/utils.ts → lib/dom.ts

@@ -1,40 +1,3 @@
-import type { FetchAdvancedOpts } from "./types";
-
-/**
- * Automatically appends an `s` to the passed `word`, if `num` is not equal to 1
- * @param word A word in singular form, to auto-convert to plural
- * @param num If this is an array or NodeList, the amount of items is used
- */
-export function autoPlural(word: string, num: number | unknown[] | NodeList) {
-  if(Array.isArray(num) || num instanceof NodeList)
-    num = num.length;
-  return `${word}${num === 1 ? "" : "s"}`;
-}
-
-/** Ensures the passed `value` always stays between `min` and `max` */
-export function clamp(value: number, min: number, max: number) {
-  return Math.max(Math.min(value, max), min);
-}
-
-/** Pauses async execution for the specified time in ms */
-export function pauseFor(time: number) {
-  return new Promise((res) => {
-    setTimeout(res, time);
-  });
-}
-
-/**
- * Calls the passed `func` after the specified `timeout` in ms.  
- * Any subsequent calls to this function will reset the timer and discard previous calls.
- */
-export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout = 300) { // eslint-disable-line @typescript-eslint/no-explicit-any
-  let timer: number | undefined;
-  return function(...args: TArgs[]) {
-    clearTimeout(timer);
-    timer = setTimeout(() => func.apply(this, args), timeout) as unknown as number;
-  };
-}
-
 /**
  * Returns `unsafeWindow` if the `@grant unsafeWindow` is given, otherwise falls back to the regular `window`
  */
@@ -100,22 +63,6 @@ export function preloadImages(srcUrls: string[], rejects = false) {
   return Promise.allSettled(promises);
 }
 
-/** Calls the fetch API with special options like a timeout */
-export async function fetchAdvanced(url: string, options: FetchAdvancedOpts = {}) {
-  const { timeout = 10000 } = options;
-
-  const controller = new AbortController();
-  const id = setTimeout(() => controller.abort(), timeout);
-
-  const res = await fetch(url, {
-    ...options,
-    signal: controller.signal,
-  });
-
-  clearTimeout(id);
-  return res;
-}
-
 /**
  * Creates an invisible anchor with a `_blank` target and clicks it.  
  * Contrary to `window.open()`, this has a lesser chance to get blocked by the browser's popup blocker and doesn't open the URL as a new window.  

+ 3 - 1
lib/index.ts

@@ -1,3 +1,5 @@
-export * from "./utils";
+export * from "./dom";
+export * from "./math";
+export * from "./misc";
 export * from "./onSelector";
 export type * from "./types";

+ 47 - 0
lib/math.ts

@@ -0,0 +1,47 @@
+/** Ensures the passed `value` always stays between `min` and `max` */
+export function clamp(value: number, min: number, max: number) {
+  return Math.max(Math.min(value, max), min);
+}
+
+/**
+ * Transforms the value parameter from the numerical range `range_1_min-range_1_max` to the numerical range `range_2_min-range_2_max`  
+ * For example, you can map the value 2 in the range of 0-5 to the range of 0-10 and you'd get a 4 as a result.
+ */
+export function mapRange(value: number, range_1_min: number, range_1_max: number, range_2_min: number, range_2_max: number) {
+  if(Number(range_1_min) === 0.0 && Number(range_2_min) === 0.0)
+    return value * (range_2_max / range_1_max);
+
+  return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
+}
+
+/** Returns a random number between `min` and `max` (inclusive) */
+export function randRange(min: number, max: number): number
+/** Returns a random number between 0 and `max` (inclusive) */
+export function randRange(max: number): number
+/** Returns a random number between `min` and `max` (inclusive) */
+export function randRange(...args: number[]): number {
+  let min: number, max: number;
+
+  if(typeof args[0] === "number" && typeof args[1] === "number") {
+    // using randRange(min, max)
+    [ min, max ] = args;
+  }
+  else if(typeof args[0] === "number" && typeof args[1] !== "number") {
+    // using randRange(max)
+    min = 0;
+    max = args[0];
+  }
+  else
+    throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
+
+  min = Number(min);
+  max = Number(max);
+
+  if(isNaN(min) || isNaN(max))
+    throw new TypeError("Parameters \"min\" and \"max\" can't be NaN");
+
+  if(min > max)
+    throw new TypeError("Parameter \"min\" can't be bigger than \"max\"");
+
+  return Math.floor(Math.random() * (max - min + 1)) + min;
+}

+ 47 - 0
lib/misc.ts

@@ -0,0 +1,47 @@
+import type { FetchAdvancedOpts } from "./types";
+
+/**
+ * Automatically appends an `s` to the passed `word`, if `num` is not equal to 1
+ * @param word A word in singular form, to auto-convert to plural
+ * @param num If this is an array or NodeList, the amount of items is used
+ */
+export function autoPlural(word: string, num: number | unknown[] | NodeList) {
+  if(Array.isArray(num) || num instanceof NodeList)
+    num = num.length;
+  return `${word}${num === 1 ? "" : "s"}`;
+}
+
+/** Pauses async execution for the specified time in ms */
+export function pauseFor(time: number) {
+  return new Promise((res) => {
+    setTimeout(res, time);
+  });
+}
+
+/**
+ * Calls the passed `func` after the specified `timeout` in ms.  
+ * Any subsequent calls to this function will reset the timer and discard previous calls.
+ */
+export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout = 300) { // eslint-disable-line @typescript-eslint/no-explicit-any
+  let timer: number | undefined;
+  return function(...args: TArgs[]) {
+    clearTimeout(timer);
+    timer = setTimeout(() => func.apply(this, args), timeout) as unknown as number;
+  };
+}
+
+/** Calls the fetch API with special options like a timeout */
+export async function fetchAdvanced(url: string, options: FetchAdvancedOpts = {}) {
+  const { timeout = 10000 } = options;
+
+  const controller = new AbortController();
+  const id = setTimeout(() => controller.abort(), timeout);
+
+  const res = await fetch(url, {
+    ...options,
+    signal: controller.signal,
+  });
+
+  clearTimeout(id);
+  return res;
+}