wait-for-element

Provides utility functions to query elements asyncronously that are not yet loaded or available on the page.

当前为 2024-02-28 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/488161/1335046/wait-for-element.js

  1. // ==UserScript==
  2. // @name wait-for-element
  3. // @description Provides utility functions to query elements asyncronously that are not yet loaded or available on the page.
  4. // @version 0.1.0
  5. // @namespace owowed.moe
  6. // @author owowed <island@owowed.moe>
  7. // @license LGPL-3.0
  8. // @require https://update.greasyfork.org/scripts/488160/1335044/make-mutation-observer.js
  9. // ==/UserScript==
  10.  
  11. /**
  12. * @typedef WaitForElementOptions Options that modify wait for element functions behavior.
  13. * @prop {string} [id] Select element by id.
  14. * @prop {string | string[]} selector Selector that matches target element. If `selector` is `string[]`, then `multiple` option is forced enabled.
  15. * @prop {ParentNode} [parent] Parent element to start query select from. By default, it will query select from `document`.
  16. * @prop {AbortSignal} [abortSignal] Abort signal to abort element querying.
  17. * @prop {boolean} [multiple] Query multiple elements instead of a single one.
  18. * @prop {number} [timeout] Set timeout for element querying. Reaching timeout will throw `WaitForElementTimeoutError`. If `undefined`, then timeout will be disabled.
  19. * @prop {number} [maxTries] Set how many attempts function can do element querying. Reaching max tries will throw `WaitForElementMaxTriesError`.
  20. * @prop {boolean} [ensureDomContentLoaded] Wait for `DOMContentLoad` event before execution, or if event already fired, it will be immediately executed.
  21. * @prop {MutationObserverInit} [observerOptions] Set options for `MutationObserver` used in wait for element functions.
  22. * @prop {(elem: HTMLElement) => boolean} [filter] Filter querying element.
  23. * @prop {(elem: HTMLElement) => HTMLElement} [transform] Transform querying element.
  24. * @prop {boolean} [throwError] Throw error in certain situation except for not finding an element, instead of returning `null`. By default, its set to `true`.
  25. */
  26.  
  27. /**
  28. * @typedef {Promise<Element[] | Element | null>} WaitForElementReturnValue
  29. */
  30.  
  31. class WaitForElementError extends Error {
  32. name = this.constructor.name;
  33. }
  34. class WaitForElementTimeoutError extends WaitForElementError {}
  35. class WaitForElementMaximumTriesError extends WaitForElementError {}
  36.  
  37. /**
  38. * Query element asyncronously until the element is available on the page. This function immediately accepts `parent` as its first parameter. `parent` parameter will specify element to start query select from.
  39. * @param {NonNullable<WaitForElementOptions["parent"]>} parent
  40. * @param {WaitForElementOptions["selector"]} selector
  41. * @param {WaitForElementOptions} options
  42. * @returns {WaitForElementReturnValue}
  43. */
  44. function waitForElementByParent(parent, selector, options) {
  45. return waitForElementOptions({ selector, parent, ...options });
  46. }
  47.  
  48. /**
  49. * Query element asyncronously until the element is available on the page. This function immediately accepts `selector` as its first parameter.
  50. * @param {WaitForElementOptions["selector"]} selector
  51. * @param {WaitForElementOptions} options
  52. * @returns {WaitForElementReturnValue}
  53. */
  54. function waitForElement(selector, options) {
  55. return waitForElementOptions({ selector, ...options });
  56. }
  57.  
  58. /**
  59. * Query multiple elements asyncronously until the element is available on the page.
  60. * @param {WaitForElementOptions["selector"]} selector
  61. * @param {WaitForElementOptions} options
  62. * @returns {WaitForElementReturnValue}
  63. */
  64. function waitForElementAll(selectors, options) {
  65. return waitForElementOptions({ selectors, multiple: true, ...options });
  66. }
  67.  
  68. /**
  69. * Query element asyncronously until the element is available on the page. This function immediately accepts `WaitForElementOptions` as its first parameter.
  70. * @param {WaitForElementOptions} options
  71. * @returns {WaitForElementReturnValue}
  72. */
  73. function waitForElementOptions(
  74. { id,
  75. selector,
  76. parent = document.documentElement,
  77. abortSignal, // abort controller signal
  78. multiple = false,
  79. timeout = 5000,
  80. maxTries = Infinity,
  81. ensureDomContentLoaded = true,
  82. observerOptions = {},
  83. filter,
  84. transform,
  85. throwError } = {}) {
  86. /**
  87. * function that apply filter and transform
  88. * filter will always be applied first before transforming element
  89. * @param {*} result result of element queries
  90. */
  91. function* applyFilterTransform(result) {
  92. if (filter != undefined) {
  93. for (let elem of result) {
  94. if (filter(elem)) {
  95. if (transform) elem = transform(elem);
  96. yield elem;
  97. }
  98. }
  99. }
  100. else if (transform != undefined) {
  101. for (const elem of result) {
  102. yield transform(elem);
  103. }
  104. }
  105. }
  106.  
  107. function queryElement() {
  108. abortSignal?.throwIfAborted();
  109.  
  110. if (id) {
  111. return document.getElementById(id);
  112. }
  113. else if (multiple) {
  114. if (Array.isArray(selector)) {
  115. selector = selector.join(", ");
  116. }
  117. return Array.from(applyFilterTransform(parent.querySelectorAll(selector)));
  118. }
  119. else if (Array.isArray(selector)) {
  120. multiple = true;
  121. return queryElement(selector);
  122. }
  123. else {
  124. let result = parent.querySelector(selector);
  125.  
  126. if (transform) result = transform(result);
  127. if (filter != undefined && !filter(result)) {
  128. return null;
  129. }
  130. return result;
  131. }
  132. }
  133.  
  134. async function waitForElement() {
  135. const firstResult = queryElement();
  136.  
  137. if (firstResult) return firstResult;
  138. return new Promise((resolve, reject) => {
  139. let timeoutId = null;
  140. let queryTries = 0;
  141.  
  142. const observer = makeMutationObserver(
  143. parent,
  144. () => {
  145. const result = queryElement(observer);
  146. if (result != null && result.length != 0) {
  147. cleanup();
  148. resolve(result);
  149. }
  150. else if (queryTries >= maxTries) {
  151. cleanup();
  152. throwErrorOrNull(new WaitForElementMaximumTriesError(`maximum number of tries (${maxTries}) reached waiting for element "${selector}"`));
  153. }
  154. },
  155. {
  156. childList: true,
  157. subtree: true,
  158. abortSignal,
  159. ...observerOptions
  160. }
  161. );
  162. if (timeout != undefined) {
  163. timeoutId = setTimeout(() => {
  164. cleanup();
  165. throwErrorOrNull(new WaitForElementTimeoutError(`timeout waiting for element "${selector}"`));
  166. }, timeout);
  167. }
  168. abortSignal?.addEventListener("abort", () => {
  169. cleanup();
  170. throwErrorOrNull(new DOMException(abortSignal.reason, "AbortError"));
  171. });
  172.  
  173. function cleanup() {
  174. clearTimeout(timeoutId);
  175. observer?.disconnect();
  176. }
  177.  
  178. function throwErrorOrNull(error) {
  179. if (throwError) {
  180. reject(error);
  181. }
  182. else {
  183. resolve(null);
  184. }
  185. }
  186. });
  187. }
  188.  
  189. if (ensureDomContentLoaded && document.readyState == "loading") {
  190. return new Promise((resolve, reject) => {
  191. document.addEventListener("DOMContentLoaded", () => {
  192. waitForElement()
  193. .then((result) => resolve(result))
  194. .catch((reason) => reject(reason));
  195. });
  196. });
  197. }
  198. else {
  199. return waitForElement();
  200. }
  201. }