Greasy Fork is available in English.

UserscriptAPI

My API for userscripts.

As of 22.09.2021. See ბოლო ვერსია.

ეს სკრიპტი არ უნდა იყოს პირდაპირ დაინსტალირებული. ეს ბიბლიოთეკაა, სხვა სკრიპტებისთვის უნდა ჩართეთ მეტა-დირექტივაში // @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. }