zoukankan      html  css  js  c++  java
  • 使用 div 标签 contenteditable="true" 实现一个 聊天框,支持 Ctrl + v 粘贴图片

    演示

    聊天输入框 组件

    src/components/ChatInput/index.vue

    <template>
      <div class="chat-input">
        <div class="left">
          <div
            id="editor"
            ref="editor"
            contenteditable="true"
            :class="editorClass"
            :style="editorStyle"
            @paste.prevent="handlePaste($event)"
            @keyup="handleKeyUp($event)"
            @keydown="handleKeyDown($event)"
          >
            <br>
          </div>
          <div><i v-show="loading" class="el-icon-loading" /></div>
        </div>
      </div>
    </template>
    
    <script>
    /**
     * 聊天输入框
     * events
     * change   function(value)
     * enter    function
     *
     * methods
     * clean    function
     * focus    function
     */
    import axios from 'axios'
    
    export default {
      name: 'ChatInput',
      props: {
        editorClass: {
          type: String,
          default: ''
        },
        editorStyle: {
          type: Object,
          default: () => ({})
        },
        imgShowWidth: { // 聊天输入框中粘贴的图片显示的宽度
          type: Number,
          default: 50
        },
        imgShowHeight: { // 聊天输入框中粘贴的图片显示的高度
          type: Number,
          default: 50
        },
        uploadUrl: {
          type: String,
          default: 'https://imio.jd.com/uploadfile/file/uploadImg.action'
        },
        name: { // 上传 表单 name
          type: String,
          default: 'upload'
        },
        enter: { // 是否支持回车, 目前还有个 bug 中文输入后,在结尾回车,需要回车两次
          type: Boolean,
          default: false
        }
      },
      data() {
        return {
          msgList: [],
          loading: false
        }
      },
      methods: {
        async handlePaste(event) {
          const pasteResult = this.handlePastePlainText(event.clipboardData)
          if (pasteResult) return
          await this.handlePasteImageFile(event.clipboardData)
        },
    
        handleKeyUp(event) {
          const childNodes = event.target.childNodes
          this.emitChange(childNodes)
          if (event.keyCode === 13) {
            this.$emit('enter')
          }
        },
    
        handleKeyDown(event) {
          if (event.keyCode === 13) { // 禁止换行默认行为
            event.preventDefault()
            if (this.enter) {
              const oBr = document.createElement('br')
              this.cursorInsert(oBr)
            }
          }
        },
    
        // 去格式粘贴 文本
        handlePastePlainText(clipboardData) {
          const text = clipboardData.getData('text/plain')
          if (text) {
            const textNode = document.createTextNode(text)
            this.cursorInsert(textNode)
            return true
          }
          return false
        },
    
        // 粘贴图片
        async handlePasteImageFile(clipboardData) {
          const img = this.getPasteImageFile(clipboardData.files)
          if (!img) return
          const uploadRes = await this.uploadChatImg(img)
          if (!uploadRes) {
            this.$message.error('图片上传失败,请重新上传')
            return
          }
          const oImage = await this.getImageObject(uploadRes, this.imgShowWidth, this.imgShowHeight)
          this.cursorInsert(oImage)
          // 光标处插入 image 后,重新出发 emit 时间
          const oEditor = this.$refs.editor
          this.emitChange(oEditor.childNodes)
        },
    
        emitChange(editorChildNodes) {
          const oldMsgList = JSON.parse(JSON.stringify(this.msgList))
          this.msgList = [] // 重置
          for (let i = 0; i < editorChildNodes.length; i++) {
            if (editorChildNodes[i].nodeType === 1 && editorChildNodes[i].nodeName === 'BR') { // 处理回车
              const lastMsg = this.msgList[this.msgList.length - 1]
              if (lastMsg?.type === 'text') {
                lastMsg.content += '
    '
              }
            } else if (editorChildNodes[i].nodeType === 3 && editorChildNodes[i].nodeValue) {
              const lastMsg = this.msgList[this.msgList.length - 1]
              if (lastMsg?.type === 'text') {
                lastMsg.content += editorChildNodes[i].nodeValue
              } else {
                this.msgList.push({
                  type: 'text',
                  content: editorChildNodes[i].nodeValue
                })
              }
            } else if (editorChildNodes[i].nodeType === 1 && editorChildNodes[i].nodeName === 'IMG') {
              const dataset = editorChildNodes[i].dataset
              this.msgList.push({
                type: 'image',
                url: editorChildNodes[i].src,
                 +dataset.width,
                height: +dataset.height
              })
            }
          }
          if (!this.msgList.length && !oldMsgList.length) {
            return
          }
          this.$emit('change', [...this.msgList])
        },
    
        // 光标处插入节点
        cursorInsert(node) {
          // 获取光标范围
          const selObj = window.getSelection()
          const range = selObj.getRangeAt(0)
          // 光标处插入节点
          range.insertNode(node)
          // 取消insert node 后的选中状态,将光标恢复到 insert node 后面
          range.collapse(false)
        },
    
        getPasteImageFile(clipboardDataFiles) {
          if (!clipboardDataFiles.length) {
            console.log('没有要粘贴的文件')
            return null
          }
          // 剪切版中选择的(用户第一个点的在尾)第一张图片
          const clipboardDataFileList = Array.from(clipboardDataFiles || [])
          let firstSelectedImage = null
          clipboardDataFileList.forEach(file => {
            if (!file.type.match(/image//i)) {
              return
            }
            firstSelectedImage = file
          })
          /**
           * 这里的 firstSelectedFile 对象就是和 <input type="file" /> onchange 时 一样的 文件对象
           * */
          return firstSelectedImage
        },
    
        /**
         * 上传聊天图片
         * @param file
         * @return {Promise<null|{ number, height: number, length: number, md5: string, path: string}>}
         */
        async uploadChatImg(file) {
          const formData = new FormData()
          formData.append(this.name, file)
          this.loading = true
          try {
            const resp = await axios.post(this.uploadUrl, formData)
            if (resp.status === 200 && resp.data.code === 0) {
              return resp.data
            }
            return null
          } catch (e) {
            return null
          } finally {
            this.loading = false
          }
        },
        // 获取一个 image object
        getImageObject(uploadRes, showWidth, showHeight) {
          const oImage = new Image(showWidth, showHeight)
          const datasetFields = ['width', 'height']
          datasetFields.forEach(field => {
            oImage.setAttribute(`data-${field}`, uploadRes[field])
          })
          oImage.src = uploadRes.path
          return oImage
        },
        // 清除 输入框
        clean() {
          this.$refs.editor.innerHTML = ''
        },
        // 输入框 焦点
        focus() {
          this.$refs.editor.focus()
        }
      }
    }
    </script>
    
    <style scoped lang="scss">
    .chat-input {
      display: flex;
      border: 1px solid #dcdfe6;
      .left {
         100%;
        div:nth-of-type(1) {
          padding: 4px;
           300px;
          min-height: 100px;
          outline: none;
        }
      }
    }
    </style>
    
    

    组件使用实例

    <template>
      <div>
        <chat-input
          ref="chat-input"
          :editor-style="{ '100%'}"
          @change="handleChatInputChange"
          @enter="handleChatSend"
        />
      </div>
    </template>
    
    <script>
    
    import ChatInput from '@/components/ChatInput'
    
    export default {
      name: 'UseDemo',
      components: { ChatInput },
      data() {
        return {
          chatInputValue: []
        }
      },
      mounted() {
        // setTimeout(() => {
        //   this.chatInputFocus()
        //   this.chatInputClean()
        // }, 3000)
      },
      methods: {
        // 获取焦点
        chatInputFocus() {
          this.$refs['chat-input'].focus()
        },
    
        // 清空内容
        chatInputClean() {
          this.$refs['chat-input'].clean()
        },
    
        handleChatInputChange(value) {
          console.log('输入框的值变化', value)
          this.chatInputValue = value ?? []
        },
    
        async handleChatSend() {
          console.log('您按了 Enter 键')
          // ... 可用来 调接口 发送数据到后台
        }
      }
    }
    </script>
    
    <style scoped>
    
    </style>
    
    

    参考

    Window.getSelection

    聊天输入框粘贴图片

    js实现ctrl+v粘贴上传图片(兼容chrome、firefox、ie11)

    求一个编辑框,可以输入文字,也可以粘贴图片

    contenteditable=”true” js 实现去格式粘贴

    利用Blob进行文件上传的完整步骤

  • 相关阅读:
    2028 ACM Lowest Common Multiple Plus
    2032 杨辉三角
    2011 ACM 0和1思想
    grid
    Change position in observation
    1490 ACM 数学
    1489 ACM 贪心
    2009 ACM 水题
    Book Lending Registration
    MR1和MR2(Yarn)工作原理流程
  • 原文地址:https://www.cnblogs.com/taohuaya/p/15308987.html
Copyright © 2011-2022 走看看