wait-for-element

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

目前為 2024-02-29 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/488161/1335321/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.1
  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({ selector: 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 for multiple elements
  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 && transformFn == undefined) {
  93. yield result;
  94. return;
  95. }
  96. const filterFn = filter ?? (() => true);
  97. const transformFn = transform ?? ((x) => x);
  98. for (const elem of result) {
  99. if (filterFn(elem)) {
  100. yield transformFn(elem);
  101. }
  102. }
  103. }
  104.  
  105. function queryElement() {
  106. abortSignal?.throwIfAborted();
  107.  
  108. if (id) {
  109. return document.getElementById(id);
  110. }
  111. else if (multiple) {
  112. if (Array.isArray(selector)) {
  113. selector = selector.join(", ");
  114. }
  115. return Array.from(applyFilterTransform(parent.querySelectorAll(selector)));
  116. }
  117. else if (Array.isArray(selector)) {
  118. multiple = true;
  119. return queryElement();
  120. }
  121. else {
  122. let result = parent.querySelector(selector);
  123.  
  124. if (transform) result = transform(result);
  125. if (filter != undefined && !filter(result)) {
  126. return null;
  127. }
  128. return result;
  129. }
  130. }
  131.  
  132. function waitForElement() {
  133. const firstResult = queryElement();
  134.  
  135. if (firstResult) return firstResult;
  136. return new Promise((resolve, reject) => {
  137. let timeoutId = null;
  138. let queryTries = 0;
  139.  
  140. const observer = makeMutationObserver(
  141. parent,
  142. () => {
  143. const result = queryElement(observer);
  144. if (result != null && result.length != 0) {
  145. cleanup();
  146. resolve(result);
  147. }
  148. else if (queryTries >= maxTries) {
  149. cleanup();
  150. throwErrorOrNull(new WaitForElementMaximumTriesError(`maximum number of tries (${maxTries}) reached waiting for element "${selector}"`));
  151. }
  152. },
  153. {
  154. childList: true,
  155. subtree: true,
  156. abortSignal,
  157. ...observerOptions
  158. }
  159. );
  160. if (timeout != undefined) {
  161. timeoutId = setTimeout(() => {
  162. cleanup();
  163. throwErrorOrNull(new WaitForElementTimeoutError(`timeout waiting for element "${selector}"`));
  164. }, timeout);
  165. }
  166. abortSignal?.addEventListener("abort", () => {
  167. cleanup();
  168. throwErrorOrNull(new DOMException(abortSignal.reason, "AbortError"));
  169. });
  170.  
  171. function cleanup() {
  172. clearTimeout(timeoutId);
  173. observer?.disconnect();
  174. }
  175.  
  176. function throwErrorOrNull(error) {
  177. if (throwError) {
  178. reject(error);
  179. }
  180. else {
  181. resolve(null);
  182. }
  183. }
  184. });
  185. }
  186.  
  187. if (ensureDomContentLoaded && document.readyState == "loading") {
  188. return new Promise((resolve, reject) => {
  189. document.addEventListener("DOMContentLoaded", () => {
  190. waitForElement()
  191. .then((result) => resolve(result))
  192. .catch((reason) => reject(reason));
  193. });
  194. });
  195. }
  196. else {
  197. return waitForElement();
  198. }
  199. }