UserscriptAPI

My API for userscripts.

当前为 2021-06-29 提交的版本,查看 最新版本

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

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