UserscriptAPI

My API for userscripts.

当前为 2021-09-10 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/409641/969309/UserscriptAPI.js

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