UserscriptAPI

My API for userscripts.

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

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

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