UserscriptAPI

My API for userscripts.

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

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

  1. /* exported UserscriptAPI */
  2. /**
  3. * UserscriptAPI
  4. *
  5. * 需要引入模块方可工作,详见 `README.md`。
  6. * @version 2.2.0.20210925
  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. * @param {string} name 模块名称
  102. * @returns {Object} 模块实例,无对应模块时返回 `null`
  103. */
  104. #getModuleInstance(name) {
  105. const module = UserscriptAPI.#modules[name]
  106. return module ? new module(this) : null
  107. }
  108.  
  109. /**
  110. * 初始化模块样式(仅应在模块构造器中使用)
  111. * @param {string} css 样式
  112. */
  113. initModuleStyle(css) {
  114. this.#moduleCssQueue.push(css)
  115. }
  116.  
  117. /**
  118. * UserscriptAPIBase
  119. * @version 1.1.0.20210925
  120. */
  121. base = new class UserscriptAPIBase {
  122. /**
  123. * @param {UserscriptAPI} api `UserscriptAPI`
  124. */
  125. constructor(api) {
  126. this.api = api
  127. }
  128.  
  129. /**
  130. * 添加样式
  131. * @param {string} css 样式
  132. * @param {Document} [doc=document] 文档
  133. * @returns {HTMLStyleElement} `<style>`
  134. */
  135. addStyle(css, doc = document) {
  136. const { api } = this
  137. const style = doc.createElement('style')
  138. style.className = `${api.options.id}-style`
  139. style.textContent = css
  140. const parent = doc.head || doc.documentElement
  141. if (parent) {
  142. parent.append(style)
  143. } else { // 极端情况下会出现,DevTools 网络+CPU 双限制可模拟
  144. api.wait?.waitForConditionPassed({
  145. condition: () => doc.head || doc.documentElement,
  146. timeout: 0,
  147. }).then(parent => parent.append(style))
  148. }
  149. return style
  150. }
  151.  
  152. /**
  153. * 判断给定 URL 是否匹配
  154. * @param {RegExp | RegExp[]} regex 用于判断是否匹配的正则表达式,或正则表达式数组
  155. * @param {'OR' | 'AND'} [mode='OR'] 匹配模式
  156. * @returns {boolean} 是否匹配
  157. */
  158. urlMatch(regex, mode = 'OR') {
  159. let result = false
  160. const { href } = location
  161. if (Array.isArray(regex)) {
  162. if (regex.length > 0) {
  163. if (mode === 'AND') {
  164. result = true
  165. for (const ex of regex) {
  166. if (!ex.test(href)) {
  167. result = false
  168. break
  169. }
  170. }
  171. } else if (mode === 'OR') {
  172. for (const ex of regex) {
  173. if (ex.test(href)) {
  174. result = true
  175. break
  176. }
  177. }
  178. }
  179. }
  180. } else {
  181. result = regex.test(href)
  182. }
  183. return result
  184. }
  185.  
  186. /**
  187. * 初始化 `urlchange` 事件
  188. * @see {@link https://stackoverflow.com/a/52809105 How to detect if URL has changed after hash in JavaScript}
  189. */
  190. initUrlchangeEvent() {
  191. if (!history._urlchangeEvent) {
  192. const urlEvent = () => {
  193. const event = new Event('urlchange')
  194. // 添加属性,使其与 Tampermonkey urlchange 保持一致
  195. event.url = location.href
  196. return event
  197. }
  198. history.pushState = (f => function() {
  199. const ret = Reflect.apply(f, this, arguments)
  200. window.dispatchEvent(new Event('pushstate'))
  201. window.dispatchEvent(urlEvent())
  202. return ret
  203. })(history.pushState)
  204. history.replaceState = (f => function() {
  205. const ret = Reflect.apply(f, this, arguments)
  206. window.dispatchEvent(new Event('replacestate'))
  207. window.dispatchEvent(urlEvent())
  208. return ret
  209. })(history.replaceState)
  210. window.addEventListener('popstate', () => {
  211. window.dispatchEvent(urlEvent())
  212. })
  213. history._urlchangeEvent = true
  214. }
  215. }
  216.  
  217. /**
  218. * 生成消抖函数
  219. * @param {Function} fn 目标函数
  220. * @param {number} [wait=0] 消抖延迟
  221. * @param {Object} [options] 选项
  222. * @param {boolean} [options.leading] 是否在延迟开始前调用目标函数
  223. * @param {boolean} [options.trailing=true] 是否在延迟结束后调用目标函数
  224. * @param {number} [options.maxWait=0] 最大延迟时间(非准确),`0` 表示禁用
  225. * @returns {Function} 消抖函数 `debounced`,可调用 `debounced.cancel()` 取消执行
  226. */
  227. debounce(fn, wait = 0, options = {}) {
  228. options = {
  229. leading: false,
  230. trailing: true,
  231. maxWait: 0,
  232. ...options,
  233. }
  234.  
  235. let tid = null
  236. let start = null
  237. let execute = null
  238. let callback = null
  239.  
  240. /** @this {*} thisArg */
  241. function debounced() {
  242. execute = () => {
  243. Reflect.apply(fn, this, arguments)
  244. execute = null
  245. }
  246. callback = () => {
  247. if (options.trailing) {
  248. execute?.()
  249. }
  250. tid = null
  251. start = null
  252. }
  253.  
  254. if (tid) {
  255. clearTimeout(tid)
  256. if (options.maxWait > 0 && Date.now() - start > options.maxWait) {
  257. callback()
  258. }
  259. }
  260.  
  261. if (!tid && options.leading) {
  262. execute?.()
  263. }
  264.  
  265. if (!start) {
  266. start = Date.now()
  267. }
  268.  
  269. tid = setTimeout(callback, wait)
  270. }
  271.  
  272. debounced.cancel = function() {
  273. if (tid) {
  274. clearTimeout(tid)
  275. tid = null
  276. start = null
  277. }
  278. }
  279.  
  280. return debounced
  281. }
  282.  
  283. /**
  284. * 生成节流函数
  285. * @param {Function} fn 目标函数
  286. * @param {number} [wait=0] 节流延迟(非准确)
  287. * @returns {Function} 节流函数 `throttled`,可调用 `throttled.cancel()` 取消执行
  288. */
  289. throttle(fn, wait = 0) {
  290. return this.debounce(fn, wait, {
  291. leading: true,
  292. trailing: true,
  293. maxWait: wait,
  294. })
  295. }
  296.  
  297. /**
  298. * 创建基础提醒对话框(异步)
  299. *
  300. * 若没有引入 `message` 模块,可使用 `api.message.alert()` 引用该方法。
  301. * @param {string} msg 信息
  302. */
  303. alert(msg) {
  304. const { label } = this.api.options
  305. return new Promise(resolve => {
  306. resolve(alert(`${label ? `${label}\n\n` : ''}${msg}`))
  307. })
  308. }
  309.  
  310. /**
  311. * 创建基础确认对话框(异步)
  312. *
  313. * 若没有引入 `message` 模块,可使用 `api.message.confirm()` 引用该方法。
  314. * @param {string} msg 信息
  315. * @returns {Promise<boolean>} 用户输入
  316. */
  317. confirm(msg) {
  318. const { label } = this.api.options
  319. return new Promise(resolve => {
  320. resolve(confirm(`${label ? `${label}\n\n` : ''}${msg}`))
  321. })
  322. }
  323.  
  324. /**
  325. * 创建基础输入对话框(异步)
  326. *
  327. * 若没有引入 `message` 模块,可使用 `api.message.prompt()` 引用该方法。
  328. * @param {string} msg 信息
  329. * @param {string} [val] 默认值
  330. * @returns {Promise<string>} 用户输入
  331. */
  332. prompt(msg, val) {
  333. const { label } = this.api.options
  334. return new Promise(resolve => {
  335. resolve(prompt(`${label ? `${label}\n\n` : ''}${msg}`, val))
  336. })
  337. }
  338. }(this)
  339.  
  340. /**
  341. * UserscriptAPILogger
  342. * @version 1.2.0.20210925
  343. */
  344. logger = new class UserscriptAPILogger {
  345. #logCss = `
  346. background-color: black;
  347. color: white;
  348. border-radius: 2px;
  349. padding: 2px;
  350. margin-right: 4px;
  351. `
  352.  
  353. /**
  354. * @param {UserscriptAPI} api `UserscriptAPI`
  355. */
  356. constructor(api) {
  357. this.api = api
  358. }
  359.  
  360. /**
  361. * 打印格式化日志
  362. * @param {'info' | 'warn' | 'error'} fn 日志函数名
  363. * @param {*[]} message 日志信息
  364. */
  365. #log(fn, ...message) {
  366. const output = console[fn]
  367. const label = this.api.options.label ?? ''
  368. const causes = []
  369. let template = null
  370. if (message.length > 0) {
  371. const types = []
  372. for (const [idx, m] of message.entries()) {
  373. if (m) {
  374. types.push(typeof m === 'string' ? '%s' : '%o')
  375. if (m instanceof Error && m.cause !== undefined) {
  376. causes.push(m.cause)
  377. }
  378. } else {
  379. if (m === undefined) {
  380. message[idx] = '[undefined]'
  381. } else if (m === null) {
  382. message[idx] = '[null]'
  383. } else if (m === '') {
  384. message[idx] = '[empty string]'
  385. }
  386. types.push(typeof message[idx] === 'string' ? '%s' : '%o')
  387. }
  388. }
  389. template = types.join(', ')
  390. } else {
  391. template = '[undefined]'
  392. }
  393. output(`%c${label}%c${template}`, this.#logCss, null, ...message)
  394. for (const [idx, cause] of causes.entries()) {
  395. output(`%c${label}%c${idx + 1}-th error is caused by %o`, this.#logCss, null, cause)
  396. }
  397. }
  398.  
  399. /**
  400. * 打印日志
  401. * @param {*[]} message 日志信息
  402. */
  403. info(...message) {
  404. this.#log('info', ...message)
  405. }
  406.  
  407. /**
  408. * 打印警告日志
  409. * @param {*[]} message 警告日志信息
  410. */
  411. warn(...message) {
  412. this.#log('warn', ...message)
  413. }
  414.  
  415. /**
  416. * 打印错误日志
  417. * @param {*[]} message 错误日志信息
  418. */
  419. error(...message) {
  420. this.#log('error', ...message)
  421. }
  422. }(this)
  423. }