utils.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import type { FetchAdvancedOpts } from "./types";
  2. /**
  3. * Automatically appends an `s` to the passed `word`, if `num` is not equal to 1
  4. * @param word A word in singular form, to auto-convert to plural
  5. * @param num If this is an array or NodeList, the amount of items is used
  6. */
  7. export function autoPlural(word: string, num: number | unknown[] | NodeList) {
  8. if(Array.isArray(num) || num instanceof NodeList)
  9. num = num.length;
  10. return `${word}${num === 1 ? "" : "s"}`;
  11. }
  12. /** Ensures the passed `value` always stays between `min` and `max` */
  13. export function clamp(value: number, min: number, max: number) {
  14. return Math.max(Math.min(value, max), min);
  15. }
  16. /** Pauses async execution for the specified time in ms */
  17. export function pauseFor(time: number) {
  18. return new Promise((res) => {
  19. setTimeout(res, time);
  20. });
  21. }
  22. /**
  23. * Calls the passed `func` after the specified `timeout` in ms.
  24. * Any subsequent calls to this function will reset the timer and discard previous calls.
  25. */
  26. export function debounce<TFunc extends (...args: TArgs[]) => void, TArgs = any>(func: TFunc, timeout = 300) { // eslint-disable-line @typescript-eslint/no-explicit-any
  27. let timer: number | undefined;
  28. return function(...args: TArgs[]) {
  29. clearTimeout(timer);
  30. timer = setTimeout(() => func.apply(this, args), timeout) as unknown as number;
  31. };
  32. }
  33. /**
  34. * Returns `unsafeWindow` if the `@grant unsafeWindow` is given, otherwise falls back to the regular `window`
  35. */
  36. export function getUnsafeWindow() {
  37. try {
  38. // throws ReferenceError if the "@grant unsafeWindow" isn't present
  39. return unsafeWindow;
  40. }
  41. catch(e) {
  42. return window;
  43. }
  44. }
  45. /**
  46. * Inserts `afterElement` as a sibling just after the provided `beforeElement`
  47. * @returns Returns the `afterElement`
  48. */
  49. export function insertAfter(beforeElement: HTMLElement, afterElement: HTMLElement) {
  50. beforeElement.parentNode?.insertBefore(afterElement, beforeElement.nextSibling);
  51. return afterElement;
  52. }
  53. /**
  54. * Adds a parent container around the provided element
  55. * @returns Returns the new parent element
  56. */
  57. export function addParent(element: HTMLElement, newParent: HTMLElement) {
  58. const oldParent = element.parentNode;
  59. if(!oldParent)
  60. throw new Error("Element doesn't have a parent node");
  61. oldParent.replaceChild(newParent, element);
  62. newParent.appendChild(element);
  63. return newParent;
  64. }
  65. /**
  66. * Adds global CSS style in the form of a `<style>` element in the document's `<head>`
  67. * This needs to be run after the `DOMContentLoaded` event has fired on the document object (or instantly if `@run-at document-end` is used).
  68. * @param style CSS string
  69. */
  70. export function addGlobalStyle(style: string) {
  71. const styleElem = document.createElement("style");
  72. styleElem.innerHTML = style;
  73. document.head.appendChild(styleElem);
  74. }
  75. /**
  76. * Preloads an array of image URLs so they can be loaded instantly from the browser cache later on
  77. * @param rejects If set to `true`, the returned PromiseSettledResults will contain rejections for any of the images that failed to load
  78. * @returns Returns an array of `PromiseSettledResult` - each resolved result will contain the loaded image element, while each rejected result will contain an `ErrorEvent`
  79. */
  80. export function preloadImages(srcUrls: string[], rejects = false) {
  81. const promises = srcUrls.map(src => new Promise((res, rej) => {
  82. const image = new Image();
  83. image.src = src;
  84. image.addEventListener("load", () => res(image));
  85. image.addEventListener("error", (evt) => rejects && rej(evt));
  86. }));
  87. return Promise.allSettled(promises);
  88. }
  89. /** Calls the fetch API with special options like a timeout */
  90. export async function fetchAdvanced(url: string, options: FetchAdvancedOpts = {}) {
  91. const { timeout = 10000 } = options;
  92. const controller = new AbortController();
  93. const id = setTimeout(() => controller.abort(), timeout);
  94. const res = await fetch(url, {
  95. ...options,
  96. signal: controller.signal,
  97. });
  98. clearTimeout(id);
  99. return res;
  100. }
  101. /**
  102. * Creates an invisible anchor with a `_blank` target and clicks it.
  103. * 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.
  104. *
  105. * This function has to be run in relatively quick succession in response to a user interaction event, else the browser might reject it.
  106. */
  107. export function openInNewTab(href: string) {
  108. const openElem = document.createElement("a");
  109. Object.assign(openElem, {
  110. className: "userutils-open-in-new-tab",
  111. target: "_blank",
  112. rel: "noopener noreferrer",
  113. href,
  114. });
  115. openElem.style.display = "none";
  116. document.body.appendChild(openElem);
  117. openElem.click();
  118. // timeout just to be safe
  119. setTimeout(openElem.remove, 50);
  120. }
  121. /**
  122. * Intercepts the specified event on the passed object and prevents it from being called if the called `predicate` function returns a truthy value.
  123. * Calling this function will set the `stackTraceLimit` to 1000 to ensure the stack trace is preserved.
  124. */
  125. export function interceptEvent<TEvtObj extends EventTarget>(eventObject: TEvtObj, eventName: Parameters<TEvtObj["addEventListener"]>[0], predicate: () => boolean) {
  126. // default is between 10 and 100 on conventional browsers so this should hopefully be more than enough
  127. // @ts-ignore
  128. if(Error.stackTraceLimit < 1000) {
  129. // @ts-ignore
  130. Error.stackTraceLimit = 1000;
  131. }
  132. (function(original: typeof eventObject.addEventListener) {
  133. // @ts-ignore
  134. element.__proto__.addEventListener = function(...args: Parameters<typeof eventObject.addEventListener>) {
  135. if(args[0] === eventName && predicate())
  136. return;
  137. else
  138. return original.apply(this, args);
  139. };
  140. // @ts-ignore
  141. })(eventObject.__proto__.addEventListener);
  142. }
  143. /**
  144. * Intercepts the specified event on the window object and prevents it from being called if the called `predicate` function returns a truthy value.
  145. * Calling this function will set the `stackTraceLimit` to 1000 to ensure the stack trace is preserved.
  146. */
  147. export function interceptWindowEvent(eventName: keyof WindowEventMap, predicate: () => boolean) {
  148. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  149. }