UserscriptAPI

My API for userscripts.

目前為 2021-06-30 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/409641/945829/UserscriptAPI.js

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