UserscriptAPIWait

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

As of 2021-09-11. 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/969564/UserscriptAPIWait.js

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