UserscriptAPIMessage

https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/432000/1095149/UserscriptAPIMessage.js

  1. /**
  2. * UserscriptAPIMessage
  3. *
  4. * 依赖于 `UserscriptAPI`,`UserscriptAPIDom`。
  5. * @version 1.3.1.20220918
  6. * @author Laster2800
  7. * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
  8. */
  9. class UserscriptAPIMessage {
  10. /**
  11. * @param {UserscriptAPI} api `UserscriptAPI`
  12. */
  13. constructor(api) {
  14. this.api = api
  15.  
  16. api.initModuleStyle(`
  17. .${api.options.id}-infobox {
  18. z-index: 100000000;
  19. background-color: #000000bf;
  20. font-size: 16px;
  21. max-width: 24em;
  22. min-width: 2em;
  23. color: white;
  24. padding: 0.5em 1em;
  25. border-radius: 9.6px;
  26. opacity: 0;
  27. transition: opacity ${api.options.fadeTime}ms ease-in-out;
  28. pointer-events: none;
  29. text-align: justify;
  30. }
  31.  
  32. .${api.options.id}-infobox .hover-info {
  33. display: flex;
  34. align-items: center;
  35. gap: 1em;
  36. }
  37.  
  38. .${api.options.id}-dialog {
  39. z-index: 90000000;
  40. background-color: white;
  41. font-size: 17px;
  42. min-width: 18em;
  43. max-width: 35em;
  44. border-radius: 4px;
  45. opacity: 0;
  46. box-shadow: #000000aa 0px 3px 6px;
  47. transition: opacity 150ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
  48. }
  49. .${api.options.id}-dialog .gm-header {
  50. padding: 0.5em 1em 0.4em;
  51. border-bottom: 1px solid #d5d5d5;
  52. }
  53. .${api.options.id}-dialog .gm-body {
  54. padding: 0.8em 1em;
  55. }
  56. .${api.options.id}-dialog .gm-bottom {
  57. padding: 0 1em 0.6em;
  58. text-align: right;
  59. }
  60. .${api.options.id}-dialog .gm-content {
  61. line-height: 1.6em;
  62. }
  63. .${api.options.id}-dialog button.gm-interactive {
  64. font-size: 0.9em;
  65. padding: 0.1em 0.6em;
  66. margin-left: 0.8em;
  67. cursor: pointer;
  68. background-color: white;
  69. border: 1px solid #909090;
  70. border-radius: 2px;
  71. }
  72. .${api.options.id}-dialog button.gm-interactive:hover,
  73. .${api.options.id}-dialog button.gm-interactive:focus {
  74. background-color: #ebebeb;
  75. }
  76. .${api.options.id}-dialog input.gm-interactive {
  77. outline: none;
  78. width: calc(100% - 12px);
  79. margin-top: 0.6em;
  80. padding: 4px 6px;
  81. border: 1px solid #909090;
  82. border-radius: 2px;
  83. }
  84. .${api.options.id}-dialog textarea.gm-interactive {
  85. outline: none;
  86. width: calc(100% - 2em);
  87. margin: 0.6em 0 -0.4em;
  88. padding: 1em;
  89. resize: none;
  90. border: 1px solid #909090;
  91. border-radius: 2px;
  92. }
  93. .${api.options.id}-dialog textarea.gm-interactive::-webkit-scrollbar {
  94. width: 6px;
  95. height: 6px;
  96. background-color: transparent;
  97. }
  98. .${api.options.id}-dialog textarea.gm-interactive::-webkit-scrollbar-thumb {
  99. border-radius: 3px;
  100. background-color: #0000002b;
  101. }
  102. .${api.options.id}-dialog textarea.gm-interactive::-webkit-scrollbar-corner {
  103. background-color: transparent;
  104. }
  105. `)
  106. }
  107.  
  108. /**
  109. * @typedef infoOptions
  110. * @property {(infobox: HTMLElement) => void} [onOpened] 信息打开后的回调
  111. * @property {(infobox: HTMLElement) => void} [onClosed] 信息关闭后的回调
  112. * @property {boolean} [autoClose=true] 是否自动关闭信息,配合 `ms` 使用
  113. * @property {number} [ms=1500] 显示时间(单位:ms,不含渐显/渐隐时间)
  114. * @property {boolean} [html=false] 是否将 `msg` 理解为 HTML
  115. * @property {string} [width] 信息框的宽度;缺省时根据内容决定,但有最小宽度和最大宽度的限制,设为 `auto` 可解除限制
  116. * @property {{top: string, left: string}} [position] 信息框的位置,必须带单位或以百分号结尾;不设置该项时,相当于设置为 `{ top: '80%', left: '50%' }`
  117. */
  118. /**
  119. * 创建信息
  120. * @param {string} msg 信息
  121. * @param {infoOptions | number} [options] 选项 / 显示时间(单位:ms,不含渐显/渐隐时间)
  122. * @return {HTMLElement} 信息框元素
  123. */
  124. info(msg, options) {
  125. const { api } = this
  126. if (typeof options === 'number') {
  127. options = { ms: options }
  128. }
  129. options = {
  130. autoClose: true,
  131. ms: 1500,
  132. position: { top: '85%' },
  133. ...options,
  134. }
  135.  
  136. const infobox = document.createElement('div')
  137. infobox.className = `${api.options.id}-infobox`
  138. if (options.width) {
  139. infobox.style.minWidth = 'auto'
  140. infobox.style.maxWidth = 'none'
  141. infobox.style.width = options.width
  142. }
  143. if (options.html) {
  144. infobox.innerHTML = msg
  145. } else {
  146. infobox.textContent = msg
  147. }
  148. document.body.append(infobox)
  149. api.dom.setPosition(infobox, options.position)
  150.  
  151. api.dom.fade(true, infobox, () => {
  152. options.onOpened?.(infobox)
  153. if (options.autoClose) {
  154. setTimeout(() => {
  155. this.close(infobox, options.onClosed)
  156. }, options.ms)
  157. }
  158. })
  159. return infobox
  160. }
  161.  
  162. /**
  163. * 创建悬浮信息
  164. *
  165. * 后续可通过启动元素上的 `hoverInfo` 属性修改悬浮信息设置,也可再次在启动元素上调用该方法修改。
  166. * @param {HTMLElement} el 启动元素
  167. * @param {string} msg 信息
  168. * @param {string} [flag] 标志信息
  169. * @param {Object} [options] 选项
  170. * @param {string} [options.flagSize='1.8em'] 标志大小
  171. * @param {string} [options.width] 信息框的宽度;缺省时根据内容决定,但有最小宽度和最大宽度的限制,设为 `auto` 可解除限制
  172. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时,沿用 `api.message.infobox()` 的默认设置
  173. * @param {() => boolean} [options.disabled] 用于获取是否禁用信息的函数
  174. */
  175. hoverInfo(el, msg, flag, options) {
  176. const created = el.hoverInfo
  177. el.hoverInfo = { msg, flag, options: { flagSize: '1.8em', ...options } }
  178. if (!created) {
  179. /** @type {MutationObserver} */
  180. let ob = null
  181. el.addEventListener('mouseenter', () => {
  182. const opt = el.hoverInfo
  183. if (opt.options.disabled?.()) return
  184. const htmlMsg = `
  185. <div class="hover-info">
  186. ${opt.flag ? `<div style="font-size:${opt.options.flagSize};line-height:${opt.options.flagSize}">${opt.flag}</div>` : ''}
  187. <div>${opt.msg}</div>
  188. </div>
  189. `
  190. el.infobox = this.info(htmlMsg, { ...opt.options, html: true, autoClose: false })
  191. // 避免 el 被移除后悬浮信息无法关闭
  192. ob ??= new MutationObserver((records => {
  193. for (const record of records) {
  194. for (const node of record.removedNodes) {
  195. if (node === el || node.contains(el)) {
  196. this.close(el.infobox)
  197. ob.disconnect()
  198. ob = null
  199. return
  200. }
  201. }
  202. }
  203. }))
  204. ob.observe(document, { childList: true, subtree: true })
  205. })
  206. el.addEventListener('mouseleave', () => {
  207. this.close(el.infobox)
  208. ob?.disconnect()
  209. })
  210. }
  211. }
  212.  
  213. /**
  214. * @typedef DialogElement
  215. * @property {0 | 1 | 2 | 3 | 4} state 状态(初始 | 开启中 | 打开 | 关闭中 | 关闭)
  216. * @property {HTMLElement[]} interactives 交互元素
  217. * @property {(callback?: () => void) => void} open 打开对话框
  218. * @property {(callback?: () => void) => void} close 关闭对话框
  219. */
  220. /**
  221. * 创建对话框
  222. * @param {string} msg 信息
  223. * @param {Object} [options] 选项
  224. * @param {boolean} [options.html] 信息是否为 HTML
  225. * @param {string} [options.title=api.options.label] 标题
  226. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  227. * @param {boolean} [options.lineInput] 是否添加单行输入框
  228. * @param {boolean} [options.boxInput] 是否添加多行输入框
  229. * @param {string[]} [options.buttons] 对话框按钮文本
  230. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  231. * @param {{top: string, left: string}} [options.position] 信息框的位置,必须带单位或以百分号结尾;不设置该项时绝对居中
  232. * @returns {HTMLElement & DialogElement} 对话框元素
  233. */
  234. dialog(msg, options) {
  235. const { api } = this
  236. options = {
  237. title: api.options.label,
  238. position: {
  239. top: '50%',
  240. left: '50%',
  241. },
  242. ...options,
  243. }
  244.  
  245. const dialog = document.createElement('div')
  246. dialog.className = `${api.options.id}-dialog`
  247. if (options.width) {
  248. dialog.style.minWidth = 'auto'
  249. dialog.style.maxWidth = 'none'
  250. dialog.style.width = options.width
  251. }
  252.  
  253. let bottomHtml = ''
  254. if (options.buttons) {
  255. for (const button of options.buttons) {
  256. bottomHtml += `<button class="gm-interactive">${button}</button>`
  257. }
  258. if (bottomHtml) {
  259. bottomHtml = `<div class="gm-bottom">${bottomHtml}</div>`
  260. }
  261. }
  262. dialog.innerHTML = `
  263. ${options.title ? '<div class="gm-header"></div>' : ''}
  264. <div class="gm-body">
  265. <div class="gm-content"></div>
  266. ${options.lineInput ? '<input type="text" class="gm-interactive">' : ''}
  267. ${options.boxInput ? '<textarea class="gm-interactive"></textarea>' : ''}
  268. </div>
  269. ${bottomHtml}
  270. `
  271. if (options.title) {
  272. const header = dialog.querySelector('.gm-header')
  273. if (options.titleHtml) {
  274. header.innerHTML = options.title
  275. } else {
  276. header.textContent = options.title
  277. }
  278. }
  279. const content = dialog.querySelector('.gm-content')
  280. if (options.html) {
  281. content.innerHTML = msg
  282. } else {
  283. content.textContent = msg
  284. }
  285. dialog.interactives = dialog.querySelectorAll('.gm-interactive')
  286. document.body.append(dialog)
  287.  
  288. dialog.state = 0
  289. dialog.fadeOutNoInteractive = true
  290. dialog.open = callback => {
  291. dialog.state = 1
  292. api.dom.setPosition(dialog, options.position)
  293. api.dom.fade(true, dialog, () => {
  294. dialog.state = 2
  295. callback?.()
  296. })
  297. }
  298. dialog.close = callback => {
  299. dialog.state = 3
  300. this.close(dialog, () => {
  301. dialog.state = 4
  302. callback?.()
  303. })
  304. }
  305. return dialog
  306. }
  307.  
  308. /**
  309. * 创建提醒对话框
  310. *
  311. * 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
  312. * @param {string} msg 信息
  313. * @param {Object} [options] 选项
  314. * @param {boolean} [options.primitive] 使用原生组件
  315. * @param {boolean} [options.html] 信息是否为 HTML
  316. * @param {string} [options.title=api.options.label] 标题
  317. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  318. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  319. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
  320. * @param {string[]} [options.btnText] 按钮文本
  321. * @param {Object} [ret] 方法通过该对象将必要数据返回
  322. * @returns {Promise<void>} 用户输入
  323. */
  324. alert(msg, options, ret) {
  325. return new Promise(resolve => {
  326. let primitive = !document.body || options?.primitive
  327. if (!primitive) {
  328. try {
  329. const btnText = []
  330. btnText[0] = options?.btnText?.[0] ?? '确定'
  331. const dialog = this.dialog(msg, {
  332. ...options,
  333. buttons: btnText,
  334. })
  335. const confirm = dialog.interactives[0]
  336. confirm.focus({ preventScroll: true })
  337. confirm.addEventListener('click', () => {
  338. dialog.close()
  339. resolve()
  340. })
  341. dialog.open()
  342. if (ret) {
  343. ret.dialog = dialog
  344. }
  345. } catch { // not true error
  346. primitive = true
  347. }
  348. }
  349. if (primitive) {
  350. const { label } = this.api.options
  351. if (options?.html) {
  352. const el = document.createElement('div')
  353. el.innerHTML = msg
  354. msg = el.textContent
  355. }
  356. resolve(alert(`${label ? `${label}\n\n` : ''}${msg}`))
  357. }
  358. })
  359. }
  360.  
  361. /**
  362. * 创建确认对话框
  363. *
  364. * 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
  365. * @param {string} msg 信息
  366. * @param {Object} [options] 选项
  367. * @param {boolean} [options.primitive] 使用原生组件
  368. * @param {boolean} [options.html] 信息是否为 HTML
  369. * @param {string} [options.title=api.options.label] 标题
  370. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  371. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  372. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
  373. * @param {string[]} [options.btnText] 按钮文本
  374. * @param {Object} [ret] 方法通过该对象将必要数据返回
  375. * @returns {Promise<boolean>} 用户输入
  376. */
  377. confirm(msg, options, ret) {
  378. return new Promise(resolve => {
  379. let primitive = !document.body || options?.primitive
  380. if (!primitive) {
  381. try {
  382. const btnText = []
  383. btnText[0] = options?.btnText?.[0] ?? '确定'
  384. btnText[1] = options?.btnText?.[1] ?? '取消'
  385. const dialog = this.dialog(msg, {
  386. ...options,
  387. buttons: btnText,
  388. })
  389. const [confirm, cancel] = dialog.interactives
  390. confirm.focus({ preventScroll: true })
  391. confirm.addEventListener('click', () => {
  392. dialog.close()
  393. resolve(true)
  394. })
  395. cancel.addEventListener('click', () => {
  396. dialog.close()
  397. resolve(false)
  398. })
  399. dialog.open()
  400. if (ret) {
  401. ret.dialog = dialog
  402. }
  403. } catch { // not true error
  404. primitive = true
  405. }
  406. }
  407. if (primitive) {
  408. const { label } = this.api.options
  409. if (options?.html) {
  410. const el = document.createElement('div')
  411. el.innerHTML = msg
  412. msg = el.textContent
  413. }
  414. resolve(confirm(`${label ? `${label}\n\n` : ''}${msg}`))
  415. }
  416. })
  417. }
  418.  
  419. /**
  420. * 创建输入对话框
  421. *
  422. * 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
  423. * @param {string} msg 信息
  424. * @param {string} [val] 默认值
  425. * @param {Object} [options] 选项
  426. * @param {boolean} [options.primitive] 使用原生组件
  427. * @param {boolean} [options.html] 信息是否为 HTML
  428. * @param {string} [options.title=api.options.label] 标题
  429. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  430. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  431. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
  432. * @param {string[]} [options.btnText] 按钮文本
  433. * @param {Object} [ret] 方法通过该对象将必要数据返回
  434. * @returns {Promise<string>} 用户输入
  435. */
  436. prompt(msg, val, options, ret) {
  437. return new Promise(resolve => {
  438. let primitive = !document.body || options?.primitive
  439. if (!primitive) {
  440. try {
  441. const btnText = []
  442. btnText[0] = options?.btnText?.[0] ?? '确定'
  443. btnText[1] = options?.btnText?.[1] ?? '取消'
  444. const dialog = this.dialog(msg, {
  445. ...options,
  446. buttons: btnText,
  447. lineInput: true,
  448. })
  449. const [input, confirm, cancel] = dialog.interactives
  450. if (val) {
  451. input.value = val
  452. input.setSelectionRange(0, input.value.length)
  453. }
  454. input.focus({ preventScroll: true })
  455. input.addEventListener('keyup', e => {
  456. if (e.key === 'Enter') {
  457. confirm.dispatchEvent(new Event('click'))
  458. }
  459. })
  460. confirm.addEventListener('click', () => {
  461. dialog.close()
  462. resolve(input.value)
  463. })
  464. cancel.addEventListener('click', () => {
  465. dialog.close()
  466. resolve(null)
  467. })
  468. dialog.open()
  469. if (ret) {
  470. ret.dialog = dialog
  471. }
  472. } catch { // not true error
  473. primitive = true
  474. }
  475. }
  476. if (primitive) {
  477. const { label } = this.api.options
  478. if (options?.html) {
  479. const el = document.createElement('div')
  480. el.innerHTML = msg
  481. msg = el.textContent
  482. }
  483. resolve(prompt(`${label ? `${label}\n\n` : ''}${msg}`, val))
  484. }
  485. })
  486. }
  487.  
  488. /**
  489. * 关闭信息元素
  490. * @param {HTMLElement} msgEl 信息元素
  491. * @param {(msgEl: HTMLElement) => void} [callback] 信息关闭后的回调
  492. */
  493. close(msgEl, callback) {
  494. if (msgEl) {
  495. this.api.dom.fade(false, msgEl, () => {
  496. callback?.(msgEl)
  497. msgEl?.remove()
  498. })
  499. }
  500. }
  501. }
  502.  
  503. /* global UserscriptAPI */
  504. // eslint-disable-next-line no-lone-blocks
  505. { UserscriptAPI.registerModule('message', UserscriptAPIMessage) }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址