utils.ts 5.3 KB

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