Greasy Fork is available in English.

UserscriptAPI

My API for userscripts.

Ekde 2021/07/15. Vidu La ĝisdata versio.

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/950773/UserscriptAPI.js

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