UserscriptAPI

My API for userscripts.

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

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