UserscriptAPIWait

https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI

As of 2021-09-25. See the latest version.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/432002/973745/UserscriptAPIWait.js

  1. /**
  2. * UserscriptAPIWait
  3. *
  4. * 依赖于 `UserscriptAPI`。
  5. * @version 1.2.0.20210925
  6. * @author Laster2800
  7. * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
  8. */
  9. class UserscriptAPIWait {
  10. /**
  11. * @param {UserscriptAPI} api `UserscriptAPI`
  12. */
  13. constructor(api) {
  14. this.api = api
  15. }
  16.  
  17. /**
  18. * @typedef waitConditionOptions
  19. * @property {() => (* | Promise)} condition 条件,当 `condition()` 返回的 `result` 为真值时达成条件
  20. * @property {(result: *) => void} [callback] 当达成条件时执行 `callback(result)`
  21. * @property {number} [interval] 检测时间间隔
  22. * @property {number} [timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  23. * @property {(options: waitConditionOptions) => void} [onTimeout] 检测超时时执行 `onTimeout()`
  24. * @property {boolean} [stopOnTimeout] 检测超时时是否终止检测
  25. * @property {() => (* | Promise)} [stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  26. * @property {(options: waitConditionOptions) => void} [onStop] 终止条件达成时执行 `onStop()`(包括终止条件的二次判断达成)
  27. * @property {number} [stopInterval] 终止条件二次判断期间的检测时间间隔
  28. * @property {number} [stopTimeout] 终止条件二次判断期间的检测超时时间,设置为 `0` 时禁用终止条件二次判断
  29. * @property {(options: waitConditionOptions, e: Error) => void} [onError] 条件检测过程中发生错误时执行 `onError(e)`
  30. * @property {boolean} [stopOnError] 条件检测过程中发生错误时,是否终止检测
  31. * @property {number} [timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  32. */
  33. /**
  34. * 在条件达成后执行操作
  35. *
  36. * 当条件达成后,如果不存在终止条件,那么直接执行 `callback(result)`。
  37. *
  38. * 当条件达成后,如果存在终止条件,且 `stopTimeout` 大于 0,则还会在接下来的 `stopTimeout` 时间内判断是否达成终止条件,称为终止条件的二次判断。如果在此期间,终止条件通过,则表示依然不达成条件,故执行 `onStop()` 而非 `callback(result)`。如果在此期间,终止条件一直失败,则顺利通过检测,执行 `callback(result)`。
  39. *
  40. * @param {waitConditionOptions} options 选项;缺失选项用 `api.options.wait.condition` 填充
  41. * @returns {() => boolean} 执行后终止检测的函数
  42. */
  43. executeAfterConditionPassed(options) {
  44. options = {
  45. ...this.api.options.wait.condition,
  46. ...options,
  47. }
  48. let stop = false
  49. let endTime = null
  50. if (options.timeout === 0) {
  51. endTime = 0
  52. } else {
  53. endTime = Math.max(Date.now() + options.timeout - options.timePadding, 1)
  54. }
  55. const task = async () => {
  56. if (stop) return
  57. let result = null
  58. try {
  59. result = await options.condition()
  60. } catch (e) {
  61. options.onError?.(options, e)
  62. if (options.stopOnError) {
  63. stop = true
  64. }
  65. }
  66. if (stop) return
  67. const stopResult = await options.stopCondition?.()
  68. if (stopResult) {
  69. stop = true
  70. options.onStop?.(options)
  71. } else if (endTime !== 0 && Date.now() > endTime) {
  72. if (options.stopOnTimeout) {
  73. stop = true
  74. } else {
  75. endTime = 0
  76. }
  77. options.onTimeout?.(options)
  78. } else if (result) {
  79. stop = true
  80. if (options.stopCondition && options.stopTimeout > 0) {
  81. this.executeAfterConditionPassed({
  82. condition: options.stopCondition,
  83. callback: () => options.onStop(options),
  84. interval: options.stopInterval,
  85. timeout: options.stopTimeout,
  86. onTimeout: () => options.callback(result),
  87. })
  88. } else {
  89. options.callback(result)
  90. }
  91. }
  92. if (!stop) {
  93. setTimeout(task, options.interval)
  94. }
  95. }
  96. setTimeout(async () => {
  97. if (stop) return
  98. await task()
  99. if (stop) return
  100. setTimeout(task, options.interval)
  101. }, options.timePadding)
  102. return () => { stop = true }
  103. }
  104.  
  105. /**
  106. * @typedef waitElementOptions
  107. * @property {string} selector 该选择器指定要等待加载的元素 `element`
  108. * @property {HTMLElement} [base] 基元素
  109. * @property {HTMLElement[]} [exclude] 若 `element` 在其中则跳过,并继续检测
  110. * @property {(element: HTMLElement) => void} [callback] 当 `element` 加载成功时执行 `callback(element)`
  111. * @property {boolean} [subtree] 是否将检测范围扩展为基元素的整棵子树
  112. * @property {boolean} [multiple] 若一次检测到多个目标元素,是否在所有元素上执行回调函数(否则只处理第一个结果)
  113. * @property {boolean} [repeat] `element` 加载成功后是否继续检测
  114. * @property {number} [throttleWait] 检测节流时间(非准确)
  115. * @property {number} [timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  116. * @property {(options: waitElementOptions) => void} [onTimeout] 检测超时时执行 `onTimeout()`
  117. * @property {boolean} [stopOnTimeout] 检测超时时是否终止检测
  118. * @property {() => (* | Promise)} [stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  119. * @property {(options: waitElementOptions) => void} [onStop] 终止条件达成时执行 `onStop()`
  120. * @property {(options: waitElementOptions, e: Error) => void} [onError] 检测过程中发生错误时执行 `onError(e)`
  121. * @property {boolean} [stopOnError] 检测过程中发生错误时,是否终止检测
  122. * @property {number} [timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  123. */
  124. /**
  125. * 在元素加载完成后执行操作
  126. *
  127. * 若执行时已存在对应元素,则针对已存在的对应元素同步执行 `callback(element)`。
  128. *
  129. * ```plaintext
  130. * +──────────+────────+───────────────────────────────────+
  131. * multiple | repeat | 说明
  132. * +──────────+────────+───────────────────────────────────+
  133. * false | false | 查找第一个匹配元素,然后终止查找
  134. * true | false | 查找所有匹配元素,然后终止查找
  135. * false | true | 查找最后一个非标记匹配元素,并标记所有
  136. * | | 匹配元素,然后继续监听元素插入
  137. * true | true | 查找所有非标记匹配元素,并标记所有匹配
  138. * | | 元素,然后继续监听元素插入
  139. * +────────────+──────────+───────────────────────────────────+
  140. * ```
  141. *
  142. * @param {waitElementOptions} options 选项;缺失选项用 `api.options.wait.element` 填充
  143. * @returns {() => boolean} 执行后终止检测的函数
  144. */
  145. executeAfterElementLoaded(options) {
  146. const { api } = this
  147. options = {
  148. ...api.options.wait.element,
  149. ...options,
  150. }
  151.  
  152. let loaded = false
  153. let stopped = false
  154. let tid = null // background timer id
  155.  
  156. let excluded = null
  157. if (options.exclude) {
  158. excluded = new WeakSet(options.exclude)
  159. } else if (options.repeat) {
  160. excluded = new WeakSet()
  161. }
  162. const valid = el => !(excluded?.has(el))
  163.  
  164. const stop = () => {
  165. if (!stopped) {
  166. stopped = true
  167. ob.disconnect()
  168. if (tid) {
  169. clearTimeout(tid)
  170. tid = null
  171. }
  172. }
  173. }
  174.  
  175. const singleTask = el => {
  176. let success = false
  177. try {
  178. if (valid(el)) {
  179. success = true // success 指查找成功,回调出错不影响
  180. options.repeat && excluded.add(el)
  181. options.callback(el)
  182. }
  183. } catch (e) {
  184. if (options.stopOnError) {
  185. throw e
  186. } else {
  187. options.onError?.(options, e)
  188. }
  189. }
  190. return success
  191. }
  192. const task = root => {
  193. let success = false
  194. if (options.multiple) {
  195. for (const el of root.querySelectorAll(options.selector)) {
  196. success = singleTask(el)
  197. }
  198. } else if (options.repeat) {
  199. const elements = root.querySelectorAll(options.selector)
  200. for (let i = elements.length - 1; i >= 0; i--) {
  201. const el = elements[i]
  202. if (success) {
  203. if (valid(el)) {
  204. excluded.add(el)
  205. }
  206. } else {
  207. success = singleTask(el)
  208. }
  209. }
  210. } else {
  211. const el = root.querySelector(options.selector)
  212. success = el && singleTask(el)
  213. }
  214. loaded ||= success
  215. if (loaded && !options.repeat) {
  216. stop()
  217. }
  218. return success
  219. }
  220. const throttledTask = options.throttleWait > 0 ? api.base.throttle(task, options.throttleWait) : task
  221.  
  222. const ob = new MutationObserver(() => {
  223. if (stopped) return
  224. try {
  225. if (options.stopCondition?.()) {
  226. stop()
  227. options.onStop?.(options)
  228. return
  229. }
  230. throttledTask(options.base)
  231. } catch (e) {
  232. options.onError?.(options, e)
  233. if (options.stopOnError) {
  234. stop()
  235. }
  236. }
  237. })
  238.  
  239. const main = () => {
  240. if (stopped) return
  241. try {
  242. if (options.stopCondition?.()) {
  243. stop()
  244. options.onStop?.(options)
  245. return
  246. }
  247. task(options.base)
  248. } catch (e) {
  249. options.onError?.(options, e)
  250. if (options.stopOnError) {
  251. stop()
  252. }
  253. }
  254. if (stopped) return
  255. ob.observe(options.base, {
  256. childList: true,
  257. subtree: options.subtree,
  258. })
  259. if (options.timeout > 0) {
  260. tid = setTimeout(() => {
  261. if (stopped) return
  262. tid = null
  263. if (!loaded) {
  264. if (options.stopOnTimeout) {
  265. stop()
  266. }
  267. options.onTimeout?.(options)
  268. } else { // 只要检测到,无论重复与否,都不算超时;需永久检测必须设 timeout 为 0
  269. stop()
  270. }
  271. }, Math.max(options.timeout - options.timePadding, 0))
  272. }
  273. }
  274. options.timePadding > 0 ? setTimeout(main, options.timePadding) : main()
  275. return stop
  276. }
  277.  
  278. /**
  279. * 等待条件达成
  280. *
  281. * 执行细节类似于 {@link executeAfterConditionPassed}。在原来执行 `callback(result)` 的地方执行 `resolve(result)`,被终止或超时执行 `reject()`。
  282. * @param {Object} options 选项;缺失选项用 `api.options.wait.condition` 填充
  283. * @param {() => (* | Promise)} options.condition 条件,当 `condition()` 返回的 `result` 为真值时达成条件
  284. * @param {number} [options.interval] 检测时间间隔
  285. * @param {number} [options.timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  286. * @param {boolean} [options.stopOnTimeout] 检测超时时是否终止检测
  287. * @param {() => (* | Promise)} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  288. * @param {number} [options.stopInterval] 终止条件二次判断期间的检测时间间隔
  289. * @param {number} [options.stopTimeout] 终止条件二次判断期间的检测超时时间,设置为 `0` 时禁用终止条件二次判断
  290. * @param {boolean} [options.stopOnError] 条件检测过程中发生错误时,是否终止检测
  291. * @param {number} [options.timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  292. * @returns {Promise} `result`
  293. * @throws 等待超时、达成终止条件、等待错误时抛出
  294. * @see executeAfterConditionPassed
  295. */
  296. waitForConditionPassed(options) {
  297. const { api } = this
  298. return new Promise((resolve, reject) => {
  299. this.executeAfterConditionPassed({
  300. ...options,
  301. callback: result => resolve(result),
  302. onTimeout: options => {
  303. if (options.stopOnTimeout) {
  304. reject(new Error('waitForConditionPassed: TIMEOUT', { cause: options }))
  305. } else {
  306. api.logger.warn('waitForConditionPassed: TIMEOUT', options)
  307. }
  308. },
  309. onStop: options => {
  310. reject(new Error('waitForConditionPassed: STOP', { cause: options }))
  311. },
  312. onError: (options, e) => {
  313. reject(new Error('waitForConditionPassed: ERROR', { cause: [options, e] }))
  314. },
  315. })
  316. })
  317. }
  318.  
  319. /**
  320. * 等待元素加载完成
  321. *
  322. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  323. * @param {Object} options 选项;缺失选项用 `api.options.wait.element` 填充
  324. * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
  325. * @param {HTMLElement} [options.base] 基元素
  326. * @param {HTMLElement[]} [options.exclude] 若 `element` 在其中则跳过,并继续检测
  327. * @param {boolean} [options.subtree] 是否将检测范围扩展为基元素的整棵子树
  328. * @param {number} [options.throttleWait] 检测节流时间(非准确)
  329. * @param {number} [options.timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  330. * @param {() => (* | Promise)} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  331. * @param {() => void} [options.onStop] 终止条件达成时执行 `onStop()`
  332. * @param {boolean} [options.stopOnTimeout] 检测超时时是否终止检测
  333. * @param {boolean} [options.stopOnError] 检测过程中发生错误时,是否终止检测
  334. * @param {number} [options.timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  335. * @returns {Promise<HTMLElement>} `element`
  336. * @throws 等待超时、达成终止条件、等待错误时抛出
  337. * @see executeAfterElementLoaded
  338. */
  339. waitForElementLoaded(options) {
  340. const { api } = this
  341. return new Promise((resolve, reject) => {
  342. this.executeAfterElementLoaded({
  343. ...options,
  344. callback: element => resolve(element),
  345. onTimeout: options => {
  346. if (options.stopOnTimeout) {
  347. reject(new Error('waitForElementLoaded: TIMEOUT', { cause: options }))
  348. } else {
  349. api.logger.warn('waitForElementLoaded: TIMEOUT', options)
  350. }
  351. },
  352. onStop: options => {
  353. reject(new Error('waitForElementLoaded: STOP', { cause: options }))
  354. },
  355. onError: (options, e) => {
  356. reject(new Error('waitForElementLoaded: ERROR', { cause: [options, e] }))
  357. },
  358. })
  359. })
  360. }
  361.  
  362. /**
  363. * 元素加载选择器
  364. *
  365. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  366. * @param {string} selector 该选择器指定要等待加载的元素 `element`
  367. * @param {HTMLElement} [base=api.options.wait.element.base] 基元素
  368. * @param {boolean} [stopOnTimeout=api.options.wait.element.stopOnTimeout] 检测超时时是否终止检测
  369. * @returns {Promise<HTMLElement>} `element`
  370. * @throws 等待超时、达成终止条件、等待错误时抛出
  371. * @see executeAfterElementLoaded
  372. */
  373. $(selector, base = this.api.options.wait.element.base, stopOnTimeout = this.api.options.wait.element.stopOnTimeout) {
  374. const { api } = this
  375. return new Promise((resolve, reject) => {
  376. this.executeAfterElementLoaded({
  377. selector, base, stopOnTimeout,
  378. callback: element => resolve(element),
  379. onTimeout: options => {
  380. if (options.stopOnTimeout) {
  381. reject(new Error('waitQuerySelector: TIMEOUT', { cause: options }))
  382. } else {
  383. api.logger.warn('waitQuerySelector: TIMEOUT', options)
  384. }
  385. },
  386. onStop: options => {
  387. reject(new Error('waitQuerySelector: STOP', { cause: options }))
  388. },
  389. onError: (options, e) => {
  390. reject(new Error('waitQuerySelector: ERROR', { cause: [options, e] }))
  391. },
  392. })
  393. })
  394. }
  395. }
  396.  
  397. /* global UserscriptAPI */
  398. // eslint-disable-next-line no-lone-blocks
  399. { UserscriptAPI.registerModule('wait', UserscriptAPIWait) }