UserscriptAPI

My API for userscripts.

Verze ze dne 22. 09. 2021. Zobrazit nejnovější verzi.

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/409641/972855/UserscriptAPI.js

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