UserscriptAPI

My API for userscripts.

Από την 11/08/2021. Δείτε την τελευταία έκδοση.

Αυτός ο κώδικας δεν πρέπει να εγκατασταθεί άμεσα. Είναι μια βιβλιοθήκη για άλλους κώδικες που περιλαμβάνεται μέσω της οδηγίας meta // @require https://update.greatest.deepsurf.us/scripts/409641/959254/UserscriptAPI.js

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