UserscriptAPI

My API for userscripts.

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

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

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