UserscriptAPIMessage

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

Version vom 09.09.2021. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/432000/968897/UserscriptAPIMessage.js

/**
 * UserscriptAPIMessage
 *
 * 依赖于 `UserscriptAPI`,`UserscriptAPIDom`。
 * @version 1.1.7.20210909
 * @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: grid;
        grid-auto-flow: column;
        column-gap: 1em;
        align-items: center;
      }

      .${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;
      }
    `)
  }

  /**
   * 创建信息
   * @param {string} msg 信息
   * @param {Object} [options] 选项
   * @param {() => void} [options.onOpened] 信息打开后的回调
   * @param {() => void} [options.onClosed] 信息关闭后的回调
   * @param {boolean} [options.autoClose=true] 是否自动关闭信息,配合 `options.ms` 使用
   * @param {number} [options.ms=1500] 显示时间(单位:ms,不含渐显/渐隐时间)
   * @param {boolean} [options.html=false] 是否将 `msg` 理解为 HTML
   * @param {string} [options.width] 信息框的宽度;缺省时根据内容决定,但有最小宽度和最大宽度的限制,设为 `auto` 可解除限制
   * @param {{top: string, left: string}} [options.position] 信息框的位置,必须带单位或以百分号结尾;不设置该项时,相当于设置为 `{ top: '80%', left: '50%' }`
   * @return {HTMLElement} 信息框元素
   */
  info(msg, options) {
    const api = this.api
    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.appendChild(infobox)
    api.dom.setPosition(infobox, options.position)

    api.dom.fade(true, infobox, () => {
      options.onOpened?.call(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 _self = this
    const created = el.hoverInfo
    el.hoverInfo = { msg, flag, options: { flagSize: '1.8em', ...options } }
    if (!created) {
      el.addEventListener('mouseenter', function() {
        const opt = this.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>
        `
        this.infobox = _self.info(htmlMsg, { ...opt.options, html: true, autoClose: false })
      })
      el.addEventListener('mouseleave', function() {
        _self.close(this.infobox)
      })
    }
  }

  /**
   * @typedef DialogElement
   * @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 _self = this
    const api = _self.api
    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.appendChild(dialog)

    dialog.fadeOutNoInteractive = true
    dialog.open = function(callback) {
      api.dom.setPosition(this, options.position)
      api.dom.fade(true, this, callback && (() => callback.call(this)))
    }
    dialog.close = function(callback) {
      _self.close(this, callback)
    }
    return dialog
  }

  /**
   * 创建提醒对话框
   * @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] 信息框的位置,不设置该项时绝对居中
   * @returns {Promise<void>} 用户输入
   */
  alert(msg, options) {
    const _self = this
    return new Promise(resolve => {
      let primitive = !document.body || options?.primitive
      if (!primitive) {
        try {
          const dialog = _self.dialog(msg, {
            ...options,
            buttons: ['确定'],
          })
          const confirm = dialog.interactives[0]
          confirm.focus({ preventScroll: true })
          confirm.addEventListener('click', function() {
            dialog.close()
            resolve()
          })
          dialog.open()
        } catch (e) { // not true error
          primitive = true
        }
      }
      if (primitive) {
        const label = _self.api.options.label
        if (options?.html) {
          const el = document.createElement('div')
          el.innerHTML = msg
          msg = el.textContent
        }
        resolve(alert(`${label ? `${label}\n\n` : ''}${msg}`))
      }
    })
  }

  /**
   * 创建确认对话框
   * @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] 信息框的位置,不设置该项时绝对居中
   * @returns {Promise<boolean>} 用户输入
   */
  confirm(msg, options) {
    const _self = this
    return new Promise(resolve => {
      let primitive = !document.body || options?.primitive
      if (!primitive) {
        try {
          const dialog = _self.dialog(msg, {
            ...options,
            buttons: ['确定', '取消'],
          })
          const confirm = dialog.interactives[0]
          const cancel = dialog.interactives[1]
          confirm.focus({ preventScroll: true })
          confirm.addEventListener('click', function() {
            dialog.close()
            resolve(true)
          })
          cancel.addEventListener('click', function() {
            dialog.close()
            resolve(false)
          })
          dialog.open()
        } catch (e) { // not true error
          primitive = true
        }
      }
      if (primitive) {
        const label = _self.api.options.label
        if (options?.html) {
          const el = document.createElement('div')
          el.innerHTML = msg
          msg = el.textContent
        }
        resolve(confirm(`${label ? `${label}\n\n` : ''}${msg}`))
      }
    })
  }

  /**
   * 创建输入对话框
   * @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] 信息框的位置,不设置该项时绝对居中
   * @returns {Promise<string>} 用户输入
   */
  prompt(msg, val, options) {
    const _self = this
    return new Promise(resolve => {
      let primitive = !document.body || options?.primitive
      if (!primitive) {
        try {
          const dialog = _self.dialog(msg, {
            ...options,
            buttons: ['确定', '取消'],
            lineInput: true,
          })
          const input = dialog.interactives[0]
          const confirm = dialog.interactives[1]
          const cancel = dialog.interactives[2]
          if (val) {
            input.value = val
            input.setSelectionRange(0, input.value.length)
          }
          input.focus({ preventScroll: true })
          input.addEventListener('keyup', function(e) {
            if (e.key == 'Enter') {
              confirm.dispatchEvent(new Event('click'))
            }
          })
          confirm.addEventListener('click', function() {
            dialog.close()
            resolve(input.value)
          })
          cancel.addEventListener('click', function() {
            dialog.close()
            resolve(null)
          })
          dialog.open()
        } catch (e) { // not true error
          primitive = true
        }
      }
      if (primitive) {
        const label = _self.api.options.label
        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 {() => void} [callback] 信息关闭后的回调
   */
  close(msgEl, callback) {
    if (msgEl) {
      this.api.dom.fade(false, msgEl, () => {
        callback?.call(msgEl)
        msgEl?.remove()
      })
    }
  }
}

/* global UserscriptAPI */
{ UserscriptAPI.registerModule('message', UserscriptAPIMessage) }