UserscriptAPI

My API for userscripts.

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/409641/1435266/UserscriptAPI.js

  1. /* exported UserscriptAPI */
  2. /**
  3. * UserscriptAPI
  4. *
  5. * 需要引入模块方可工作,详见 `README.md`。
  6. * @version 2.2.1.20230314
  7. * @author Laster2800
  8. * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
  9. */
  10. class UserscriptAPI {
  11. /** @type {{[name: string]: Function}} 可访问模块 */
  12. static #modules = {}
  13. /** @type {string[]} 待添加模块样式队列 */
  14. #moduleCssQueue = []
  15.  
  16. /**
  17. * @param {Object} [options] 选项
  18. * @param {string} [options.id='default'] 标识符
  19. * @param {string} [options.label] 日志标签,为空时不设置标签
  20. * @param {Object} [options.wait] `wait` API 默认选项(默认值见构造器代码)
  21. * @param {Object} [options.wait.condition] `wait` 条件 API 默认选项
  22. * @param {Object} [options.wait.element] `wait` 元素 API 默认选项
  23. * @param {number} [options.fadeTime=400] UI 渐变时间
  24. */
  25. constructor(options) {
  26. this.options = {
  27. id: 'default',
  28. label: null,
  29. fadeTime: 400,
  30. ...options,
  31. wait: {
  32. condition: {
  33. callback: result => this.logger.info(result),
  34. interval: 100,
  35. timeout: 10000,
  36. onTimeout: options => this.logger[options.stopOnTimeout ? 'error' : 'warn']('executeAfterConditionPassed: TIMEOUT', options),
  37. stopOnTimeout: true,
  38. stopCondition: null,
  39. onStop: options => this.logger.error('executeAfterConditionPassed: STOP', options),
  40. stopInterval: 50,
  41. stopTimeout: 0,
  42. onError: (options, e) => this.logger.error('executeAfterConditionPassed: ERROR', options, e),
  43. stopOnError: true,
  44. timePadding: 0,
  45. ...options?.wait?.condition,
  46. },
  47. element: {
  48. base: document,
  49. exclude: null,
  50. callback: el => this.logger.info(el),
  51. subtree: true,
  52. multiple: false,
  53. repeat: false,
  54. throttleWait: 100,
  55. timeout: 10000,
  56. onTimeout: options => this.logger[options.stopOnTimeout ? 'error' : 'warn']('executeAfterElementLoaded: TIMEOUT', options),
  57. stopOnTimeout: false,
  58. stopCondition: null,
  59. onStop: options => this.logger.error('executeAfterElementLoaded: STOP', options),
  60. onError: (options, e) => this.logger.error('executeAfterElementLoaded: ERROR', options, e),
  61. stopOnError: true,
  62. timePadding: 0,
  63. ...options?.wait?.element,
  64. },
  65. },
  66. }
  67.  
  68. /** @type {UserscriptAPIDom} */
  69. this.dom = this.#getModuleInstance('dom')
  70. /** @type {UserscriptAPIMessage} */
  71. this.message = this.#getModuleInstance('message')
  72. /** @type {UserscriptAPIWait} */
  73. this.wait = this.#getModuleInstance('wait')
  74. /** @type {UserscriptAPIWeb} */
  75. this.web = this.#getModuleInstance('web')
  76.  
  77. if (!this.message) {
  78. this.message = {
  79. api: this,
  80. alert: this.base.alert,
  81. confirm: this.base.confirm,
  82. prompt: this.base.prompt,
  83. }
  84. }
  85.  
  86. for (const css of this.#moduleCssQueue) {
  87. this.base.addStyle(css)
  88. }
  89. }
  90.  
  91. /**
  92. * 注册模块
  93. * @param {string} name 模块名称
  94. * @param {Object} module 模块类
  95. */
  96. static registerModule(name, module) {
  97. this.#modules[name] = module
  98. }
  99.  
  100. /**
  101. * 获取模块实例
  102. * @param {string} name 模块名称
  103. * @returns {Object} 模块实例,无对应模块时返回 `null`
  104. */
  105. #getModuleInstance(name) {
  106. const module = UserscriptAPI.#modules[name]
  107. return module ? new module(this) : null
  108. }
  109.  
  110. /**
  111. * 初始化模块样式(仅应在模块构造器中使用)
  112. * @param {string} css 样式
  113. */
  114. initModuleStyle(css) {
  115. this.#moduleCssQueue.push(css)
  116. }
  117.  
  118. /**
  119. * UserscriptAPIBase
  120. * @version 1.3.0.20240827
  121. */
  122. base = new class UserscriptAPIBase {
  123. /**
  124. * @param {UserscriptAPI} api `UserscriptAPI`
  125. */
  126. constructor(api) {
  127. this.api = api
  128. }
  129.  
  130. /**
  131. * 添加样式
  132. * @param {string} css 样式
  133. * @param {Document | DocumentFragment} [doc=document] 文档
  134. * @returns {HTMLStyleElement} `<style>`
  135. */
  136. addStyle(css, doc = document) {
  137. const { api } = this
  138. let style = null
  139. if (doc instanceof Document) {
  140. style = doc.createElement('style')
  141. style.className = `${api.options.id}-style`
  142. style.textContent = css
  143. const parent = doc.head || doc.documentElement
  144. if (parent) {
  145. parent.append(style)
  146. } else { // 极端情况下会出现,DevTools 网络+CPU 双限制可模拟
  147. api.wait?.waitForConditionPassed({
  148. condition: () => doc.head || doc.documentElement,
  149. timeout: 0,
  150. }).then(parent => parent.append(style))
  151. }
  152. } else if (doc instanceof DocumentFragment) {
  153. style = document.createElement('style')
  154. style.className = `${api.options.id}-style`
  155. style.textContent = css
  156. doc.appendChild(style)
  157. }
  158. return style
  159. }
  160.  
  161. /**
  162. * 判断给定 URL 是否匹配
  163. * @param {RegExp | RegExp[]} regex 用于判断是否匹配的正则表达式,或正则表达式数组
  164. * @param {'OR' | 'AND'} [mode='OR'] 匹配模式
  165. * @returns {boolean} 是否匹配
  166. */
  167. urlMatch(regex, mode = 'OR') {
  168. let result = false
  169. const { href } = location
  170. if (Array.isArray(regex)) {
  171. if (regex.length > 0) {
  172. if (mode === 'AND') {
  173. result = true
  174. for (const ex of regex) {
  175. if (!ex.test(href)) {
  176. result = false
  177. break
  178. }
  179. }
  180. } else if (mode === 'OR') {
  181. for (const ex of regex) {
  182. if (ex.test(href)) {
  183. result = true
  184. break
  185. }
  186. }
  187. }
  188. }
  189. } else {
  190. result = regex.test(href)
  191. }
  192. return result
  193. }
  194.  
  195. /**
  196. * 初始化 `urlchange` 事件
  197. * @example
  198. * window.addEventListener('urlchange', e => { ... })
  199. * window.addEventListener('urlchange', e => e.stopPropagation(), true)
  200. * window.onurlchange = function(e) { ... }
  201. * @see {@link https://stackoverflow.com/a/52809105 How to detect if URL has changed after hash in JavaScript}
  202. * @see {@link https://stackoverflow.com/a/69342637 Event bubbles before captured on `window`}
  203. */
  204. initUrlchangeEvent() {
  205. const win = typeof unsafeWindow === 'object' ? unsafeWindow : window
  206. if (win[Symbol.for('onurlchange')] === undefined) {
  207. let url = new URL(location.href)
  208. const dispatchEvent = () => {
  209. const event = new CustomEvent('urlchange', {
  210. detail: { prev: url, curr: new URL(location.href) },
  211. bubbles: true,
  212. })
  213. url = event.detail.curr
  214. if (typeof window.onurlchange === 'function') { // 若直接调用则 eventPhase 不对,且会有一些其他问题
  215. // 这一方案只能让事件处理器属性在最后被激活,但正确的顺序是:https://stackoverflow.com/a/49806959
  216. // 要实现正确的顺序,需用 defineProperty 定义 onurlchange,但 Tampermonkey 已经定义了该属性
  217. // 尽管目前 Tampermonkey 定义的属性是可写的,但为了向前兼容性及简化代码考虑,决定采用当前方案
  218. window.addEventListener('urlchange', window.onurlchange, { once: true })
  219. }
  220. document.dispatchEvent(event) // 在 window 上 dispatch 不能确保在冒泡前捕获,至少目前是这样
  221. }
  222.  
  223. history.pushState = (f => (...args) => {
  224. const ret = Reflect.apply(f, history, args)
  225. dispatchEvent()
  226. return ret
  227. })(history.pushState)
  228. history.replaceState = (f => (...args) => {
  229. const ret = Reflect.apply(f, history, args)
  230. dispatchEvent()
  231. return ret
  232. })(history.replaceState)
  233. window.addEventListener('popstate', () => {
  234. dispatchEvent()
  235. })
  236. win[Symbol.for('onurlchange')] = true
  237. }
  238. }
  239.  
  240. /**
  241. * 生成消抖函数
  242. * @param {Function} fn 目标函数
  243. * @param {number} [wait=0] 消抖延迟
  244. * @param {Object} [options] 选项
  245. * @param {boolean} [options.leading] 是否在延迟开始前调用目标函数
  246. * @param {boolean} [options.trailing=true] 是否在延迟结束后调用目标函数
  247. * @param {number} [options.maxWait=0] 最大延迟时间(非准确),`0` 表示禁用
  248. * @returns {Function} 消抖函数 `debounced`,可调用 `debounced.cancel()` 取消执行
  249. */
  250. debounce(fn, wait = 0, options = {}) {
  251. options = {
  252. leading: false,
  253. trailing: true,
  254. maxWait: 0,
  255. ...options,
  256. }
  257.  
  258. let tid = null
  259. let start = null
  260. let execute = null
  261. let callback = null
  262.  
  263. /** @this {*} thisArg */
  264. function debounced(...args) {
  265. execute = () => {
  266. Reflect.apply(fn, this, args)
  267. execute = null
  268. }
  269. callback = () => {
  270. if (options.trailing) {
  271. execute?.()
  272. }
  273. tid = null
  274. start = null
  275. }
  276.  
  277. if (tid) {
  278. clearTimeout(tid)
  279. if (options.maxWait > 0 && Date.now() - start > options.maxWait) {
  280. callback()
  281. }
  282. }
  283.  
  284. if (!tid && options.leading) {
  285. execute?.()
  286. }
  287.  
  288. if (!start) {
  289. start = Date.now()
  290. }
  291.  
  292. tid = setTimeout(callback, wait)
  293. }
  294.  
  295. debounced.cancel = function() {
  296. if (tid) {
  297. clearTimeout(tid)
  298. tid = null
  299. start = null
  300. }
  301. }
  302.  
  303. return debounced
  304. }
  305.  
  306. /**
  307. * 生成节流函数
  308. * @param {Function} fn 目标函数
  309. * @param {number} [wait=0] 节流延迟(非准确)
  310. * @returns {Function} 节流函数 `throttled`,可调用 `throttled.cancel()` 取消执行
  311. */
  312. throttle(fn, wait = 0) {
  313. return this.debounce(fn, wait, {
  314. leading: true,
  315. trailing: true,
  316. maxWait: wait,
  317. })
  318. }
  319.  
  320. /**
  321. * 创建基础提醒对话框(异步)
  322. *
  323. * 若没有引入 `message` 模块,可使用 `api.message.alert()` 引用该方法。
  324. * @param {string} msg 信息
  325. */
  326. alert(msg) {
  327. const { label } = this.api.options
  328. return new Promise(resolve => {
  329. resolve(alert(`${label ? `${label}\n\n` : ''}${msg}`))
  330. })
  331. }
  332.  
  333. /**
  334. * 创建基础确认对话框(异步)
  335. *
  336. * 若没有引入 `message` 模块,可使用 `api.message.confirm()` 引用该方法。
  337. * @param {string} msg 信息
  338. * @returns {Promise<boolean>} 用户输入
  339. */
  340. confirm(msg) {
  341. const { label } = this.api.options
  342. return new Promise(resolve => {
  343. resolve(confirm(`${label ? `${label}\n\n` : ''}${msg}`))
  344. })
  345. }
  346.  
  347. /**
  348. * 创建基础输入对话框(异步)
  349. *
  350. * 若没有引入 `message` 模块,可使用 `api.message.prompt()` 引用该方法。
  351. * @param {string} msg 信息
  352. * @param {string} [val] 默认值
  353. * @returns {Promise<string>} 用户输入
  354. */
  355. prompt(msg, val) {
  356. const { label } = this.api.options
  357. return new Promise(resolve => {
  358. resolve(prompt(`${label ? `${label}\n\n` : ''}${msg}`, val))
  359. })
  360. }
  361. }(this)
  362.  
  363. /**
  364. * UserscriptAPILogger
  365. * @version 1.2.0.20210925
  366. */
  367. logger = new class UserscriptAPILogger {
  368. #logCss = `
  369. background-color: black;
  370. color: white;
  371. border-radius: 2px;
  372. padding: 2px;
  373. margin-right: 4px;
  374. `
  375.  
  376. /**
  377. * @param {UserscriptAPI} api `UserscriptAPI`
  378. */
  379. constructor(api) {
  380. this.api = api
  381. }
  382.  
  383. /**
  384. * 打印格式化日志
  385. * @param {'info' | 'warn' | 'error'} fn 日志函数名
  386. * @param {*[]} message 日志信息
  387. */
  388. #log(fn, ...message) {
  389. const output = console[fn]
  390. const label = this.api.options.label ?? ''
  391. const causes = []
  392. let template = null
  393. if (message.length > 0) {
  394. const types = []
  395. for (const [idx, m] of message.entries()) {
  396. if (m) {
  397. types.push(typeof m === 'string' ? '%s' : '%o')
  398. if (m instanceof Error && m.cause !== undefined) {
  399. causes.push(m.cause)
  400. }
  401. } else {
  402. if (m === undefined) {
  403. message[idx] = '[undefined]'
  404. } else if (m === null) {
  405. message[idx] = '[null]'
  406. } else if (m === '') {
  407. message[idx] = '[empty string]'
  408. }
  409. types.push(typeof message[idx] === 'string' ? '%s' : '%o')
  410. }
  411. }
  412. template = types.join(', ')
  413. } else {
  414. template = '[undefined]'
  415. }
  416. output(`%c${label}%c${template}`, this.#logCss, null, ...message)
  417. for (const [idx, cause] of causes.entries()) {
  418. output(`%c${label}%c${idx + 1}-th error is caused by %o`, this.#logCss, null, cause)
  419. }
  420. }
  421.  
  422. /**
  423. * 打印日志
  424. * @param {*[]} message 日志信息
  425. */
  426. info(...message) {
  427. this.#log('info', ...message)
  428. }
  429.  
  430. /**
  431. * 打印警告日志
  432. * @param {*[]} message 警告日志信息
  433. */
  434. warn(...message) {
  435. this.#log('warn', ...message)
  436. }
  437.  
  438. /**
  439. * 打印错误日志
  440. * @param {*[]} message 错误日志信息
  441. */
  442. error(...message) {
  443. this.#log('error', ...message)
  444. }
  445. }(this)
  446. }