UserscriptAPI

My API for userscripts.

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

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

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