wait-for-element

Provides utility functions to get and wait for elements asyncronously that are not yet loaded or available on the page.

目前为 2024-02-24 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/488161/1332704/wait-for-element.js

  1. // ==UserScript==
  2. // @name wait-for-element
  3. // @description Provides utility functions to get and wait for elements asyncronously that are not yet loaded or available on the page.
  4. // @version 0.0.1
  5. // @namespace owowed.moe
  6. // @author owowed <island@owowed.moe>
  7. // @require https://update.greasyfork.org/scripts/488160/1332703/make-mutation-observer.js
  8. // @license LGPL-3.0
  9. // ==/UserScript==
  10.  
  11. /**
  12. * @typedef WaitForElementOptions
  13. * @prop {string} [id] - when used, it will select element by ID, and it will use the `document` as the selector parent.
  14. * @prop {string | string[]} selector - the selector for the element. If `selector` is `string[]`, then `multiple` option is automatically enabled. Setting `multiple` option to `false` will not have effect at all.
  15. * @prop {ParentNode} [parent] - when used, it will instead use `parent` parent element as the selector parent. This option will specify/limit the scope of query selector from `parent` parent element. This may be useful for optimizing selecting element.
  16. * @prop {AbortSignal} [abortSignal] - when used, user may able to abort waiting for element by using `AbortSignal#abort()`.
  17. * @prop {boolean} [multiple] - when used, `waitForElement*` will act as `ParentNode#querySelectorAll()`.
  18. * @prop {number} [timeout] - will set wait for element timeout. Default timeout is 5 seconds.
  19. * @prop {boolean} [enableTimeout] - if timeout set by `timeout` reached, `waitForElement*` will throw `WaitForElementTimeoutError`.
  20. * @prop {number} [maxTries] - will set how many attempt `waitForElement*` will query select, and if it reached, it will throw `WaitForElementMaximumTriesError`.
  21. * @prop {boolean} [ensureDomContentLoaded] - ensure DOM content loaded by listening to `DOMContentLoad` event, and then execute by that, or checking by using `document.readyState`.
  22. * @prop {MutationObserverInit} [observerOptions] - set options for `MutationObserver` used in `waitForElement*`.
  23. * @prop {(elem: HTMLElement) => boolean} [filter] - filter multiple or single element before being returned.
  24. * @prop {(elem: HTMLElement) => HTMLElement} [transform] - transform or modify multiple or single element before being returned.
  25. */
  26.  
  27. /**
  28. * @typedef {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. * Wait for element asyncronously until the element is available on the page. This function immediately accept `parent` as its first parameter. `parent` parameter will specify/limit the scope of query selector. This may be useful for optimizing selecting element.
  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. * Wait for element asyncronously until the element is available on the page. This function immediately accept `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. * Wait for element asyncronously until the element is available on the page.
  60. * @param {WaitForElementOptions} options
  61. * @returns {WaitForElementReturnValue}
  62. */
  63. function waitForElementOptions(
  64. { id,
  65. selector,
  66. parent = document.documentElement,
  67. abortSignal, // abort controller signal
  68. multiple = false,
  69. timeout = 5000,
  70. enableTimeout = true,
  71. maxTries = Number.MAX_SAFE_INTEGER,
  72. ensureDomContentLoaded = true,
  73. observerOptions = {},
  74. filter,
  75. transform,
  76. throwError } = {}) {
  77. return new Promise((resolve, reject) => {
  78. let result, tries = 0;
  79. function* applyFilterTransform(result) {
  80. if (filter != undefined) {
  81. for (let elem of result) {
  82. if (filter(elem)) {
  83. if (transform) elem = transform(elem);
  84. yield elem;
  85. }
  86. }
  87. }
  88. else if (transform != undefined) {
  89. for (const elem of result) {
  90. yield transform(elem);
  91. }
  92. }
  93. }
  94.  
  95. function tryQueryElement(observer) {
  96. abortSignal?.throwIfAborted();
  97.  
  98. if (multiple && id == undefined) {
  99. if (Array.isArray(selector)) {
  100. result = [];
  101. for (const sel of selector) {
  102. result = result.concat(Array.from(parent.querySelectorAll(sel)));
  103. }
  104. }
  105. else {
  106. result = Array.from(parent.querySelectorAll(selector));
  107. }
  108. result = Array.from(applyFilterTransform(result));
  109. }
  110. else {
  111. if (id) {
  112. result = document.getElementById(id);
  113. }
  114. else if (Array.isArray(selector)) {
  115. result = [];
  116.  
  117. function* querySelectorIterator() {
  118. for (const sel of selector) {
  119. yield parent.querySelector(sel);
  120. }
  121. }
  122.  
  123. result = Array.from(applyFilterTransform(querySelectorIterator()));
  124. }
  125. else {
  126. result = parent.querySelector(selector);
  127. }
  128. if (transform) result = transform(result);
  129. if (filter != undefined && !filter(result)) {
  130. return false;
  131. }
  132. }
  133.  
  134. if (multiple ? result?.length > 0 : result) {
  135. observer?.disconnect();
  136. resolve(result);
  137. return result;
  138. }
  139.  
  140. tries++;
  141.  
  142. if (tries >= maxTries) {
  143. observer?.disconnect();
  144. if (throwError) {
  145. reject(new WaitForElementMaximumTriesError(`maximum number of tries (${maxTries}) reached waiting for element "${selector}"`));
  146. }
  147. else {
  148. resolve(null);
  149. }
  150. return false;
  151. }
  152. }
  153.  
  154. function startWaitForElement() {
  155. const firstResult = tryQueryElement();
  156.  
  157. if (firstResult) return;
  158. let observer = makeMutationObserver(
  159. { target: parent,
  160. childList: true,
  161. subtree: true,
  162. abortSignal,
  163. ...observerOptions },
  164. () => tryQueryElement(observer));
  165. let timeoutId = null;
  166. if (enableTimeout) {
  167. timeoutId = setTimeout(() => {
  168. observer.disconnect();
  169. if (throwError) {
  170. reject(new WaitForElementTimeoutError(`timeout waiting for element "${selector}"`));
  171. }
  172. else {
  173. resolve(null);
  174. }
  175. }, timeout);
  176. }
  177. abortSignal?.addEventListener("abort", () => {
  178. clearTimeout(timeoutId);
  179. observer.disconnect();
  180. if (throwError) {
  181. reject(new DOMException(abortSignal.reason, "AbortError"));
  182. }
  183. else {
  184. resolve(null);
  185. }
  186. });
  187. tryQueryElement();
  188. }
  189.  
  190. if (ensureDomContentLoaded && document.readyState == "loading") {
  191. document.addEventListener("DOMContentLoaded", () => {
  192. startWaitForElement();
  193. });
  194. }
  195. else {
  196. startWaitForElement();
  197. }
  198. });
  199. }