UserscriptAPI

My API for userscripts.

当前为 2021-07-11 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/409641/949361/UserscriptAPI.js

  1. /* exported UserscriptAPI */
  2. /**
  3. * UserscriptAPI
  4. *
  5. * 根据使用到的功能,可能需要通过 `@grant` 引入 `GM_xmlhttpRequest` 或 `GM_download`。
  6. * @version 1.1.0.20210711
  7. * @author Laster2800
  8. */
  9. class UserscriptAPI {
  10. /**
  11. * @param {Object} [options] 选项
  12. * @param {string} [options.id='_0'] 标识符
  13. * @param {string} [options.label] 日志标签,为空时不设置标签
  14. * @param {number} [options.conditionInterval=100] `wait` 条件 API 默认 `options.interval`
  15. * @param {number} [options.conditionTimeout=6000] `wait` 条件 API 默认 `options.timeout`
  16. * @param {number} [options.elementThrottleWait=100] `wait` 元素 API 默认 `options.throttleWait`
  17. * @param {number} [options.elementTimeout=10000] `wait` 元素 API 默认 `options.timeout`
  18. * @param {number} [options.fadeTime=400] UI 渐变时间(单位:ms)
  19. */
  20. constructor(options) {
  21. this.options = {
  22. id: '_0',
  23. label: null,
  24. conditionInterval: 100,
  25. conditionTimeout: 6000,
  26. elementThrottleWait: 100,
  27. elementTimeout: 10000,
  28. fadeTime: 400,
  29. ...options,
  30. }
  31.  
  32. const original = window[`_api_${this.options.id}`]
  33. if (original) {
  34. original.options = this.options
  35. return original
  36. }
  37. window[`_api_${this.options.id}`] = this
  38.  
  39. const api = this
  40. const logCss = `
  41. background-color: black;
  42. color: white;
  43. border-radius: 2px;
  44. padding: 2px;
  45. margin-right: 2px;
  46. `
  47.  
  48. /** DOM 相关 */
  49. this.dom = {
  50. /**
  51. * 初始化 urlchange 事件
  52. * @see {@link https://stackoverflow.com/a/52809105 How to detect if URL has changed after hash in JavaScript}
  53. */
  54. initUrlchangeEvent() {
  55. if (!history._urlchangeEventInitialized) {
  56. const urlEvent = () => {
  57. const event = new Event('urlchange')
  58. // 添加属性,使其与 Tampermonkey urlchange 保持一致
  59. event.url = location.href
  60. return event
  61. }
  62. history.pushState = (f => function pushState() {
  63. const ret = f.apply(this, arguments)
  64. window.dispatchEvent(new Event('pushstate'))
  65. window.dispatchEvent(urlEvent())
  66. return ret
  67. })(history.pushState)
  68. history.replaceState = (f => function replaceState() {
  69. const ret = f.apply(this, arguments)
  70. window.dispatchEvent(new Event('replacestate'))
  71. window.dispatchEvent(urlEvent())
  72. return ret
  73. })(history.replaceState)
  74. window.addEventListener('popstate', () => {
  75. window.dispatchEvent(urlEvent())
  76. })
  77. history._urlchangeEventInitialized = true
  78. }
  79. },
  80.  
  81. /**
  82. * 将一个元素绝对居中
  83. *
  84. * 要求该元素此时可见且尺寸为确定值(一般要求为块状元素)。运行后会在 `target` 上附加 `_absoluteCenter` 方法,若该方法已存在,则无视 `config` 直接执行 `target._absoluteCenter()`。
  85. * @param {HTMLElement} target 目标元素
  86. * @param {Object} [config] 配置
  87. * @param {string} [config.position='fixed'] 定位方式
  88. * @param {string} [config.top='50%'] `style.top`
  89. * @param {string} [config.left='50%'] `style.left`
  90. */
  91. setAbsoluteCenter(target, config) {
  92. if (!target._absoluteCenter) {
  93. const defaultConfig = {
  94. position: 'fixed',
  95. top: '50%',
  96. left: '50%',
  97. }
  98. config = { ...defaultConfig, ...config }
  99. target._absoluteCenter = () => {
  100. const style = getComputedStyle(target)
  101. const top = (parseFloat(style.height) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)) / 2
  102. const left = (parseFloat(style.width) + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)) / 2
  103. target.style.top = `calc(${config.top} - ${top}px)`
  104. target.style.left = `calc(${config.left} - ${left}px)`
  105. target.style.position = config.position
  106. }
  107. window.addEventListener('resize', api.tool.throttle(target._absoluteCenter), 100)
  108. }
  109. target._absoluteCenter()
  110. },
  111.  
  112. /**
  113. * 处理 HTML 元素的渐显和渐隐
  114. * @param {boolean} inOut 渐显/渐隐
  115. * @param {HTMLElement} target HTML 元素
  116. * @param {() => void} [callback] 处理完成的回调函数
  117. */
  118. fade(inOut, target, callback) {
  119. // fadeId 等同于当前时间戳,其意义在于保证对于同一元素,后执行的操作必将覆盖前的操作
  120. const fadeId = new Date().getTime()
  121. target._fadeId = fadeId
  122. if (inOut) { // 渐显
  123. // 只有 display 可视情况下修改 opacity 才会触发 transition
  124. if (getComputedStyle(target).display == 'none') {
  125. target.style.display = 'unset'
  126. }
  127. setTimeout(() => {
  128. let success = false
  129. if (target._fadeId <= fadeId) {
  130. target.style.opacity = '1'
  131. success = true
  132. }
  133. callback && callback(success)
  134. }, 10) // 此处的 10ms 是为了保证修改 display 后在浏览器上真正生效,按 HTML5 定义,浏览器需保证 display 在修改 4ms 后保证生效,但实际上大部分浏览器貌似做不到,等个 10ms 再修改 opacity
  135. } else { // 渐隐
  136. target.style.opacity = '0'
  137. setTimeout(() => {
  138. let success = false
  139. if (target._fadeId <= fadeId) {
  140. target.style.display = 'none'
  141. success = true
  142. }
  143. callback && callback(success)
  144. }, api.options.fadeTime)
  145. }
  146. },
  147.  
  148. /**
  149. * 为 HTML 元素添加 `class`
  150. * @param {HTMLElement} el 目标元素
  151. * @param {string} className `class`
  152. */
  153. addClass(el, className) {
  154. if (el instanceof HTMLElement) {
  155. if (!el.className) {
  156. el.className = className
  157. } else {
  158. const clz = el.className.split(' ')
  159. if (clz.indexOf(className) < 0) {
  160. clz.push(className)
  161. el.className = clz.join(' ')
  162. }
  163. }
  164. }
  165. },
  166.  
  167. /**
  168. * 为 HTML 元素移除 `class`
  169. * @param {HTMLElement} el 目标元素
  170. * @param {string} [className] `class`,未指定时移除所有 `class`
  171. */
  172. removeClass(el, className) {
  173. if (el instanceof HTMLElement) {
  174. if (typeof className == 'string') {
  175. if (el.className == className) {
  176. el.className = ''
  177. } else {
  178. let clz = el.className.split(' ')
  179. clz = clz.reduce((prev, current) => {
  180. if (current != className) {
  181. prev.push(current)
  182. }
  183. return prev
  184. }, [])
  185. el.className = clz.join(' ')
  186. }
  187. } else {
  188. el.className = ''
  189. }
  190. }
  191. },
  192.  
  193. /**
  194. * 判断 HTML 元素类名中是否含有 `class`
  195. * @param {HTMLElement | {className: string}} el 目标元素
  196. * @param {string | string[]} className `class`,支持同时判断多个
  197. * @param {boolean} [and] 同时判断多个 `class` 时,默认采取 `OR` 逻辑,是否采用 `AND` 逻辑
  198. * @returns {boolean} 是否含有 `class`
  199. */
  200. containsClass(el, className, and = false) {
  201. const trim = clz => clz.startsWith('.') ? clz.slice(1) : clz
  202. if (el instanceof HTMLElement || typeof el.className == 'string') {
  203. if (el.className == trim(String(className))) {
  204. return true
  205. } else {
  206. const clz = el.className.split(' ')
  207. if (className instanceof Array) {
  208. if (and) {
  209. for (const c of className) {
  210. if (clz.indexOf(trim(c)) < 0) {
  211. return false
  212. }
  213. }
  214. return true
  215. } else {
  216. for (const c of className) {
  217. if (clz.indexOf(trim(c)) >= 0) {
  218. return true
  219. }
  220. }
  221. return false
  222. }
  223. } else {
  224. return clz.indexOf(trim(className)) >= 0
  225. }
  226. }
  227. }
  228. return false
  229. },
  230.  
  231. /**
  232. * 判断 HTML 元素是否为 `fixed` 定位,或其是否在 `fixed` 定位的元素下
  233. * @param {HTMLElement} el 目标元素
  234. * @param {HTMLElement} [endEl] 终止元素,当搜索到该元素时终止判断(不会判断该元素)
  235. * @returns {boolean} HTML 元素是否为 `fixed` 定位,或其是否在 `fixed` 定位的元素下
  236. */
  237. isFixed(el, endEl) {
  238. while (el instanceof HTMLElement && el != endEl) {
  239. if (window.getComputedStyle(el).position == 'fixed') {
  240. return true
  241. }
  242. el = el.parentNode
  243. }
  244. return false
  245. },
  246. }
  247. /** 信息通知相关 */
  248. this.message = {
  249. /**
  250. * 创建信息
  251. * @param {string} msg 信息
  252. * @param {Object} [config] 设置
  253. * @param {boolean} [config.autoClose=true] 是否自动关闭信息,配合 `config.ms` 使用
  254. * @param {number} [config.ms=1500] 显示时间(单位:ms,不含渐显/渐隐时间)
  255. * @param {boolean} [config.html=false] 是否将 `msg` 理解为 HTML
  256. * @param {string} [config.width] 信息框的宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  257. * @param {{top: string, left: string}} [config.position] 信息框的位置,不设置该项时,相当于设置为 `{ top: '70%', left: '50%' }`
  258. * @return {HTMLElement} 信息框元素
  259. */
  260. create(msg, config) {
  261. const defaultConfig = {
  262. autoClose: true,
  263. ms: 1500,
  264. html: false,
  265. width: null,
  266. position: {
  267. top: '70%',
  268. left: '50%',
  269. },
  270. }
  271. config = { ...defaultConfig, ...config }
  272.  
  273. const msgbox = document.createElement('div')
  274. msgbox.className = `${api.options.id}-msgbox`
  275. if (config.width) {
  276. msgbox.style.minWidth = 'auto' // 为什么一个是 auto 一个是 none?真是神奇的设计
  277. msgbox.style.maxWidth = 'none'
  278. msgbox.style.width = config.width
  279. }
  280. msgbox.style.display = 'block'
  281. if (config.html) {
  282. msgbox.innerHTML = msg
  283. } else {
  284. msgbox.innerText = msg
  285. }
  286. document.body.appendChild(msgbox)
  287. setTimeout(() => {
  288. api.dom.setAbsoluteCenter(msgbox, config.position)
  289. }, 10)
  290.  
  291. api.dom.fade(true, msgbox, () => {
  292. if (config.autoClose) {
  293. setTimeout(() => {
  294. this.close(msgbox)
  295. }, config.ms)
  296. }
  297. })
  298. return msgbox
  299. },
  300.  
  301. /**
  302. * 关闭信息
  303. * @param {HTMLElement} msgbox 信息框元素
  304. */
  305. close(msgbox) {
  306. if (msgbox) {
  307. api.dom.fade(false, msgbox, () => {
  308. msgbox && msgbox.remove()
  309. })
  310. }
  311. },
  312.  
  313. /**
  314. * 创建高级信息
  315. * @param {HTMLElement} el 启动元素
  316. * @param {string} msg 信息
  317. * @param {string} flag 标志信息
  318. * @param {Object} [config] 设置
  319. * @param {string} [config.flagSize='1.8em'] 标志大小
  320. * @param {string} [config.width] 信息框的宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  321. * @param {{top: string, left: string}} [config.position] 信息框的位置,不设置该项时,沿用 `UserscriptAPI.message.create()` 的默认设置
  322. * @param {() => boolean} [config.disabled] 是否处于禁用状态
  323. */
  324. advanced(el, msg, flag, config) {
  325. const defaultConfig = {
  326. flagSize: '1.8em',
  327. // 不能把数据列出,否则解构的时候会出问题
  328. }
  329. config = { ...defaultConfig, ...config }
  330.  
  331. const _self = this
  332. el.show = false
  333. el.onmouseenter = function() {
  334. if (config.disabled && config.disabled()) {
  335. return
  336. }
  337.  
  338. const htmlMsg = `
  339. <table class="gm-advanced-table"><tr>
  340. <td style="font-size:${config.flagSize};line-height:${config.flagSize}">${flag}</td>
  341. <td>${msg}</td>
  342. </tr></table>
  343. `
  344. this.msgbox = _self.create(htmlMsg, { ...config, html: true, autoClose: false })
  345.  
  346. // 可能信息框刚好生成覆盖在 el 上,需要做一个处理
  347. this.msgbox.onmouseenter = function() {
  348. this.mouseOver = true
  349. }
  350. // 从信息框出来也会关闭信息框,防止覆盖的情况下无法关闭
  351. this.msgbox.onmouseleave = function() {
  352. _self.close(this)
  353. }
  354. }
  355. el.onmouseleave = function() {
  356. setTimeout(() => {
  357. if (this.msgbox && !this.msgbox.mouseOver) {
  358. this.msgbox.onmouseleave = null
  359. _self.close(this.msgbox)
  360. }
  361. })
  362. }
  363. },
  364. }
  365. /** 用于等待元素加载/条件达成再执行操作 */
  366. this.wait = {
  367. /**
  368. * 在条件满足后执行操作
  369. *
  370. * 当条件满足后,如果不存在终止条件,那么直接执行 `callback(result)`。
  371. *
  372. * 当条件满足后,如果存在终止条件,且 `stopTimeout` 大于 0,则还会在接下来的 `stopTimeout` 时间内判断是否满足终止条件,称为终止条件的二次判断。
  373. * 如果在此期间,终止条件通过,则表示依然不满足条件,故执行 `onStop()` 而非 `callback(result)`。
  374. * 如果在此期间,终止条件一直失败,则顺利通过检测,执行 `callback(result)`。
  375. *
  376. * @param {Object} options 选项
  377. * @param {() => *} options.condition 条件,当 `condition()` 返回的 `result` 为真值时满足条件
  378. * @param {(result) => void} [options.callback] 当满足条件时执行 `callback(result)`
  379. * @param {number} [options.interval=UserscriptAPI.options.conditionInterval] 检测时间间隔(单位:ms)
  380. * @param {number} [options.timeout=UserscriptAPI.options.conditionTimeout] 检测超时时间,检测时间超过该值时终止检测(单位:ms);设置为 `0` 时永远不会超时
  381. * @param {() => void} [options.onTimeout] 检测超时时执行 `onTimeout()`
  382. * @param {() => *} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  383. * @param {() => void} [options.onStop] 终止条件达成时执行 `onStop()`(包括终止条件的二次判断达成)
  384. * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
  385. * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
  386. * @param {(e) => void} [options.onError] 条件检测过程中发生错误时执行 `onError()`
  387. * @param {boolean} [options.stopOnError] 条件检测过程中发生错误时,是否终止检测
  388. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  389. * @returns {() => boolean} 执行后终止检测的函数
  390. */
  391. executeAfterConditionPassed(options) {
  392. options = {
  393. callback: result => api.logger.info(result),
  394. interval: api.options.conditionInterval,
  395. timeout: api.options.conditionTimeout,
  396. onTimeout: () => api.logger.error(['TIMEOUT', 'executeAfterConditionPassed', options]),
  397. stopCondition: null,
  398. onStop: () => api.logger.error(['STOP', 'executeAfterConditionPassed', options]),
  399. stopInterval: 50,
  400. stopTimeout: 0,
  401. onError: () => api.logger.error(['ERROR', 'executeAfterConditionPassed', options]),
  402. stopOnError: false,
  403. timePadding: 0,
  404. ...options,
  405. }
  406.  
  407. let tid = null
  408. let stop = false
  409. let cnt = 0
  410. let maxCnt = null
  411. if (options.timeout === 0) {
  412. maxCnt = 0
  413. } else {
  414. maxCnt = (options.timeout - options.timePadding) / options.interval
  415. }
  416. const task = async () => {
  417. let result = null
  418. try {
  419. result = await options.condition()
  420. } catch (e) {
  421. options.onError && options.onError.call(options, e)
  422. if (options.stopOnError) {
  423. clearInterval(tid)
  424. }
  425. }
  426. const stopResult = options.stopCondition && await options.stopCondition()
  427. if (stop) {
  428. clearInterval(tid)
  429. } else if (stopResult) {
  430. clearInterval(tid)
  431. options.onStop && options.onStop.call(options)
  432. } else if (maxCnt !== 0 && ++cnt > maxCnt) {
  433. clearInterval(tid)
  434. options.onTimeout && options.onTimeout.call(options)
  435. } else if (result) {
  436. clearInterval(tid)
  437. if (options.stopCondition && options.stopTimeout > 0) {
  438. this.executeAfterConditionPassed({
  439. condition: options.stopCondition,
  440. callback: options.onStop,
  441. interval: options.stopInterval,
  442. timeout: options.stopTimeout,
  443. onTimeout: () => options.callback.call(options, result)
  444. })
  445. } else {
  446. options.callback.call(options, result)
  447. }
  448. }
  449. }
  450. setTimeout(() => {
  451. tid = setInterval(task, options.interval)
  452. task()
  453. }, options.timePadding)
  454. return function() {
  455. stop = true
  456. }
  457. },
  458.  
  459. /**
  460. * 等待 DOM 中出现特定元素
  461. * @param {Object} options 选项
  462. * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
  463. * @param {HTMLElement} [options.base=document] 基元素
  464. * @param {HTMLElement[]} [options.exclude] 若 `element` 在其中则跳过,并继续检测
  465. * @param {(element: HTMLElement) => void} [options.callback] 当 `element` 加载成功时执行 `callback(element)`
  466. * @param {boolean} [options.subtree=true] 是否将检测范围扩展为基元素的整棵子树
  467. * @param {boolean} [options.multiple] 若一次检测到多个目标元素,是否在所有元素上执行回调函数(否则只处理第一个结果)
  468. * @param {boolean} [options.repeat] `element` 加载成功后是否继续检测
  469. * @param {number} [options.throttleWait=UserscriptAPI.options.elementThrottleWait] 检测节流时间(单位:ms);节流控制仅当 `repeat` 为 `false` 时生效,设置为 `0` 时禁用节流控制
  470. * @param {number} [options.timeout=UserscriptAPI.options.elementTimeout] 检测超时时间,检测时间超过该值时终止检测(单位:ms);设置为 `0` 时永远不会超时
  471. * @param {() => void} [options.onTimeout] 检测超时时执行 `onTimeout()`
  472. * @param {(e) => void} [options.onError] 检测过程中发生错误时执行 `onError()`
  473. * @param {boolean} [options.stopOnError] 检测过程中发生错误时,是否终止检测
  474. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  475. * @returns {() => boolean} 执行后终止检测的函数
  476. */
  477. executeAfterElementLoaded(options) {
  478. options = {
  479. base: document,
  480. exclude: null,
  481. callback: el => api.logger.info(el),
  482. subtree: true,
  483. multiple: false,
  484. repeat: false,
  485. throttleWait: api.options.elementThrottleWait,
  486. timeout: api.options.elementTimeout,
  487. onTimeout: () => api.logger.error(['TIMEOUT', 'executeAfterElementLoaded', options]),
  488. onError: () => api.logger.error(['ERROR', 'executeAfterElementLoaded', options]),
  489. stopOnError: false,
  490. timePadding: 0,
  491. ...options,
  492. }
  493.  
  494. let loaded = false
  495. let stopped = false
  496.  
  497. const stop = () => {
  498. if (!stopped) {
  499. stopped = true
  500. ob.disconnect()
  501. }
  502. }
  503.  
  504. const isExcluded = element => {
  505. return options.exclude && options.exclude.indexOf(element) >= 0
  506. }
  507.  
  508. const task = root => {
  509. let success = false
  510. if (options.multiple) {
  511. const elements = root.querySelectorAll(options.selector)
  512. if (elements.length > 0) {
  513. for (const element of elements) {
  514. if (!isExcluded(element)) {
  515. success = true
  516. options.callback.call(options, element)
  517. }
  518. }
  519. }
  520. } else {
  521. const element = root.querySelector(options.selector)
  522. if (element && !isExcluded(element)) {
  523. success = true
  524. options.callback.call(options, element)
  525. }
  526. }
  527. loaded = success || loaded
  528. return success
  529. }
  530. const singleTask = (!options.repeat && options.throttleWait > 0) ? api.tool.throttle(task, options.throttleWait) : task
  531.  
  532. const repeatTask = records => {
  533. let success = false
  534. for (const record of records) {
  535. for (const addedNode of record.addedNodes) {
  536. if (addedNode instanceof HTMLElement) {
  537. const virtualRoot = document.createElement('div')
  538. virtualRoot.appendChild(addedNode.cloneNode())
  539. const el = virtualRoot.querySelector(options.selector)
  540. if (el && !isExcluded(addedNode)) {
  541. success = true
  542. loaded = true
  543. options.callback.call(options, addedNode)
  544. if (!options.multiple) {
  545. return true
  546. }
  547. }
  548. success = task(addedNode) || success
  549. if (success && !options.multiple) {
  550. return true
  551. }
  552. }
  553. }
  554. }
  555. }
  556.  
  557. const ob = new MutationObserver(records => {
  558. try {
  559. if (options.repeat) {
  560. repeatTask(records)
  561. } else {
  562. singleTask(options.base)
  563. }
  564. if (loaded && !options.repeat) {
  565. stop()
  566. }
  567. } catch (e) {
  568. options.onError && options.onError.call(options, e)
  569. if (options.stopOnError) {
  570. stop()
  571. }
  572. }
  573. })
  574.  
  575. setTimeout(() => {
  576. try {
  577. task(options.base)
  578. } catch (e) {
  579. options.onError && options.onError.call(options, e)
  580. if (options.stopOnError) {
  581. stop()
  582. }
  583. }
  584. if (!stopped) {
  585. if (!loaded || options.repeat) {
  586. ob.observe(options.base, {
  587. childList: true,
  588. subtree: options.subtree,
  589. })
  590. if (options.timeout > 0) {
  591. setTimeout(() => {
  592. if (!stopped) {
  593. if (!loaded || options.repeat) {
  594. stop()
  595. }
  596. if (!loaded) {
  597. options.onTimeout && options.onTimeout.call(options)
  598. }
  599. }
  600. }, Math.max(options.timeout - options.timePadding, 0))
  601. }
  602. }
  603. }
  604. }, options.timePadding)
  605. return stop
  606. },
  607.  
  608. /**
  609. * 等待条件满足
  610. *
  611. * 执行细节类似于 {@link executeAfterConditionPassed}。在原来执行 `callback(result)` 的地方执行 `resolve(result)`,被终止或超时执行 `reject()`。
  612. * @async
  613. * @param {Object} options 选项
  614. * @param {() => *} options.condition 条件,当 `condition()` 返回的 `result` 为真值时满足条件
  615. * @param {number} [options.interval=UserscriptAPI.options.conditionInterval] 检测时间间隔(单位:ms)
  616. * @param {number} [options.timeout=UserscriptAPI.options.conditionTimeout] 检测超时时间,检测时间超过该值时终止检测(单位:ms);设置为 `0` 时永远不会超时
  617. * @param {() => *} [options.stopCondition] 终止条件,当 `stopCondition()` 返回的 `stopResult` 为真值时终止检测
  618. * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
  619. * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
  620. * @param {boolean} [options.stopOnError] 条件检测过程中发生错误时,是否终止检测
  621. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  622. * @returns {Promise} `result`
  623. * @throws 当等待超时或者被终止时抛出
  624. * @see executeAfterConditionPassed
  625. */
  626. async waitForConditionPassed(options) {
  627. return new Promise((resolve, reject) => {
  628. this.executeAfterConditionPassed({
  629. ...options,
  630. callback: result => resolve(result),
  631. onTimeout: function() {
  632. reject(['TIMEOUT', 'waitForConditionPassed', this])
  633. },
  634. onStop: function() {
  635. reject(['STOP', 'waitForConditionPassed', this])
  636. },
  637. onError: function(e) {
  638. reject(['ERROR', 'waitForConditionPassed', this, e])
  639. },
  640. })
  641. })
  642. },
  643.  
  644. /**
  645. * 等待元素加载
  646. *
  647. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  648. * @async
  649. * @param {Object} options 选项
  650. * @param {string} options.selector 该选择器指定要等待加载的元素 `element`
  651. * @param {HTMLElement} [options.base=document] 基元素
  652. * @param {HTMLElement[]} [options.exclude] 若 `element` 在其中则跳过,并继续检测
  653. * @param {boolean} [options.subtree=true] 是否将检测范围扩展为基元素的整棵子树
  654. * @param {number} [options.throttleWait=UserscriptAPI.options.elementThrottleWait] 检测节流时间(单位:ms);节流控制仅当 `repeat` 为 `false` 时生效,设置为 `0` 时禁用节流控制
  655. * @param {number} [options.timeout=UserscriptAPI.options.elementTimeout] 检测超时时间,检测时间超过该值时终止检测(单位:ms);设置为 `0` 时永远不会超时
  656. * @param {boolean} [options.stopOnError] 检测过程中发生错误时,是否终止检测
  657. * @param {number} [options.timePadding=0] 等待 `timePadding`ms 后才开始执行;包含在 `timeout` 中,因此不能大于 `timeout`
  658. * @returns {Promise<HTMLElement>} `element`
  659. * @throws 当等待超时时抛出
  660. * @see executeAfterElementLoaded
  661. */
  662. async waitForElementLoaded(options) {
  663. return new Promise((resolve, reject) => {
  664. this.executeAfterElementLoaded({
  665. ...options,
  666. callback: element => resolve(element),
  667. onTimeout: function() {
  668. reject(['TIMEOUT', 'waitForElementLoaded', this])
  669. },
  670. onError: function() {
  671. reject(['ERROR', 'waitForElementLoaded', this])
  672. },
  673. })
  674. })
  675. },
  676.  
  677. /**
  678. * 元素加载选择器
  679. *
  680. * 执行细节类似于 {@link executeAfterElementLoaded}。在原来执行 `callback(element)` 的地方执行 `resolve(element)`,被终止或超时执行 `reject()`。
  681. * @async
  682. * @param {string} selector 该选择器指定要等待加载的元素 `element`
  683. * @param {HTMLElement} [base=document] 基元素
  684. * @param {boolean} [subtree=true] 是否将检测范围扩展为基元素的整棵子树
  685. * @returns {Promise<HTMLElement>} `element`
  686. * @throws 当等待超时时抛出
  687. * @see executeAfterElementLoaded
  688. */
  689. async waitQuerySelector(selector, base = document, subtree = true) {
  690. const options = { selector, base, subtree }
  691. return new Promise((resolve, reject) => {
  692. this.executeAfterElementLoaded({
  693. ...options,
  694. callback: element => resolve(element),
  695. onTimeout: function() {
  696. reject(['TIMEOUT', 'waitQuerySelector', this])
  697. },
  698. onError: function() {
  699. reject(['ERROR', 'waitQuerySelector', this])
  700. },
  701. })
  702. })
  703. },
  704. }
  705. /** 网络相关 */
  706. this.web = {
  707. /** @typedef {Object} GM_xmlhttpRequest_details */
  708. /** @typedef {Object} GM_xmlhttpRequest_response */
  709. /**
  710. * 发起网络请求
  711. * @async
  712. * @param {GM_xmlhttpRequest_details} details 定义及细节同 {@link GM_xmlhttpRequest} 的 `details`
  713. * @returns {Promise<GM_xmlhttpRequest_response>} 响应对象
  714. * @throws 当请求发生错误或者超时时抛出
  715. * @see {@link https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest GM_xmlhttpRequest}
  716. */
  717. async request(details) {
  718. if (details) {
  719. return new Promise((resolve, reject) => {
  720. const throwHandler = function(msg) {
  721. api.logger.error('NETWORK REQUEST ERROR')
  722. reject(msg)
  723. }
  724. details.onerror = details.onerror || (() => throwHandler(['ERROR', 'request', details]))
  725. details.ontimeout = details.ontimeout || (() => throwHandler(['TIMEOUT', 'request', details]))
  726. details.onload = details.onload || (response => resolve(response))
  727. GM_xmlhttpRequest(details)
  728. })
  729. }
  730. },
  731.  
  732. /** @typedef {Object} GM_download_details */
  733. /**
  734. * 下载资源
  735. * @param {GM_download_details} details 定义及细节同 {@link GM_download} 的 `details`
  736. * @returns {() => void} 用于终止下载的方法
  737. * @see {@link https://www.tampermonkey.net/documentation.php#GM_download GM_download}
  738. */
  739. download(details) {
  740. if (details) {
  741. try {
  742. const cfg = { ...details }
  743. let name = cfg.name
  744. if (name.indexOf('.') > -1) {
  745. let parts = cfg.url.split('/')
  746. const last = parts[parts.length - 1].split('?')[0]
  747. if (last.indexOf('.') > -1) {
  748. parts = last.split('.')
  749. name = `${name}.${parts[parts.length - 1]}`
  750. } else {
  751. name = name.replaceAll('.', '_')
  752. }
  753. cfg.name = name
  754. }
  755. if (!cfg.onerror) {
  756. cfg.onerror = function(error, details) {
  757. api.logger.error('DOWNLOAD ERROR')
  758. api.logger.error([error, details])
  759. }
  760. }
  761. if (!cfg.ontimeout) {
  762. cfg.ontimeout = function() {
  763. api.logger.error('DOWNLOAD TIMEOUT')
  764. }
  765. }
  766. GM_download(cfg)
  767. } catch (e) {
  768. api.logger.error('DOWNLOAD ERROR')
  769. api.logger.error(e)
  770. }
  771. }
  772. return () => {}
  773. },
  774.  
  775. /**
  776. * 判断给定 URL 是否匹配
  777. * @param {RegExp | RegExp[]} reg 用于判断是否匹配的正则表达式,或正则表达式数组
  778. * @param {'SINGLE' | 'AND' | 'OR'} [mode='SINGLE'] 匹配模式
  779. * @returns {boolean} 是否匹配
  780. */
  781. urlMatch(reg, mode = 'SINGLE') {
  782. let result = false
  783. const href = location.href
  784. if (mode == 'SINGLE') {
  785. if (reg instanceof Array) {
  786. if (reg.length > 0) {
  787. reg = reg[0]
  788. } else {
  789. reg = null
  790. }
  791. }
  792. if (reg) {
  793. result = reg.test(href)
  794. }
  795. } else {
  796. if (!(reg instanceof Array)) {
  797. reg = [reg]
  798. }
  799. if (reg.length > 0) {
  800. if (mode == 'AND') {
  801. result = true
  802. for (const r of reg) {
  803. if (!r.test(href)) {
  804. result = false
  805. break
  806. }
  807. }
  808. } else if (mode == 'OR') {
  809. for (const r of reg) {
  810. if (r.test(href)) {
  811. result = true
  812. break
  813. }
  814. }
  815. }
  816. }
  817. }
  818. return result
  819. },
  820. }
  821. /**
  822. * 日志
  823. */
  824. this.logger = {
  825. /**
  826. * 打印格式化日志
  827. * @param {*} message 日志信息
  828. * @param {string} label 日志标签
  829. * @param {boolean} [error] 是否错误信息
  830. */
  831. log(message, label, error) {
  832. const output = console[error ? 'error' : 'log']
  833. const type = typeof message == 'string' ? '%s' : '%o'
  834. output(`%c${label}%c${type}`, logCss, '', message)
  835. },
  836.  
  837. /**
  838. * 打印日志
  839. * @param {*} message 日志信息
  840. */
  841. info(message) {
  842. if (message === undefined) {
  843. message = '[undefined]'
  844. } else if (message === null) {
  845. message = '[null]'
  846. } else if (message === '') {
  847. message = '[empty string]'
  848. }
  849. if (api.options.label) {
  850. this.log(message, api.options.label)
  851. } else {
  852. console.log(message)
  853. }
  854. },
  855.  
  856. /**
  857. * 打印错误日志
  858. * @param {*} message 错误日志信息
  859. */
  860. error(message) {
  861. if (message === undefined) {
  862. message = '[undefined]'
  863. } else if (message === null) {
  864. message = '[null]'
  865. } else if (message === '') {
  866. message = '[empty string]'
  867. }
  868. if (api.options.label) {
  869. this.log(message, api.options.label, true)
  870. } else {
  871. console.error(message)
  872. }
  873. },
  874. }
  875. /**
  876. * 工具
  877. */
  878. this.tool = {
  879. /**
  880. * 生成消抖函数
  881. * @param {Function} fn 目标函数
  882. * @param {number} [wait=0] 消抖延迟(单位:ms)
  883. * @param {Object} [options] 选项
  884. * @param {boolean} [options.leading] 是否在延迟开始前调用目标函数
  885. * @param {boolean} [options.trailing=true] 是否在延迟结束后调用目标函数
  886. * @param {number} [options.maxWait=0] 最大延迟时间(非精确实现),`0` 表示禁用(单位:ms)
  887. * @returns {Function} 消抖函数 `debounced`,可调用 `debounced.cancel()` 取消执行
  888. */
  889. debounce(fn, wait = 0, options = {}) {
  890. options = {
  891. leading: false,
  892. trailing: true,
  893. maxWait: 0,
  894. ...options,
  895. }
  896.  
  897. let tid = null
  898. let start = null
  899. let execute = null
  900. let callback = null
  901.  
  902. function debounced() {
  903. execute = () => {
  904. fn.apply(null, arguments)
  905. execute = null
  906. }
  907. callback = () => {
  908. if (options.trailing) {
  909. execute && execute()
  910. }
  911. tid = null
  912. start = null
  913. }
  914.  
  915. if (tid) {
  916. clearTimeout(tid)
  917. if (options.maxWait > 0 && new Date().getTime() - start > options.maxWait) {
  918. callback()
  919. }
  920. }
  921.  
  922. if (!tid && options.leading) {
  923. execute && execute()
  924. }
  925.  
  926. if (!start) {
  927. start = new Date().getTime()
  928. }
  929.  
  930. tid = setTimeout(callback, wait)
  931. }
  932.  
  933. debounced.cancel = function() {
  934. if (tid) {
  935. clearTimeout(tid)
  936. tid = null
  937. start = null
  938. }
  939. }
  940.  
  941. return debounced
  942. },
  943.  
  944. /**
  945. * 生成节流函数
  946. * @param {Function} fn 目标函数
  947. * @param {number} [wait=0] 节流延迟(单位:ms)
  948. * @returns {Function} 节流函数 `throttled`,可调用 `throttled.cancel()` 取消执行
  949. */
  950. throttle(fn, wait = 0) {
  951. return this.debounce(fn, wait, {
  952. leading: true,
  953. trailing: true,
  954. maxWait: wait,
  955. })
  956. },
  957. }
  958.  
  959. const css = document.head.appendChild(document.createElement('style'))
  960. css.id = `_api_${api.options.id}-css`
  961. css.type = 'text/css'
  962. css.innerHTML = `
  963. :root {
  964. --light-text-color: white;
  965. --shadow-color: #000000bf;
  966. }
  967.  
  968. .${api.options.id}-msgbox {
  969. z-index: 65535;
  970. background-color: var(--shadow-color);
  971. font-size: 16px;
  972. max-width: 24em;
  973. min-width: 2em;
  974. color: var(--light-text-color);
  975. padding: 0.5em 1em;
  976. border-radius: 0.6em;
  977. opacity: 0;
  978. transition: opacity ${api.options.fadeTime}ms ease-in-out;
  979. user-select: none;
  980. }
  981.  
  982. .${api.options.id}-msgbox .gm-advanced-table td {
  983. vertical-align: middle;
  984. }
  985. .${api.options.id}-msgbox .gm-advanced-table td:first-child {
  986. padding-right: 0.6em;
  987. }
  988. `
  989. }
  990. }