UserscriptAPI

My API for userscripts.

2021/07/22のページです。最新版はこちら

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greatest.deepsurf.us/scripts/409641/953038/UserscriptAPI.js

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