UserscriptAPI

My API for userscripts.

Fra 27.07.2021. Se den seneste versjonen.

Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://update.greatest.deepsurf.us/scripts/409641/954686/UserscriptAPI.js

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