UserscriptAPIMessage

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

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

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/432000/968897/UserscriptAPIMessage.js

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

QingJ © 2025

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