Greasy Fork is available in English.

UserscriptAPI

My API for userscripts.

As of 26. 06. 2021. See the latest version.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/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. }