لا ينبغي أن لا يتم تثبيت هذا السكريت مباشرة. هو مكتبة لسكبتات لتشمل مع التوجيه الفوقية // @require https://update.greasyfork.org/scripts/432000/1095149/UserscriptAPIMessage.js
/**
* UserscriptAPIMessage
*
* 依赖于 `UserscriptAPI`,`UserscriptAPIDom`。
* @version 1.3.1.20220918
* @author Laster2800
* @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
*/
class UserscriptAPIMessage {
/**
* @param {UserscriptAPI} api `UserscriptAPI`
*/
constructor(api) {
this.api = api
api.initModuleStyle(`
.${api.options.id}-infobox {
z-index: 100000000;
background-color: #000000bf;
font-size: 16px;
max-width: 24em;
min-width: 2em;
color: white;
padding: 0.5em 1em;
border-radius: 9.6px;
opacity: 0;
transition: opacity ${api.options.fadeTime}ms ease-in-out;
pointer-events: none;
text-align: justify;
}
.${api.options.id}-infobox .hover-info {
display: flex;
align-items: center;
gap: 1em;
}
.${api.options.id}-dialog {
z-index: 90000000;
background-color: white;
font-size: 17px;
min-width: 18em;
max-width: 35em;
border-radius: 4px;
opacity: 0;
box-shadow: #000000aa 0px 3px 6px;
transition: opacity 150ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.${api.options.id}-dialog .gm-header {
padding: 0.5em 1em 0.4em;
border-bottom: 1px solid #d5d5d5;
}
.${api.options.id}-dialog .gm-body {
padding: 0.8em 1em;
}
.${api.options.id}-dialog .gm-bottom {
padding: 0 1em 0.6em;
text-align: right;
}
.${api.options.id}-dialog .gm-content {
line-height: 1.6em;
}
.${api.options.id}-dialog button.gm-interactive {
font-size: 0.9em;
padding: 0.1em 0.6em;
margin-left: 0.8em;
cursor: pointer;
background-color: white;
border: 1px solid #909090;
border-radius: 2px;
}
.${api.options.id}-dialog button.gm-interactive:hover,
.${api.options.id}-dialog button.gm-interactive:focus {
background-color: #ebebeb;
}
.${api.options.id}-dialog input.gm-interactive {
outline: none;
width: calc(100% - 12px);
margin-top: 0.6em;
padding: 4px 6px;
border: 1px solid #909090;
border-radius: 2px;
}
.${api.options.id}-dialog textarea.gm-interactive {
outline: none;
width: calc(100% - 2em);
margin: 0.6em 0 -0.4em;
padding: 1em;
resize: none;
border: 1px solid #909090;
border-radius: 2px;
}
.${api.options.id}-dialog textarea.gm-interactive::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: transparent;
}
.${api.options.id}-dialog textarea.gm-interactive::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: #0000002b;
}
.${api.options.id}-dialog textarea.gm-interactive::-webkit-scrollbar-corner {
background-color: transparent;
}
`)
}
/**
* @typedef infoOptions
* @property {(infobox: HTMLElement) => void} [onOpened] 信息打开后的回调
* @property {(infobox: HTMLElement) => void} [onClosed] 信息关闭后的回调
* @property {boolean} [autoClose=true] 是否自动关闭信息,配合 `ms` 使用
* @property {number} [ms=1500] 显示时间(单位:ms,不含渐显/渐隐时间)
* @property {boolean} [html=false] 是否将 `msg` 理解为 HTML
* @property {string} [width] 信息框的宽度;缺省时根据内容决定,但有最小宽度和最大宽度的限制,设为 `auto` 可解除限制
* @property {{top: string, left: string}} [position] 信息框的位置,必须带单位或以百分号结尾;不设置该项时,相当于设置为 `{ top: '80%', left: '50%' }`
*/
/**
* 创建信息
* @param {string} msg 信息
* @param {infoOptions | number} [options] 选项 / 显示时间(单位:ms,不含渐显/渐隐时间)
* @return {HTMLElement} 信息框元素
*/
info(msg, options) {
const { api } = this
if (typeof options === 'number') {
options = { ms: options }
}
options = {
autoClose: true,
ms: 1500,
position: { top: '85%' },
...options,
}
const infobox = document.createElement('div')
infobox.className = `${api.options.id}-infobox`
if (options.width) {
infobox.style.minWidth = 'auto'
infobox.style.maxWidth = 'none'
infobox.style.width = options.width
}
if (options.html) {
infobox.innerHTML = msg
} else {
infobox.textContent = msg
}
document.body.append(infobox)
api.dom.setPosition(infobox, options.position)
api.dom.fade(true, infobox, () => {
options.onOpened?.(infobox)
if (options.autoClose) {
setTimeout(() => {
this.close(infobox, options.onClosed)
}, options.ms)
}
})
return infobox
}
/**
* 创建悬浮信息
*
* 后续可通过启动元素上的 `hoverInfo` 属性修改悬浮信息设置,也可再次在启动元素上调用该方法修改。
* @param {HTMLElement} el 启动元素
* @param {string} msg 信息
* @param {string} [flag] 标志信息
* @param {Object} [options] 选项
* @param {string} [options.flagSize='1.8em'] 标志大小
* @param {string} [options.width] 信息框的宽度;缺省时根据内容决定,但有最小宽度和最大宽度的限制,设为 `auto` 可解除限制
* @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时,沿用 `api.message.infobox()` 的默认设置
* @param {() => boolean} [options.disabled] 用于获取是否禁用信息的函数
*/
hoverInfo(el, msg, flag, options) {
const created = el.hoverInfo
el.hoverInfo = { msg, flag, options: { flagSize: '1.8em', ...options } }
if (!created) {
/** @type {MutationObserver} */
let ob = null
el.addEventListener('mouseenter', () => {
const opt = el.hoverInfo
if (opt.options.disabled?.()) return
const htmlMsg = `
<div class="hover-info">
${opt.flag ? `<div style="font-size:${opt.options.flagSize};line-height:${opt.options.flagSize}">${opt.flag}</div>` : ''}
<div>${opt.msg}</div>
</div>
`
el.infobox = this.info(htmlMsg, { ...opt.options, html: true, autoClose: false })
// 避免 el 被移除后悬浮信息无法关闭
ob ??= new MutationObserver((records => {
for (const record of records) {
for (const node of record.removedNodes) {
if (node === el || node.contains(el)) {
this.close(el.infobox)
ob.disconnect()
ob = null
return
}
}
}
}))
ob.observe(document, { childList: true, subtree: true })
})
el.addEventListener('mouseleave', () => {
this.close(el.infobox)
ob?.disconnect()
})
}
}
/**
* @typedef DialogElement
* @property {0 | 1 | 2 | 3 | 4} state 状态(初始 | 开启中 | 打开 | 关闭中 | 关闭)
* @property {HTMLElement[]} interactives 交互元素
* @property {(callback?: () => void) => void} open 打开对话框
* @property {(callback?: () => void) => void} close 关闭对话框
*/
/**
* 创建对话框
* @param {string} msg 信息
* @param {Object} [options] 选项
* @param {boolean} [options.html] 信息是否为 HTML
* @param {string} [options.title=api.options.label] 标题
* @param {boolean} [options.titleHtml] 标题是否为 HTML
* @param {boolean} [options.lineInput] 是否添加单行输入框
* @param {boolean} [options.boxInput] 是否添加多行输入框
* @param {string[]} [options.buttons] 对话框按钮文本
* @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
* @param {{top: string, left: string}} [options.position] 信息框的位置,必须带单位或以百分号结尾;不设置该项时绝对居中
* @returns {HTMLElement & DialogElement} 对话框元素
*/
dialog(msg, options) {
const { api } = this
options = {
title: api.options.label,
position: {
top: '50%',
left: '50%',
},
...options,
}
const dialog = document.createElement('div')
dialog.className = `${api.options.id}-dialog`
if (options.width) {
dialog.style.minWidth = 'auto'
dialog.style.maxWidth = 'none'
dialog.style.width = options.width
}
let bottomHtml = ''
if (options.buttons) {
for (const button of options.buttons) {
bottomHtml += `<button class="gm-interactive">${button}</button>`
}
if (bottomHtml) {
bottomHtml = `<div class="gm-bottom">${bottomHtml}</div>`
}
}
dialog.innerHTML = `
${options.title ? '<div class="gm-header"></div>' : ''}
<div class="gm-body">
<div class="gm-content"></div>
${options.lineInput ? '<input type="text" class="gm-interactive">' : ''}
${options.boxInput ? '<textarea class="gm-interactive"></textarea>' : ''}
</div>
${bottomHtml}
`
if (options.title) {
const header = dialog.querySelector('.gm-header')
if (options.titleHtml) {
header.innerHTML = options.title
} else {
header.textContent = options.title
}
}
const content = dialog.querySelector('.gm-content')
if (options.html) {
content.innerHTML = msg
} else {
content.textContent = msg
}
dialog.interactives = dialog.querySelectorAll('.gm-interactive')
document.body.append(dialog)
dialog.state = 0
dialog.fadeOutNoInteractive = true
dialog.open = callback => {
dialog.state = 1
api.dom.setPosition(dialog, options.position)
api.dom.fade(true, dialog, () => {
dialog.state = 2
callback?.()
})
}
dialog.close = callback => {
dialog.state = 3
this.close(dialog, () => {
dialog.state = 4
callback?.()
})
}
return dialog
}
/**
* 创建提醒对话框
*
* 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
* @param {string} msg 信息
* @param {Object} [options] 选项
* @param {boolean} [options.primitive] 使用原生组件
* @param {boolean} [options.html] 信息是否为 HTML
* @param {string} [options.title=api.options.label] 标题
* @param {boolean} [options.titleHtml] 标题是否为 HTML
* @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
* @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
* @param {string[]} [options.btnText] 按钮文本
* @param {Object} [ret] 方法通过该对象将必要数据返回
* @returns {Promise<void>} 用户输入
*/
alert(msg, options, ret) {
return new Promise(resolve => {
let primitive = !document.body || options?.primitive
if (!primitive) {
try {
const btnText = []
btnText[0] = options?.btnText?.[0] ?? '确定'
const dialog = this.dialog(msg, {
...options,
buttons: btnText,
})
const confirm = dialog.interactives[0]
confirm.focus({ preventScroll: true })
confirm.addEventListener('click', () => {
dialog.close()
resolve()
})
dialog.open()
if (ret) {
ret.dialog = dialog
}
} catch { // not true error
primitive = true
}
}
if (primitive) {
const { label } = this.api.options
if (options?.html) {
const el = document.createElement('div')
el.innerHTML = msg
msg = el.textContent
}
resolve(alert(`${label ? `${label}\n\n` : ''}${msg}`))
}
})
}
/**
* 创建确认对话框
*
* 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
* @param {string} msg 信息
* @param {Object} [options] 选项
* @param {boolean} [options.primitive] 使用原生组件
* @param {boolean} [options.html] 信息是否为 HTML
* @param {string} [options.title=api.options.label] 标题
* @param {boolean} [options.titleHtml] 标题是否为 HTML
* @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
* @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
* @param {string[]} [options.btnText] 按钮文本
* @param {Object} [ret] 方法通过该对象将必要数据返回
* @returns {Promise<boolean>} 用户输入
*/
confirm(msg, options, ret) {
return new Promise(resolve => {
let primitive = !document.body || options?.primitive
if (!primitive) {
try {
const btnText = []
btnText[0] = options?.btnText?.[0] ?? '确定'
btnText[1] = options?.btnText?.[1] ?? '取消'
const dialog = this.dialog(msg, {
...options,
buttons: btnText,
})
const [confirm, cancel] = dialog.interactives
confirm.focus({ preventScroll: true })
confirm.addEventListener('click', () => {
dialog.close()
resolve(true)
})
cancel.addEventListener('click', () => {
dialog.close()
resolve(false)
})
dialog.open()
if (ret) {
ret.dialog = dialog
}
} catch { // not true error
primitive = true
}
}
if (primitive) {
const { label } = this.api.options
if (options?.html) {
const el = document.createElement('div')
el.innerHTML = msg
msg = el.textContent
}
resolve(confirm(`${label ? `${label}\n\n` : ''}${msg}`))
}
})
}
/**
* 创建输入对话框
*
* 没有引入 `message` 模块时,`UserscriptAPI` 会创建一个基础方法作为本方法的替代。
* @param {string} msg 信息
* @param {string} [val] 默认值
* @param {Object} [options] 选项
* @param {boolean} [options.primitive] 使用原生组件
* @param {boolean} [options.html] 信息是否为 HTML
* @param {string} [options.title=api.options.label] 标题
* @param {boolean} [options.titleHtml] 标题是否为 HTML
* @param {string} [options.width] 对话框宽度,不设置的情况下根据内容决定,但有最小宽度和最大宽度的限制
* @param {{top: string, left: string}} [options.position] 信息框的位置,不设置该项时绝对居中
* @param {string[]} [options.btnText] 按钮文本
* @param {Object} [ret] 方法通过该对象将必要数据返回
* @returns {Promise<string>} 用户输入
*/
prompt(msg, val, options, ret) {
return new Promise(resolve => {
let primitive = !document.body || options?.primitive
if (!primitive) {
try {
const btnText = []
btnText[0] = options?.btnText?.[0] ?? '确定'
btnText[1] = options?.btnText?.[1] ?? '取消'
const dialog = this.dialog(msg, {
...options,
buttons: btnText,
lineInput: true,
})
const [input, confirm, cancel] = dialog.interactives
if (val) {
input.value = val
input.setSelectionRange(0, input.value.length)
}
input.focus({ preventScroll: true })
input.addEventListener('keyup', e => {
if (e.key === 'Enter') {
confirm.dispatchEvent(new Event('click'))
}
})
confirm.addEventListener('click', () => {
dialog.close()
resolve(input.value)
})
cancel.addEventListener('click', () => {
dialog.close()
resolve(null)
})
dialog.open()
if (ret) {
ret.dialog = dialog
}
} catch { // not true error
primitive = true
}
}
if (primitive) {
const { label } = this.api.options
if (options?.html) {
const el = document.createElement('div')
el.innerHTML = msg
msg = el.textContent
}
resolve(prompt(`${label ? `${label}\n\n` : ''}${msg}`, val))
}
})
}
/**
* 关闭信息元素
* @param {HTMLElement} msgEl 信息元素
* @param {(msgEl: HTMLElement) => void} [callback] 信息关闭后的回调
*/
close(msgEl, callback) {
if (msgEl) {
this.api.dom.fade(false, msgEl, () => {
callback?.(msgEl)
msgEl?.remove()
})
}
}
}
/* global UserscriptAPI */
// eslint-disable-next-line no-lone-blocks
{ UserscriptAPI.registerModule('message', UserscriptAPIMessage) }