UserscriptAPI

My API for userscripts.

נכון ליום 14-08-2021. ראה הגרסה האחרונה.

אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greatest.deepsurf.us/scripts/409641/960339/UserscriptAPI.js

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