dom.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /**
  2. * Returns `unsafeWindow` if the `@grant unsafeWindow` is given, otherwise falls back to the regular `window`
  3. */
  4. export function getUnsafeWindow() {
  5. try {
  6. // throws ReferenceError if the "@grant unsafeWindow" isn't present
  7. return unsafeWindow;
  8. }
  9. catch(e) {
  10. return window;
  11. }
  12. }
  13. /**
  14. * Inserts `afterElement` as a sibling just after the provided `beforeElement`
  15. * @returns Returns the `afterElement`
  16. */
  17. export function insertAfter(beforeElement: HTMLElement, afterElement: HTMLElement) {
  18. beforeElement.parentNode?.insertBefore(afterElement, beforeElement.nextSibling);
  19. return afterElement;
  20. }
  21. /**
  22. * Adds a parent container around the provided element
  23. * @returns Returns the new parent element
  24. */
  25. export function addParent(element: HTMLElement, newParent: HTMLElement) {
  26. const oldParent = element.parentNode;
  27. if(!oldParent)
  28. throw new Error("Element doesn't have a parent node");
  29. oldParent.replaceChild(newParent, element);
  30. newParent.appendChild(element);
  31. return newParent;
  32. }
  33. /**
  34. * Adds global CSS style in the form of a `<style>` element in the document's `<head>`
  35. * This needs to be run after the `DOMContentLoaded` event has fired on the document object (or instantly if `@run-at document-end` is used).
  36. * @param style CSS string
  37. */
  38. export function addGlobalStyle(style: string) {
  39. const styleElem = document.createElement("style");
  40. styleElem.innerHTML = style;
  41. document.head.appendChild(styleElem);
  42. }
  43. /**
  44. * Preloads an array of image URLs so they can be loaded instantly from the browser cache later on
  45. * @param rejects If set to `true`, the returned PromiseSettledResults will contain rejections for any of the images that failed to load
  46. * @returns Returns an array of `PromiseSettledResult` - each resolved result will contain the loaded image element, while each rejected result will contain an `ErrorEvent`
  47. */
  48. export function preloadImages(srcUrls: string[], rejects = false) {
  49. const promises = srcUrls.map(src => new Promise((res, rej) => {
  50. const image = new Image();
  51. image.src = src;
  52. image.addEventListener("load", () => res(image));
  53. image.addEventListener("error", (evt) => rejects && rej(evt));
  54. }));
  55. return Promise.allSettled(promises);
  56. }
  57. /**
  58. * Creates an invisible anchor with a `_blank` target and clicks it.
  59. * 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.
  60. *
  61. * This function has to be run in response to a user interaction event, else the browser might reject it.
  62. */
  63. export function openInNewTab(href: string) {
  64. const openElem = document.createElement("a");
  65. Object.assign(openElem, {
  66. className: "userutils-open-in-new-tab",
  67. target: "_blank",
  68. rel: "noopener noreferrer",
  69. href,
  70. });
  71. openElem.style.display = "none";
  72. document.body.appendChild(openElem);
  73. openElem.click();
  74. // timeout just to be safe
  75. setTimeout(openElem.remove, 50);
  76. }
  77. /**
  78. * Intercepts the specified event on the passed object and prevents it from being called if the called `predicate` function returns a truthy value.
  79. * This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are added after this function is called.
  80. * Calling this function will set the `Error.stackTraceLimit` to 1000 to ensure the stack trace is preserved.
  81. */
  82. export function interceptEvent<TEvtObj extends EventTarget>(eventObject: TEvtObj, eventName: Parameters<TEvtObj["addEventListener"]>[0], predicate: () => boolean) {
  83. // default is between 10 and 100 on conventional browsers so this should hopefully be more than enough
  84. // @ts-ignore
  85. if(typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1000) {
  86. // @ts-ignore
  87. Error.stackTraceLimit = 1000;
  88. }
  89. (function(original: typeof eventObject.addEventListener) {
  90. // @ts-ignore
  91. element.__proto__.addEventListener = function(...args: Parameters<typeof eventObject.addEventListener>) {
  92. if(args[0] === eventName && predicate())
  93. return;
  94. else
  95. return original.apply(this, args);
  96. };
  97. // @ts-ignore
  98. })(eventObject.__proto__.addEventListener);
  99. }
  100. /**
  101. * Intercepts the specified event on the window object and prevents it from being called if the called `predicate` function returns a truthy value.
  102. * This function should be called as soon as possible (I recommend using `@run-at document-start`), as it will only intercept events that are added after this function is called.
  103. * Calling this function will set the `Error.stackTraceLimit` to 1000 to ensure the stack trace is preserved.
  104. */
  105. export function interceptWindowEvent(eventName: keyof WindowEventMap, predicate: () => boolean) {
  106. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  107. }
  108. /**
  109. * Amplifies the gain of the passed media element's audio by the specified multiplier.
  110. * This function supports any media element like `<audio>` or `<video>`
  111. *
  112. * This function has to be run in response to a user interaction event, else the browser will reject it because of the strict autoplay policy.
  113. *
  114. * @returns Returns an object with the following properties:
  115. * | Property | Description |
  116. * | :-- | :-- |
  117. * | `mediaElement` | The passed media element |
  118. * | `amplify()` | A function to change the amplification level |
  119. * | `getAmpLevel()` | A function to return the current amplification level |
  120. * | `context` | The AudioContext instance |
  121. * | `source` | The MediaElementSourceNode instance |
  122. * | `gain` | The GainNode instance |
  123. */
  124. export function amplifyMedia<TElem extends HTMLMediaElement>(mediaElement: TElem, multiplier = 1.0) {
  125. // @ts-ignore
  126. const context = new (window.AudioContext || window.webkitAudioContext);
  127. const result = {
  128. mediaElement,
  129. amplify: (multiplier: number) => { result.gain.gain.value = multiplier; },
  130. getAmpLevel: () => result.gain.gain.value,
  131. context: context,
  132. source: context.createMediaElementSource(mediaElement),
  133. gain: context.createGain(),
  134. };
  135. result.source.connect(result.gain);
  136. result.gain.connect(context.destination);
  137. result.amplify(multiplier);
  138. return result;
  139. }