UserscriptAPIWait

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

Version vom 25.09.2021. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/432002/973745/UserscriptAPIWait.js

/**
 * UserscriptAPIWait
 *
 * 依赖于 `UserscriptAPI`。
 * @version 1.2.0.20210925
 * @author Laster2800
 * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
 */
class UserscriptAPIWait {
  /**
   * @param {UserscriptAPI} api `UserscriptAPI`
   */
  constructor(api) {
    this.api = api
  }

  /**
   * @typedef waitConditionOptions
   * @property {() => (* | Promise)} condition 条件,当 `condition()` 返回的 `result` 为真值时达成条件
   * @property {(result: *) => void} [callback] 当达成条件时执行 `callback(result)`
   * @property {number} [interval] 检测时间间隔
   * @property {number} [timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
   * @property {(options: waitConditionOptions) => void} [onTimeout] 检测超时时执行 `onTimeout()`
   * @property {boolean} [stopOnTimeout] 检测超时时是否终止检测
   * @property {() => (* | Promise)} [stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
   * @property {(options: waitConditionOptions) => void} [onStop] 终止条件达成时执行 `onStop()`(包括终止条件的二次判断达成)
   * @property {number} [stopInterval] 终止条件二次判断期间的检测时间间隔
   * @property {number} [stopTimeout] 终止条件二次判断期间的检测超时时间,设置为 `0` 时禁用终止条件二次判断
   * @property {(options: waitConditionOptions, e: Error) => void} [onError] 条件检测过程中发生错误时执行 `onError(e)`
   * @property {boolean} [stopOnError] 条件检测过程中发生错误时,是否终止检测
   * @property {number} [timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
   */
  /**
   * 在条件达成后执行操作
   *
   * 当条件达成后,如果不存在终止条件,那么直接执行 `callback(result)`。
   *
   * 当条件达成后,如果存在终止条件,且 `stopTimeout` 大于 0,则还会在接下来的 `stopTimeout` 时间内判断是否达成终止条件,称为终止条件的二次判断。如果在此期间,终止条件通过,则表示依然不达成条件,故执行 `onStop()` 而非 `callback(result)`。如果在此期间,终止条件一直失败,则顺利通过检测,执行 `callback(result)`。
   *
   * @param {waitConditionOptions} options 选项;缺失选项用 `api.options.wait.condition` 填充
   * @returns {() => boolean} 执行后终止检测的函数
   */
  executeAfterConditionPassed(options) {
    options = {
      ...this.api.options.wait.condition,
      ...options,
    }
    let stop = false
    let endTime = null
    if (options.timeout === 0) {
      endTime = 0
    } else {
      endTime = Math.max(Date.now() + options.timeout - options.timePadding, 1)
    }
    const task = async () => {
      if (stop) return
      let result = null
      try {
        result = await options.condition()
      } catch (e) {
        options.onError?.(options, e)
        if (options.stopOnError) {
          stop = true
        }
      }
      if (stop) return
      const stopResult = await options.stopCondition?.()
      if (stopResult) {
        stop = true
        options.onStop?.(options)
      } else if (endTime !== 0 && Date.now() > endTime) {
        if (options.stopOnTimeout) {
          stop = true
        } else {
          endTime = 0
        }
        options.onTimeout?.(options)
      } else if (result) {
        stop = true
        if (options.stopCondition && options.stopTimeout > 0) {
          this.executeAfterConditionPassed({
            condition: options.stopCondition,
            callback: () => options.onStop(options),
            interval: options.stopInterval,
            timeout: options.stopTimeout,
            onTimeout: () => options.callback(result),
          })
        } else {
          options.callback(result)
        }
      }
      if (!stop) {
        setTimeout(task, options.interval)
      }
    }
    setTimeout(async () => {
      if (stop) return
      await task()
      if (stop) return
      setTimeout(task, options.interval)
    }, options.timePadding)
    return () => { stop = true }
  }

  /**
   * @typedef waitElementOptions
   * @property {string} selector 该选择器指定要等待加载的元素 `element`
   * @property {HTMLElement} [base] 基元素
   * @property {HTMLElement[]} [exclude] 若 `element` 在其中则跳过,并继续检测
   * @property {(element: HTMLElement) => void} [callback] 当 `element` 加载成功时执行 `callback(element)`
   * @property {boolean} [subtree] 是否将检测范围扩展为基元素的整棵子树
   * @property {boolean} [multiple] 若一次检测到多个目标元素,是否在所有元素上执行回调函数(否则只处理第一个结果)
   * @property {boolean} [repeat] `element` 加载成功后是否继续检测
   * @property {number} [throttleWait] 检测节流时间(非准确)
   * @property {number} [timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
   * @property {(options: waitElementOptions) => void} [onTimeout] 检测超时时执行 `onTimeout()`
   * @property {boolean} [stopOnTimeout] 检测超时时是否终止检测
   * @property {() => (* | Promise)} [stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
   * @property {(options: waitElementOptions) => void} [onStop] 终止条件达成时执行 `onStop()`
   * @property {(options: waitElementOptions, e: Error) => void} [onError] 检测过程中发生错误时执行 `onError(e)`
   * @property {boolean} [stopOnError] 检测过程中发生错误时,是否终止检测
   * @property {number} [timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
   */
  /**
   * 在元素加载完成后执行操作
   *
   * 若执行时已存在对应元素,则针对已存在的对应元素同步执行 `callback(element)`。
   *
   * ```plaintext
   * +──────────+────────+───────────────────────────────────+
   *   multiple | repeat | 说明
   * +──────────+────────+───────────────────────────────────+
   *   false    | false  | 查找第一个匹配元素,然后终止查找
   *   true     | false  | 查找所有匹配元素,然后终止查找
   *   false    | true   | 查找最后一个非标记匹配元素,并标记所有
   *            |        | 匹配元素,然后继续监听元素插入
   *   true     | true   | 查找所有非标记匹配元素,并标记所有匹配
   *            |        | 元素,然后继续监听元素插入
   * +────────────+──────────+───────────────────────────────────+
   * ```
   *
   * @param {waitElementOptions} options 选项;缺失选项用 `api.options.wait.element` 填充
   * @returns {() => boolean} 执行后终止检测的函数
   */
  executeAfterElementLoaded(options) {
    const { api } = this
    options = {
      ...api.options.wait.element,
      ...options,
    }

    let loaded = false
    let stopped = false
    let tid = null // background timer id

    let excluded = null
    if (options.exclude) {
      excluded = new WeakSet(options.exclude)
    } else if (options.repeat) {
      excluded = new WeakSet()
    }
    const valid = el => !(excluded?.has(el))

    const stop = () => {
      if (!stopped) {
        stopped = true
        ob.disconnect()
        if (tid) {
          clearTimeout(tid)
          tid = null
        }
      }
    }

    const singleTask = el => {
      let success = false
      try {
        if (valid(el)) {
          success = true // success 指查找成功,回调出错不影响
          options.repeat && excluded.add(el)
          options.callback(el)
        }
      } catch (e) {
        if (options.stopOnError) {
          throw e
        } else {
          options.onError?.(options, e)
        }
      }
      return success
    }
    const task = root => {
      let success = false
      if (options.multiple) {
        for (const el of root.querySelectorAll(options.selector)) {
          success = singleTask(el)
        }
      } else if (options.repeat) {
        const elements = root.querySelectorAll(options.selector)
        for (let i = elements.length - 1; i >= 0; i--) {
          const el = elements[i]
          if (success) {
            if (valid(el)) {
              excluded.add(el)
            }
          } else {
            success = singleTask(el)
          }
        }
      } else {
        const el = root.querySelector(options.selector)
        success = el && singleTask(el)
      }
      loaded ||= success
      if (loaded && !options.repeat) {
        stop()
      }
      return success
    }
    const throttledTask = options.throttleWait > 0 ? api.base.throttle(task, options.throttleWait) : task

    const ob = new MutationObserver(() => {
      if (stopped) return
      try {
        if (options.stopCondition?.()) {
          stop()
          options.onStop?.(options)
          return
        }
        throttledTask(options.base)
      } catch (e) {
        options.onError?.(options, e)
        if (options.stopOnError) {
          stop()
        }
      }
    })

    const main = () => {
      if (stopped) return
      try {
        if (options.stopCondition?.()) {
          stop()
          options.onStop?.(options)
          return
        }
        task(options.base)
      } catch (e) {
        options.onError?.(options, e)
        if (options.stopOnError) {
          stop()
        }
      }
      if (stopped) return
      ob.observe(options.base, {
        childList: true,
        subtree: options.subtree,
      })
      if (options.timeout > 0) {
        tid = setTimeout(() => {
          if (stopped) return
          tid = null
          if (!loaded) {
            if (options.stopOnTimeout) {
              stop()
            }
            options.onTimeout?.(options)
          } else { // 只要检测到,无论重复与否,都不算超时;需永久检测必须设 timeout 为 0
            stop()
          }
        }, Math.max(options.timeout - options.timePadding, 0))
      }
    }
    options.timePadding > 0 ? setTimeout(main, options.timePadding) : main()
    return stop
  }

  /**
   * 等待条件达成
   *
   * 执行细节类似于 {@link executeAfterConditionPassed}。在原来执行 `callback(result)` 的地方执行 `resolve(result)`,被终止或超时执行 `reject()`。
   * @param {Object} options 选项;缺失选项用 `api.options.wait.condition` 填充
   * @param {() => (* | Promise)} options.condition 条件,当 `condition()` 返回的 `result` 为真值时达成条件
   * @param {number} [options.interval] 检测时间间隔
   * @param {number} [options.timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
   * @param {boolean} [options.stopOnTimeout] 检测超时时是否终止检测
   * @param {() => (* | Promise)} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
   * @param {number} [options.stopInterval] 终止条件二次判断期间的检测时间间隔
   * @param {number} [options.stopTimeout] 终止条件二次判断期间的检测超时时间,设置为 `0` 时禁用终止条件二次判断
   * @param {boolean} [options.stopOnError] 条件检测过程中发生错误时,是否终止检测
   * @param {number} [options.timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
   * @returns {Promise} `result`
   * @throws 等待超时、达成终止条件、等待错误时抛出
   * @see executeAfterConditionPassed
   */
  waitForConditionPassed(options) {
    const { api } = this
    return new Promise((resolve, reject) => {
      this.executeAfterConditionPassed({
        ...options,
        callback: result => resolve(result),
        onTimeout: options => {
          if (options.stopOnTimeout) {
            reject(new Error('waitForConditionPassed: TIMEOUT', { cause: options }))
          } else {
            api.logger.warn('waitForConditionPassed: TIMEOUT', options)
          }
        },
        onStop: options => {
          reject(new Error('waitForConditionPassed: STOP', { cause: options }))
        },
        onError: (options, e) => {
          reject(new Error('waitForConditionPassed: ERROR', { cause: [options, e] }))
        },
      })
    })
  }

  /**
   * 等待元素加载完成
   *
   * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
   * @param {Object} options 选项;缺失选项用 `api.options.wait.element` 填充
   * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
   * @param {HTMLElement} [options.base] 基元素
   * @param {HTMLElement[]} [options.exclude] 若 `element` 在其中则跳过,并继续检测
   * @param {boolean} [options.subtree] 是否将检测范围扩展为基元素的整棵子树
   * @param {number} [options.throttleWait] 检测节流时间(非准确)
   * @param {number} [options.timeout] 检测超时时间,检测时间超过该值时终止检测;设置为 `0` 时永远不会超时
   * @param {() => (* | Promise)} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
   * @param {() => void} [options.onStop] 终止条件达成时执行 `onStop()`
   * @param {boolean} [options.stopOnTimeout] 检测超时时是否终止检测
   * @param {boolean} [options.stopOnError] 检测过程中发生错误时,是否终止检测
   * @param {number} [options.timePadding] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
   * @returns {Promise<HTMLElement>} `element`
   * @throws 等待超时、达成终止条件、等待错误时抛出
   * @see executeAfterElementLoaded
   */
  waitForElementLoaded(options) {
    const { api } = this
    return new Promise((resolve, reject) => {
      this.executeAfterElementLoaded({
        ...options,
        callback: element => resolve(element),
        onTimeout: options => {
          if (options.stopOnTimeout) {
            reject(new Error('waitForElementLoaded: TIMEOUT', { cause: options }))
          } else {
            api.logger.warn('waitForElementLoaded: TIMEOUT', options)
          }
        },
        onStop: options => {
          reject(new Error('waitForElementLoaded: STOP', { cause: options }))
        },
        onError: (options, e) => {
          reject(new Error('waitForElementLoaded: ERROR', { cause: [options, e] }))
        },
      })
    })
  }

  /**
   * 元素加载选择器
   *
   * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
   * @param {string} selector 该选择器指定要等待加载的元素 `element`
   * @param {HTMLElement} [base=api.options.wait.element.base] 基元素
   * @param {boolean} [stopOnTimeout=api.options.wait.element.stopOnTimeout] 检测超时时是否终止检测
   * @returns {Promise<HTMLElement>} `element`
   * @throws 等待超时、达成终止条件、等待错误时抛出
   * @see executeAfterElementLoaded
   */
  $(selector, base = this.api.options.wait.element.base, stopOnTimeout = this.api.options.wait.element.stopOnTimeout) {
    const { api } = this
    return new Promise((resolve, reject) => {
      this.executeAfterElementLoaded({
        selector, base, stopOnTimeout,
        callback: element => resolve(element),
        onTimeout: options => {
          if (options.stopOnTimeout) {
            reject(new Error('waitQuerySelector: TIMEOUT', { cause: options }))
          } else {
            api.logger.warn('waitQuerySelector: TIMEOUT', options)
          }
        },
        onStop: options => {
          reject(new Error('waitQuerySelector: STOP', { cause: options }))
        },
        onError: (options, e) => {
          reject(new Error('waitQuerySelector: ERROR', { cause: [options, e] }))
        },
      })
    })
  }
}

/* global UserscriptAPI */
// eslint-disable-next-line no-lone-blocks
{ UserscriptAPI.registerModule('wait', UserscriptAPIWait) }