Rich Text Input Box of Web Chat Tool

Recently, I have been tossing about Websocket and plan to develop a chat room application to train my hands. In the process of application development, it was found that emoji can be inserted, and the rich text input box pasted with pictures actually contains a lot of interesting knowledge, so I planned to record it and share it with everyone.

Warehouse address:chat-input-box
Preview address:https://codepen.io/jrainlau/p …

First, let’s look at the demo effect:

图片描述

Isn’t it amazing? Next, I will explain step by step how all the functions are realized.

The input box is rich in text.

Traditional input boxes are all used<textarea>To make, its advantage is very simple, but the biggest defect is unable to display pictures. In order to enable the input box to display pictures (rich text), we can use the settingscontenteditable="true"attributive<div>To realize the functions inside.

Simply create oneindex.htmlFile, and then write the following:

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

Open the browser and you will see an input box with a picture by default:

clipboard.png

The cursor can move back and forth in the picture, at the same time, it can also input the content, and even delete the picture by backspace key-in other words, the picture is also part of the editable content, which means that the rich text of the input box has already been reflected.

The next task is to think about how to pass directly.control + vPaste the picture in.

Handle paste events

Anything that is “copied” orcontrol + cThe copied content (including screenshots) will be stored in the clipboard and can be entered in the input box when pasting.onpasteI heard in the incident.

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

The contents of the clipboard are stored inDataTransferItemListObject, you can use thee.clipboardData.itemsVisit:

clipboard.png

Careful readers will find that if you click directly on the consoleDataTransferItemListBefore the small arrow, will find the objectlengthThe property is 0. What about the clipboard contents? In fact, this is a small pit for Chrome debugging. In the developer tool,console.logThe object that comes out is a reference and will change as the original data changes. Since the clipboard data has been “pasted” into the input box, what you see after you expand the small arrowDataTransferItemListIt becomes empty. To this end, we can switch toconsole.tableTo show real-time results.

clipboard.png

Once you understand where the clipboard data is stored, you can write code to handle them. Since our rich text input box is relatively simple, only two types of data need to be processed, one is common text type data, including emoji expressions; The second is picture type data.

Newpaste.jsDocuments:

const onPaste = (e) => {
 //If the clipboard has no data, return it directly
 if (!  (e.clipboardData && e.clipboardData.items)) {
 return
 }
 //Package with Promise for Future Use
 return new Promise((resolve, reject) => {
 //The location of the copied content in the clipboard is uncertain, so the data is guaranteed to be accurate through traversal.
 for (let i = 0, len = e.clipboardData.items.length;   i < len;  i++) {
 const item = e.clipboardData.items[i]
 //Text Format Content Processing
 if (item.kind === 'string') {
 item.getAsString((str) => {
 resolve(str)
 })
 //picture format content processing
 } else if (item.kind === 'file') {
 const pasteFile = item.getAsFile()
 //process pasteFile
 // TODO(pasteFile)
 } else {
 reject(new Error('Not allow to paste this type!'  ))
 }
 }
 })
 }
 
 export default onPaste

Then you can go to theonPasteThe incident directly used:

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

The above code supports text format, and the picture format will be processed next. Played<input type="file">My classmates know that all file format contents including pictures will be stored inFileObject, this is the same in the clipboard. So we can write a set of general functions to read specificallyFileObject and convert it intobase64String.

Paste picture

In order to better display the picture in the input box, the size of the picture must be limited, so this picture processing function can not only readFileThe image inside the object can also be compressed.

Create a new onechooseImg.jsDocuments:

/**
 * Preview function
 *
 * @param {*} dataUrl base64 string
 * @param {*} cb callback function
 */
 function toPreviewer (dataUrl, cb) {
 cb && cb(dataUrl)
 }
 
 /**
 * picture compression function
 *
 * @param {*} img picture object
 * @param {*} fileType picture type
 * @param {*} maxWidth picture maximum width
 * @ returnbase64 string
 */
 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
 }
 
 /**
 * Select picture function
 *
 * @ param {*} input.onchange event object
 * @param {*} cb callback function
 * @ param {number} [maxsize = 200 * 1024] maximum image size
 */
 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

About usecanvasCompress pictures and useFileReaderThe contents of the read file will not be repeated here, and interested readers can refer to it by themselves.

Back to the previous steppaste.jsFunction, which takes theTODO()Rewrite intochooseImg()Just:

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

Back to the browser, if we copy a picture and paste it in the input box, we will see the printed image in the consoledata:image/png; base64The picture address at the beginning.

Insert content into input box

After the first two steps, we can already read the text and picture contents in the clipboard, and then insert them correctly into the cursor position of the input box.

For the inserted content, we can directly pass throughdocument.execCommandMethods. Detailed usage of this method can be found inMDN documentFound inside, here we only need to useinsertTextAndinsertImageJust.

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)
 })

But under some versions of Chrome browser,insertImageThe method may fail, then another method can be adopted to useSelectionTo achieve. The operation of selecting and inserting emoji later will also use it, so we might as well take a look at it first.

When we call in codewindow.getSelection()You’ll get one later.SelectionObject. If you select some text in the page and then execute it in the consolewindow.getSelection().toString(), you will see that the output is the part of the text you selected.

Corresponding to this part of the text, is arangeObject, usingwindow.getSelection().getRangeAt(0)That is, it can be accessed.rangeIt includes not only the text of the selected area, but also the starting position of the area.startOffsetAnd the end positionendOffset.

We can also passdocument.createRange()To manually create arange, write content into it and display it in the input box.

To insert a picture, you must first start fromwindow.getSelection()Obtainrange, and then insert the picture into it.

document.querySelector('.editor').addEventListener('paste', async (e) => {
 //Read the clipboard contents
 const result = await onPaste(e)
 const imgRegx = /^data:image\/png;  base64,/
 //if it is in picture format (base64), insert < img > label into the correct position by constructing range
 //If it is in text format, insert the text through the Document. ExecCommand ('INSERT TEXT') method
 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)
 }
 })

This method can also accomplish the function of pasting pictures well and has better versatility. Next we will useSelectionTo complete the insertion of emoji.

Insert emoji

Whether pasting text or pictures, our input box is always in focus. When we select emoji expression from the expression panel, the input blur will lose focus first and then refocus. Due todocument.execCommandThe method can only be triggered when the input box is in focus, so it cannot be used to process emoji insertion.

As mentioned in the previous section,SelectionWe can get the starting position of the selected text in the focused state.startOffsetAnd the end positionendOffset, if no text is selected but only in focus, then the value of the two positions is equal (equivalent to the selected text is empty), that is, the position of the cursor. As long as we can record this position before defocus, we can passrangeEmoji was inserted in the right place.

First, write two tool methods. Create a new onecursorPosition.jsDocuments:

/**
 * Get cursor position
 * @param {DOMElement} element node of element input box
 * @return {Number} cursor position
 */
 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
 }
 
 /**
 * Set cursor position
 * @param {DOMElement} element node of element input box
 * @param {Number} cursorPosition value
 */
 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)
 }

With these two methods, you can use them in the editor node. First, at the node’skeyupAndclickThe cursor position is recorded in the event:

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

After recording the cursor position, you can call theinsertEmoji()The method inserted the emoji character.

insertEmoji (emoji) {
 const text = editor.innerHTML
 //insert emoji
 editor.innerHTML = text.slice(0, cursorPosition) + emoji + text.slice(cursorPosition, text.length)
 //Move the cursor one position behind to ensure that it is behind the emoji just inserted.
 setCursorPosition(editor, this.cursorPosition + 1)
 //Update the cursor position variable saved locally (note that emoji takes up two bytes, so add 1)
 Cursorposition = getcursorpos (editor)+1//emoji occupies two positions.
 }

The end

The code involved in the article has been uploaded toWarehouseFor the sake of simplicityVueJSAfter processing it, it will not affect reading. The last thing I want to say is that this Demo has only completed the most basic part of the input box. there are still many details to deal with about copying and pasting (e.g. copying the in-line styles from other places, etc.). it will not be launched here one by one. interested readers can study it by themselves and are more welcome to leave a message with me ~