UserscriptAPIWait

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

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/1161015/UserscriptAPIWait.js

  1. /**
  2. * UserscriptAPIWait
  3. *
  4. * 依赖于 `UserscriptAPI`。
  5. * @version 1.3.2.20230314
  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. endTime = (options.timeout === 0) ? 0 : Math.max(Date.now() + options.timeout - options.timePadding, 1)
  51. const task = async () => {
  52. if (stop) return
  53. let result = null
  54. try {
  55. result = await options.condition()
  56. } catch (e) {
  57. options.onError?.(options, e)
  58. if (options.stopOnError) {
  59. stop = true
  60. }
  61. }
  62. if (stop) return
  63. const stopResult = await options.stopCondition?.()
  64. if (stopResult) {
  65. stop = true
  66. options.onStop?.(options)
  67. } else if (endTime !== 0 && Date.now() > endTime) {
  68. if (options.stopOnTimeout) {
  69. stop = true
  70. } else {
  71. endTime = 0
  72. }
  73. options.onTimeout?.(options)
  74. } else if (result) {
  75. stop = true
  76. if (options.stopCondition && options.stopTimeout > 0) {
  77. this.executeAfterConditionPassed({
  78. condition: options.stopCondition,
  79. callback: () => options.onStop(options),
  80. interval: options.stopInterval,
  81. timeout: options.stopTimeout,
  82. onTimeout: () => options.callback(result),
  83. })
  84. } else {
  85. options.callback(result)
  86. }
  87. }
  88. if (!stop) {
  89. setTimeout(task, options.interval)
  90. }
  91. }
  92. setTimeout(async () => {
  93. if (stop) return
  94. await task()
  95. if (stop) return
  96. setTimeout(task, options.interval)
  97. }, options.timePadding)
  98. return () => { stop = true }
  99. }
  100.  
  101. /**
  102. * @typedef waitElementOptions
  103. * @property {string} selector 该选择器指定要等待加载的元素 `element`
  104. * @property {HTMLElement} [base] 基元素
  105. * @property {HTMLElement[]} [exclude] 若 `element` 在其中则跳过,并继续检测
  106. * @property {(element: HTMLElement) => void} [callback] 当 `element` 加载成功时执行 `callback(element)`
  107. * @property {boolean} [subtree] 是否将检测范围扩展为基元素的整棵子树
  108. * @property {boolean} [multiple] 若一次检测到多个目标元素,是否在所有元素上执行回调函数(否则只处理第一个结果)
  109. * @property {boolean} [repeat] `element` 加载成功后是否继续检测
  110. * @property {number} [throttleWait] 检测节流时间(非准确)
  111. * @property {number} [timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  112. * @property {(options: waitElementOptions) => void} [onTimeout] 检测超时时执行 `onTimeout()`
  113. * @property {boolean} [stopOnTimeout] 检测超时时是否终止检测
  114. * @property {() => (* | Promise)} [stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  115. * @property {(options: waitElementOptions) => void} [onStop] 终止条件达成时执行 `onStop()`
  116. * @property {(options: waitElementOptions, e: Error) => void} [onError] 检测过程中发生错误时执行 `onError(e)`
  117. * @property {boolean} [stopOnError] 检测过程中发生错误时,是否终止检测
  118. * @property {number} [timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  119. */
  120. /**
  121. * 在元素加载完成后执行操作
  122. *
  123. * 若执行时已存在对应元素,则针对已存在的对应元素同步执行 `callback(element)`。
  124. *
  125. * ```text
  126. * +──────────+────────+───────────────────────────────────+
  127. * multiple | repeat | 说明
  128. * +──────────+────────+───────────────────────────────────+
  129. * false | false | 查找第一个匹配元素,然后终止查找
  130. * true | false | 查找所有匹配元素,然后终止查找
  131. * false | true | 查找最后一个非标记匹配元素,并标记所有
  132. * | | 匹配元素,然后继续监听元素插入
  133. * true | true | 查找所有非标记匹配元素,并标记所有匹配
  134. * | | 元素,然后继续监听元素插入
  135. * +──────────+────────+───────────────────────────────────+
  136. * ```
  137. *
  138. * @param {waitElementOptions} options 选项;缺失选项用 `api.options.wait.element` 填充
  139. * @returns {() => boolean} 执行后终止检测的函数
  140. */
  141. executeAfterElementLoaded(options) {
  142. const { api } = this
  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. let insertUnrealElementListener = false
  152.  
  153. let excluded = null
  154. if (options.exclude) {
  155. excluded = new WeakSet(options.exclude)
  156. } else if (options.repeat) {
  157. excluded = new WeakSet()
  158. }
  159. const valid = el => !(excluded?.has(el))
  160.  
  161. const stop = () => {
  162. if (!stopped) {
  163. stopped = true
  164. ob.disconnect()
  165. if (insertUnrealElementListener) {
  166. document.removeEventListener('insert-unreal-element', core)
  167. insertUnrealElementListener = false
  168. }
  169. if (tid) {
  170. clearTimeout(tid)
  171. tid = null
  172. }
  173. }
  174. }
  175.  
  176. const singleTask = el => {
  177. let success = false
  178. try {
  179. if (valid(el)) {
  180. success = true // success 指查找成功,回调出错不影响
  181. options.repeat && excluded.add(el)
  182. options.callback(el)
  183. }
  184. } catch (e) {
  185. if (options.stopOnError) {
  186. throw e
  187. } else {
  188. options.onError?.(options, e)
  189. }
  190. }
  191. return success
  192. }
  193. const task = root => {
  194. let success = false
  195. if (options.multiple) {
  196. for (const el of root.querySelectorAll(options.selector)) {
  197. success = singleTask(el) || success
  198. }
  199. } else if (options.repeat) {
  200. const elements = root.querySelectorAll(options.selector)
  201. for (let i = elements.length - 1; i >= 0; i--) {
  202. const el = elements[i]
  203. if (success) {
  204. if (valid(el)) {
  205. excluded.add(el)
  206. }
  207. } else {
  208. success = singleTask(el)
  209. }
  210. }
  211. } else {
  212. const el = root.querySelector(options.selector)
  213. success = el && singleTask(el)
  214. }
  215. loaded ||= success
  216. if (loaded && !options.repeat) {
  217. stop()
  218. }
  219. return success
  220. }
  221. const throttledTask = options.throttleWait > 0 ? api.base.throttle(task, options.throttleWait) : task
  222.  
  223. const core = () => {
  224. if (stopped) return
  225. try {
  226. if (options.stopCondition?.()) {
  227. stop()
  228. options.onStop?.(options)
  229. return
  230. }
  231. throttledTask(options.base)
  232. } catch (e) {
  233. options.onError?.(options, e)
  234. if (options.stopOnError) {
  235. stop()
  236. }
  237. }
  238. }
  239. const ob = new MutationObserver(core)
  240.  
  241. const main = () => {
  242. if (stopped) return
  243. try {
  244. if (options.stopCondition?.()) {
  245. stop()
  246. options.onStop?.(options)
  247. return
  248. }
  249. task(options.base)
  250. } catch (e) {
  251. options.onError?.(options, e)
  252. if (options.stopOnError) {
  253. stop()
  254. }
  255. }
  256. if (stopped) return
  257. ob.observe(options.base, {
  258. childList: true,
  259. subtree: options.subtree,
  260. })
  261. // 重复加入的元素可能存在于 DocumentFragment 中,需要特殊的检测手段
  262. if (options.repeat) {
  263. initInsertUnrealElementEventDispatcher()
  264. document.addEventListener('insert-unreal-element', core)
  265. insertUnrealElementListener = true
  266. }
  267. if (options.timeout > 0) {
  268. tid = setTimeout(() => {
  269. if (stopped) return
  270. tid = null
  271. if (!loaded) {
  272. if (options.stopOnTimeout) {
  273. stop()
  274. }
  275. options.onTimeout?.(options)
  276. } else { // 只要检测到,无论重复与否,都不算超时;需永久检测必须设 timeout 为 0
  277. stop()
  278. }
  279. }, Math.max(options.timeout - options.timePadding, 0))
  280. }
  281. }
  282. options.timePadding > 0 ? setTimeout(main, options.timePadding) : main()
  283. return stop
  284.  
  285. /**
  286. * 初始化 insert-unreal-element 事件分发器
  287. *
  288. * 覆盖一系列插入节点的方法,以便在插入 DocumentFragment 向 doucment 分发 insert-unreal-element 事件。
  289. */
  290. function initInsertUnrealElementEventDispatcher() {
  291. const win = typeof unsafeWindow === 'object' ? unsafeWindow : window
  292. if (win[Symbol.for('insert-unreal-element-event-dispatcher')] === undefined) {
  293. const dispatch = (target, fns) => {
  294. for (const fn of fns) {
  295. const orig = target.prototype[fn]
  296. target.prototype[fn] = function(...args) {
  297. const result = Reflect.apply(orig, this, args)
  298. for (const arg of args) {
  299. if (arg instanceof DocumentFragment) {
  300. document.dispatchEvent(new CustomEvent('insert-unreal-element'))
  301. break
  302. }
  303. }
  304. return result
  305. }
  306. }
  307. }
  308. dispatch(Node, ['appendChild', 'insertBefore', 'replaceChild'])
  309. dispatch(Element, ['after', 'append', 'before', 'insertAdjacentElement', 'prepend', 'replaceWith'])
  310. win[Symbol.for('insert-unreal-element-event-dispatcher')] = true
  311. }
  312. }
  313. }
  314.  
  315. /**
  316. * 等待条件达成
  317. *
  318. * 执行细节类似于 {@link executeAfterConditionPassed}。在原来执行 `callback(result)` 的地方执行 `resolve(result)`,被终止或超时执行 `reject()`。
  319. * @param {Object} options 选项;缺失选项用 `api.options.wait.condition` 填充
  320. * @param {() => (* | Promise)} options.condition 条件,当 `condition()` 返回的 `result` 为真值时达成条件
  321. * @param {number} [options.interval] 检测时间间隔
  322. * @param {number} [options.timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  323. * @param {boolean} [options.stopOnTimeout] 检测超时时是否终止检测
  324. * @param {() => (* | Promise)} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  325. * @param {number} [options.stopInterval] 终止条件二次判断期间的检测时间间隔
  326. * @param {number} [options.stopTimeout] 终止条件二次判断期间的检测超时时间,设置为 `0` 时禁用终止条件二次判断
  327. * @param {boolean} [options.stopOnError] 条件检测过程中发生错误时,是否终止检测
  328. * @param {number} [options.timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  329. * @returns {Promise} `result`
  330. * @throws 等待超时、达成终止条件、等待错误时抛出
  331. * @see executeAfterConditionPassed
  332. */
  333. waitForConditionPassed(options) {
  334. const { api } = this
  335. return new Promise((resolve, reject) => {
  336. this.executeAfterConditionPassed({
  337. ...options,
  338. callback: result => resolve(result),
  339. onTimeout: options => {
  340. if (options.stopOnTimeout) {
  341. reject(new Error('waitForConditionPassed: TIMEOUT', { cause: options }))
  342. } else {
  343. api.logger.warn('waitForConditionPassed: TIMEOUT', options)
  344. }
  345. },
  346. onStop: options => {
  347. reject(new Error('waitForConditionPassed: STOP', { cause: options }))
  348. },
  349. onError: (options, e) => {
  350. reject(new Error('waitForConditionPassed: ERROR', { cause: [options, e] }))
  351. },
  352. })
  353. })
  354. }
  355.  
  356. /**
  357. * 等待元素加载完成
  358. *
  359. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  360. * @param {Object} options 选项;缺失选项用 `api.options.wait.element` 填充
  361. * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
  362. * @param {HTMLElement} [options.base] 基元素
  363. * @param {HTMLElement[]} [options.exclude] 若 `element` 在其中则跳过,并继续检测
  364. * @param {boolean} [options.subtree] 是否将检测范围扩展为基元素的整棵子树
  365. * @param {number} [options.throttleWait] 检测节流时间(非准确)
  366. * @param {number} [options.timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
  367. * @param {() => (* | Promise)} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  368. * @param {() => void} [options.onStop] 终止条件达成时执行 `onStop()`
  369. * @param {boolean} [options.stopOnTimeout] 检测超时时是否终止检测
  370. * @param {boolean} [options.stopOnError] 检测过程中发生错误时,是否终止检测
  371. * @param {number} [options.timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  372. * @returns {Promise<HTMLElement>} `element`
  373. * @throws 等待超时、达成终止条件、等待错误时抛出
  374. * @see executeAfterElementLoaded
  375. */
  376. waitForElementLoaded(options) {
  377. const { api } = this
  378. return new Promise((resolve, reject) => {
  379. this.executeAfterElementLoaded({
  380. ...options,
  381. callback: element => resolve(element),
  382. onTimeout: options => {
  383. if (options.stopOnTimeout) {
  384. reject(new Error('waitForElementLoaded: TIMEOUT', { cause: options }))
  385. } else {
  386. api.logger.warn('waitForElementLoaded: TIMEOUT', options)
  387. }
  388. },
  389. onStop: options => {
  390. reject(new Error('waitForElementLoaded: STOP', { cause: options }))
  391. },
  392. onError: (options, e) => {
  393. reject(new Error('waitForElementLoaded: ERROR', { cause: [options, e] }))
  394. },
  395. })
  396. })
  397. }
  398.  
  399. /**
  400. * 元素加载选择器
  401. *
  402. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  403. * @param {string} selector 该选择器指定要等待加载的元素 `element`
  404. * @param {HTMLElement} [base=api.options.wait.element.base] 基元素
  405. * @param {boolean} [stopOnTimeout=api.options.wait.element.stopOnTimeout] 检测超时时是否终止检测
  406. * @returns {Promise<HTMLElement>} `element`
  407. * @throws 等待超时、达成终止条件、等待错误时抛出
  408. * @see executeAfterElementLoaded
  409. */
  410. $(selector, base = this.api.options.wait.element.base, stopOnTimeout = this.api.options.wait.element.stopOnTimeout) {
  411. const { api } = this
  412. return new Promise((resolve, reject) => {
  413. this.executeAfterElementLoaded({
  414. selector, base, stopOnTimeout,
  415. callback: element => resolve(element),
  416. onTimeout: options => {
  417. if (options.stopOnTimeout) {
  418. reject(new Error('waitQuerySelector: TIMEOUT', { cause: options }))
  419. } else {
  420. api.logger.warn('waitQuerySelector: TIMEOUT', options)
  421. }
  422. },
  423. onStop: options => {
  424. reject(new Error('waitQuerySelector: STOP', { cause: options }))
  425. },
  426. onError: (options, e) => {
  427. reject(new Error('waitQuerySelector: ERROR', { cause: [options, e] }))
  428. },
  429. })
  430. })
  431. }
  432. }
  433.  
  434. /* global UserscriptAPI */
  435. // eslint-disable-next-line no-lone-blocks
  436. { UserscriptAPI.registerModule('wait', UserscriptAPIWait) }