Web聊天工具的富文本输入框

近来折腾 Websocket,盘算开辟一个聊天室运用练练手。在运用开辟的过程当中发明可以插进去 emoji ,粘贴图片的富文本输入框实在蕴含着许多风趣的学问,因而便盘算纪录下来和人人分享。

堆栈地点:chat-input-box
预览地点:https://codepen.io/jrainlau/p…

起首来看看 demo 效果:

《Web聊天工具的富文本输入框》

是否是觉得很奇异?接下来我会一步步解说这内里的功用都是怎样完成的。

输入框富文本化

传统的输入框都是运用 <textarea> 来制造的,它的上风是异常简朴,但最大的缺点倒是没法展现图片。为了可以让输入框可以展现图片(富文本化),我们可以采纳设置了 contenteditable="true" 属性的 <div> 来完成这内里的功用。

简朴建立一个 index.html 文件,然后写入以下内容:

<div class="editor" contenteditable="true">
  <img src="https://static.easyicon.net/preview/121/1214124.gif" alt="">
</div>

翻开浏览器,就可以看到一个默许已带了一张图片的输入框:

《Web聊天工具的富文本输入框》

光标可以在图片前后挪动,同时也可以输入内容,以至经由过程退格键删除这张图片——换句话说,图片也是可编辑内容的一部份,也意味着输入框的富文本化已体现出来了。

接下来的使命,就是思索怎样直接经由过程 control + v 把图片粘贴进去了。

处置惩罚粘贴事宜

任何经由过程“复制”或许 control + c 所复制的内容(包含屏幕截图)都邑储存在剪贴板,在粘贴的时刻可以在输入框的 onpaste 事宜内里监听到。

document.querySelector('.editor').addEventListener('paste', (e) => {
    console.log(e.clipboardData.items)
})

而剪贴板的的内容则寄存在 DataTransferItemList 对象中,可以经由过程 e.clipboardData.items 接见到:

《Web聊天工具的富文本输入框》

仔细的读者会发明,假如直接在控制台点开 DataTransferItemList 前的小箭头,会发明对象的 length 属性为0。说好的剪贴板内容呢?实在这是 Chrome 调试的一个小坑。在开辟者东西内里,console.log 出来的对象是一个援用,会跟着原始数据的转变而转变。因为剪贴板的数据已被“粘贴”进输入框了,所以展开小箭头今后看到的 DataTransferItemList 就变成空的了。为此,我们可以改用 console.table 来展现及时的效果。

《Web聊天工具的富文本输入框》

在邃晓了剪贴板数据的寄存位置今后,就可以编写代码来处置惩罚它们了。因为我们的富文本输入框比较简朴,所以只需要处置惩罚两类数据即可,其一是一般的文本范例数据,包含 emoji 脸色;其二则是图片范例数据。

新建 paste.js 文件:

const onPaste = (e) => {
  // 假如剪贴板没有数据则直接返回
  if (!(e.clipboardData && e.clipboardData.items)) {
    return
  }
  // 用Promise封装便于未来运用
  return new Promise((resolve, reject) => {
    // 复制的内容在剪贴板里位置不确定,所以经由过程遍向来保证数据准确
    for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {
      const item = e.clipboardData.items[i]
      // 文本花样内容处置惩罚
      if (item.kind === 'string') {
        item.getAsString((str) => {
          resolve(str)
        })
      // 图片花样内容处置惩罚
      } else if (item.kind === 'file') {
        const pasteFile = item.getAsFile()
        // 处置惩罚pasteFile
        // TODO(pasteFile)
      } else {
        reject(new Error('Not allow to paste this type!'))
      }
    }
  })
}

export default onPaste

然后就可以在 onPaste 事宜内里直接运用了:

document.querySelector('.editor').addEventListener('paste', async (e) => {
    const result = await onPaste(e)
    console.log(result)
})

上面的代码支撑文本花样,接下来就要对图片花样举行处置惩罚了。玩过 <input type="file"> 的同学会晓得,包含图片在内的一切文件花样内容都邑储存在 File 对象内里,这在剪贴板内里也是一样的。因而我们可以编写一套通用的函数,特地来读取 File 对象里的图片内容,并把它转化成 base64 字符串。

粘贴图片

为了更好地在输入框里展现图片,必需限定图片的大小,所以这个图片处置惩罚函数不仅可以读取 File 对象内里的图片,还可以对其举行紧缩。

新建一个 chooseImg.js 文件:

/**
 * 预览函数
 *
 * @param {*} dataUrl base64字符串
 * @param {*} cb 回调函数
 */
function toPreviewer (dataUrl, cb) {
  cb && cb(dataUrl)
}

/**
 * 图片紧缩函数
 *
 * @param {*} img 图片对象
 * @param {*} fileType  图片范例
 * @param {*} maxWidth 图片最大宽度
 * @returns base64字符串
 */
function compress (img, fileType, maxWidth) {
  let canvas = document.createElement('canvas')
  let ctx = canvas.getContext('2d')

  const proportion = img.width / img.height
  const width = maxWidth
  const height = maxWidth / proportion

  canvas.width = width
  canvas.height = height

  ctx.fillStyle = '#fff'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  ctx.drawImage(img, 0, 0, width, height)

  const base64data = canvas.toDataURL(fileType, 0.75)
  canvas = ctx = null

  return base64data
}

/**
 * 挑选图片函数
 *
 * @param {*} e input.onchange事宜对象
 * @param {*} cb 回调函数
 * @param {number} [maxsize=200 * 1024] 图片最大体积
 */
function chooseImg (e, cb, maxsize = 200 * 1024) {
  const file = e.target.files[0]

  if (!file || !/\/(?:jpeg|jpg|png)/i.test(file.type)) {
    return
  }

  const reader = new FileReader()
  reader.onload = function () {
    const result = this.result
    let img = new Image()

    if (result.length <= maxsize) {
      toPreviewer(result, cb)
      return
    }

    img.onload = function () {
      const compressedDataUrl = compress(img, file.type, maxsize / 1024)
      toPreviewer(compressedDataUrl, cb)
      img = null
    }

    img.src = result
  }

  reader.readAsDataURL(file)
}

export default chooseImg

关于运用
canvas 紧缩图片和运用
FileReader 读取文件的内容在这里就不赘述了,感兴趣的读者可以自行查阅。

回到上一步的 paste.js 函数,把个中的 TODO() 改写成 chooseImg() 即可:

const imgEvent = {
  target: {
    files: [pasteFile]
  }
}
chooseImg(imgEvent, (url) => {
  resolve(url)
})

回到浏览器,假如我们复制一张图片并在输入框中实行粘贴的行动,将可以在控制台看到打印出了以 data:image/png;base64 开首的图片地点。

输入框中插进去内容

经由前面两个步骤,我们后已可以读取剪贴板中的文本内容和图片内容了,接下来就是把它们准确的插进去输入框的光标位置当中。

关于插进去内容,我们可以直接经由过程 document.execCommand 要领举行。关于这个要领细致用法可以在MDN文档内里找到,在这里我们只需要运用 insertTextinsertImage 即可。

document.querySelector('.editor').addEventListener('paste', async (e) => {
    const result = await onPaste(e)
    const imgRegx = /^data:image\/png;base64,/
    const command = imgRegx.test(result) ? 'insertImage': 'insertText'
    
    document.execCommand(command, false, result)
})

但是在某些版本的 Chrome 浏览器下,insertImage 要领可能会失效,这时刻便可以采纳别的一种要领,应用 Selection 来完成。而以后挑选并插进去 emoji 的操纵也会用到它,因而无妨先来相识一下。

当我们在代码中挪用 window.getSelection() 后会取得一个 Selection 对象。假如在页面当选中一些笔墨,然后在控制台实行 window.getSelection().toString(),就会看到输出是你所挑选的那部份笔墨。

与这部份地区笔墨相对应的,是一个 range 对象,运用 window.getSelection().getRangeAt(0) 即可以接见它。range 不仅包含了选中地区笔墨的内容,还包含了地区的出发点位置 startOffset 和尽头位置 endOffset

我们也可以经由过程 document.createRange() 的方法手动建立一个 range,往它内里写入内容并展现在输入框中。

关于插进去图片来讲,要先从 window.getSelection() 猎取 range ,然后往内里插进去图片。

document.querySelector('.editor').addEventListener('paste', async (e) => {
  // 读取剪贴板的内容
  const result = await onPaste(e)
  const imgRegx = /^data:image\/png;base64,/
  // 假如是图片花样(base64),则经由过程组织range的方法把<img>标签插进去准确的位置
  // 假如是文本花样,则经由过程document.execCommand('insertText')要领把文本插进去
  if (imgRegx.test(result)) {
    const sel = window.getSelection()
    if (sel && sel.rangeCount === 1 && sel.isCollapsed) {
      const range = sel.getRangeAt(0)
      const img = new Image()
      img.src = result
      range.insertNode(img)
      range.collapse(false)
      sel.removeAllRanges()
      sel.addRange(range)
    }
  } else {
    document.execCommand('insertText', false, result)
  }
})

这类方法也能很好地完成粘贴图片的功用,而且通用性会更好。接下来我们还会应用 Selection,来完成 emoji 的插进去。

插进去 emoji

不管是粘贴文本也好,照样图片也好,我们的输入框始终是处于聚焦(focus)状况。而当我们从脸色面板里挑选 emoji 脸色的时刻,输入框会先失焦(blur),然后再从新聚焦。因为 document.execCommand 要领必需在输入框聚焦状况下才触发,所以关于处置惩罚 emoji 插进去来讲就没法运用了。

上一小节讲过,Selection 可以让我们拿到聚焦状况下所选文本的出发点位置 startOffset 和尽头位置 endOffset,假如没有挑选文本而仅仅处于聚焦状况,那末这两个位置的值相称(相当于挑选文本为空),也就是光标的位置。只需我们可以在失焦前纪录下这个位置,那末就可以经由过程 range 把 emoji 插进去准确的处所了。

起首编写两个东西要领。新建一个 cursorPosition.js 文件:


/**
 * 猎取光标位置
 * @param {DOMElement} element 输入框的dom节点
 * @return {Number} 光标位置
 */
export const getCursorPosition = (element) => {
  let caretOffset = 0
  const doc = element.ownerDocument || element.document
  const win = doc.defaultView || doc.parentWindow
  const sel = win.getSelection()
  if (sel.rangeCount > 0) {
    const range = win.getSelection().getRangeAt(0)
    const preCaretRange = range.cloneRange()
    preCaretRange.selectNodeContents(element)
    preCaretRange.setEnd(range.endContainer, range.endOffset)
    caretOffset = preCaretRange.toString().length
  }
  return caretOffset
}

/**
 * 设置光标位置
 * @param {DOMElement} element 输入框的dom节点
 * @param {Number} cursorPosition 光标位置的值
 */
export const setCursorPosition = (element, cursorPosition) => {
  const range = document.createRange()
  range.setStart(element.firstChild, cursorPosition)
  range.setEnd(element.firstChild, cursorPosition)
  const sel = window.getSelection()
  sel.removeAllRanges()
  sel.addRange(range)
}

有了这两个要领今后,就可以放入 editor 节点内里运用了。起首在节点的 keyupclick 事宜里纪录光标位置:

let cursorPosition = 0
const editor = document.querySelector('.editor')
editor.addEventListener('click', async (e) => {
  cursorPosition = getCursorPosition(editor)
})
editor.addEventListener('keyup', async (e) => {
  cursorPosition = getCursorPosition(editor)
})

纪录下光标位置后,便可经由过程挪用 insertEmoji() 要领插进去 emoji 字符了。

insertEmoji (emoji) {
  const text = editor.innerHTML
  // 插进去 emoji
  editor.innerHTML = text.slice(0, cursorPosition) + emoji + text.slice(cursorPosition, text.length)
  // 光标位置后挪一名,以保证在刚插进去的 emoji 背面
  setCursorPosition(editor, this.cursorPosition + 1)
  // 更新当地保留的光标位置变量(注重 emoji 占两个字节大小,所以要加1)
  cursorPosition = getCursorPosition(editor) + 1 //  emoji 占两位
}

尾声

文章触及的代码已上传到堆栈,为了轻便起见采纳 VueJS 处置惩罚了一下,不影响浏览。末了想说的是,这个 Demo 仅仅完成了输入框最基本的部份,关于复制粘贴另有许多细节要处置惩罚(比方把别处的行内款式也复制了进来等等),在这里就不逐一展开了,感兴趣的读者可以自行研讨,更迎接和我留言交换~

    原文作者:jrainlau
    原文地址: https://segmentfault.com/a/1190000018360827
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞