UserscriptAPIMessage

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

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

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

  1. /**
  2. * UserscriptAPIMessage
  3. *
  4. * 依赖于 `UserscriptAPI`,`UserscriptAPIDom`。
  5. * @version 1.1.10.20210919
  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 {() => void} [onOpened] 信息打开后的回调
  111. * @property {() => 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.api
  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?.call(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 _self = this
  177. const created = el.hoverInfo
  178. el.hoverInfo = { msg, flag, options: { flagSize: '1.8em', ...options } }
  179. if (!created) {
  180. el.addEventListener('mouseenter', function() {
  181. const opt = this.hoverInfo
  182. if (opt.options.disabled?.()) return
  183. const htmlMsg = `
  184. <div class="hover-info">
  185. ${opt.flag ? `<div style="font-size:${opt.options.flagSize};line-height:${opt.options.flagSize}">${opt.flag}</div>` : ''}
  186. <div>${opt.msg}</div>
  187. </div>
  188. `
  189. this.infobox = _self.info(htmlMsg, { ...opt.options, html: true, autoClose: false })
  190. })
  191. el.addEventListener('mouseleave', function() {
  192. _self.close(this.infobox)
  193. })
  194. }
  195. }
  196.  
  197. /**
  198. * @typedef DialogElement
  199. * @property {HTMLElement[]} interactives 交互元素
  200. * @property {(callback?: () => void) => void} open 打开对话框
  201. * @property {(callback?: () => void) => void} close 关闭对话框
  202. */
  203. /**
  204. * 创建对话框
  205. * @param {string} msg 信息
  206. * @param {Object} [options] 选项
  207. * @param {boolean} [options.html] 信息是否为 HTML
  208. * @param {string} [options.title=api.options.label] 标题
  209. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  210. * @param {boolean} [options.lineInput] 是否添加单行输入框
  211. * @param {boolean} [options.boxInput] 是否添加多行输入框
  212. * @param {string[]} [options.buttons] 对话框按钮文本
  213. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  214. * @param {{top: string, left: string}} [options.position] 信息框的位置,必须带单位或以百分号结尾;不设置该项时绝对居中
  215. * @returns {HTMLElement & DialogElement} 对话框元素
  216. */
  217. dialog(msg, options) {
  218. const _self = this
  219. const api = _self.api
  220. options = {
  221. title: api.options.label,
  222. position: {
  223. top: '50%',
  224. left: '50%',
  225. },
  226. ...options,
  227. }
  228.  
  229. const dialog = document.createElement('div')
  230. dialog.className = `${api.options.id}-dialog`
  231. if (options.width) {
  232. dialog.style.minWidth = 'auto'
  233. dialog.style.maxWidth = 'none'
  234. dialog.style.width = options.width
  235. }
  236.  
  237. let bottomHtml = ''
  238. if (options.buttons) {
  239. for (const button of options.buttons) {
  240. bottomHtml += `<button class="gm-interactive">${button}</button>`
  241. }
  242. if (bottomHtml) {
  243. bottomHtml = `<div class="gm-bottom">${bottomHtml}</div>`
  244. }
  245. }
  246. dialog.innerHTML = `
  247. ${options.title ? '<div class="gm-header"></div>' : ''}
  248. <div class="gm-body">
  249. <div class="gm-content"></div>
  250. ${options.lineInput ? '<input type="text" class="gm-interactive">' : ''}
  251. ${options.boxInput ? '<textarea class="gm-interactive"></textarea>' : ''}
  252. </div>
  253. ${bottomHtml}
  254. `
  255. if (options.title) {
  256. const header = dialog.querySelector('.gm-header')
  257. if (options.titleHtml) {
  258. header.innerHTML = options.title
  259. } else {
  260. header.textContent = options.title
  261. }
  262. }
  263. const content = dialog.querySelector('.gm-content')
  264. if (options.html) {
  265. content.innerHTML = msg
  266. } else {
  267. content.textContent = msg
  268. }
  269. dialog.interactives = dialog.querySelectorAll('.gm-interactive')
  270. document.body.append(dialog)
  271.  
  272. dialog.fadeOutNoInteractive = true
  273. dialog.open = function(callback) {
  274. api.dom.setPosition(this, options.position)
  275. api.dom.fade(true, this, callback && (() => callback.call(this)))
  276. }
  277. dialog.close = function(callback) {
  278. _self.close(this, callback)
  279. }
  280. return dialog
  281. }
  282.  
  283. /**
  284. * 创建提醒对话框
  285. *
  286. * 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
  287. * @param {string} msg 信息
  288. * @param {Object} [options] 选项
  289. * @param {boolean} [options.primitive] 使用原生组件
  290. * @param {boolean} [options.html] 信息是否为 HTML
  291. * @param {string} [options.title=api.options.label] 标题
  292. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  293. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  294. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
  295. * @returns {Promise<void>} 用户输入
  296. */
  297. alert(msg, options) {
  298. const _self = this
  299. return new Promise(resolve => {
  300. let primitive = !document.body || options?.primitive
  301. if (!primitive) {
  302. try {
  303. const dialog = _self.dialog(msg, {
  304. ...options,
  305. buttons: ['确定'],
  306. })
  307. const confirm = dialog.interactives[0]
  308. confirm.focus({ preventScroll: true })
  309. confirm.addEventListener('click', function() {
  310. dialog.close()
  311. resolve()
  312. })
  313. dialog.open()
  314. } catch { // not true error
  315. primitive = true
  316. }
  317. }
  318. if (primitive) {
  319. const label = _self.api.options.label
  320. if (options?.html) {
  321. const el = document.createElement('div')
  322. el.innerHTML = msg
  323. msg = el.textContent
  324. }
  325. resolve(alert(`${label ? `${label}\n\n` : ''}${msg}`))
  326. }
  327. })
  328. }
  329.  
  330. /**
  331. * 创建确认对话框
  332. *
  333. * 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
  334. * @param {string} msg 信息
  335. * @param {Object} [options] 选项
  336. * @param {boolean} [options.primitive] 使用原生组件
  337. * @param {boolean} [options.html] 信息是否为 HTML
  338. * @param {string} [options.title=api.options.label] 标题
  339. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  340. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  341. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
  342. * @returns {Promise<boolean>} 用户输入
  343. */
  344. confirm(msg, options) {
  345. const _self = this
  346. return new Promise(resolve => {
  347. let primitive = !document.body || options?.primitive
  348. if (!primitive) {
  349. try {
  350. const dialog = _self.dialog(msg, {
  351. ...options,
  352. buttons: ['确定', '取消'],
  353. })
  354. const confirm = dialog.interactives[0]
  355. const cancel = dialog.interactives[1]
  356. confirm.focus({ preventScroll: true })
  357. confirm.addEventListener('click', function() {
  358. dialog.close()
  359. resolve(true)
  360. })
  361. cancel.addEventListener('click', function() {
  362. dialog.close()
  363. resolve(false)
  364. })
  365. dialog.open()
  366. } catch { // not true error
  367. primitive = true
  368. }
  369. }
  370. if (primitive) {
  371. const label = _self.api.options.label
  372. if (options?.html) {
  373. const el = document.createElement('div')
  374. el.innerHTML = msg
  375. msg = el.textContent
  376. }
  377. resolve(confirm(`${label ? `${label}\n\n` : ''}${msg}`))
  378. }
  379. })
  380. }
  381.  
  382. /**
  383. * 创建输入对话框
  384. *
  385. * 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
  386. * @param {string} msg 信息
  387. * @param {string} [val] 默认值
  388. * @param {Object} [options] 选项
  389. * @param {boolean} [options.primitive] 使用原生组件
  390. * @param {boolean} [options.html] 信息是否为 HTML
  391. * @param {string} [options.title=api.options.label] 标题
  392. * @param {boolean} [options.titleHtml] 标题是否为 HTML
  393. * @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
  394. * @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
  395. * @returns {Promise<string>} 用户输入
  396. */
  397. prompt(msg, val, options) {
  398. const _self = this
  399. return new Promise(resolve => {
  400. let primitive = !document.body || options?.primitive
  401. if (!primitive) {
  402. try {
  403. const dialog = _self.dialog(msg, {
  404. ...options,
  405. buttons: ['确定', '取消'],
  406. lineInput: true,
  407. })
  408. const input = dialog.interactives[0]
  409. const confirm = dialog.interactives[1]
  410. const cancel = dialog.interactives[2]
  411. if (val) {
  412. input.value = val
  413. input.setSelectionRange(0, input.value.length)
  414. }
  415. input.focus({ preventScroll: true })
  416. input.addEventListener('keyup', function(e) {
  417. if (e.key == 'Enter') {
  418. confirm.dispatchEvent(new Event('click'))
  419. }
  420. })
  421. confirm.addEventListener('click', function() {
  422. dialog.close()
  423. resolve(input.value)
  424. })
  425. cancel.addEventListener('click', function() {
  426. dialog.close()
  427. resolve(null)
  428. })
  429. dialog.open()
  430. } catch { // not true error
  431. primitive = true
  432. }
  433. }
  434. if (primitive) {
  435. const label = _self.api.options.label
  436. if (options?.html) {
  437. const el = document.createElement('div')
  438. el.innerHTML = msg
  439. msg = el.textContent
  440. }
  441. resolve(prompt(`${label ? `${label}\n\n` : ''}${msg}`, val))
  442. }
  443. })
  444. }
  445.  
  446. /**
  447. * 关闭信息元素
  448. * @param {HTMLElement} msgEl 信息元素
  449. * @param {() => void} [callback] 信息关闭后的回调
  450. */
  451. close(msgEl, callback) {
  452. if (msgEl) {
  453. this.api.dom.fade(false, msgEl, () => {
  454. callback?.call(msgEl)
  455. msgEl?.remove()
  456. })
  457. }
  458. }
  459. }
  460.  
  461. /* global UserscriptAPI */
  462. { UserscriptAPI.registerModule('message', UserscriptAPIMessage) }

QingJ © 2025

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