UserscriptAPI

My API for userscripts.

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

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

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