网络聊天工具的富文本输入框
时间:2023-03-19 14:48:37
科技观察
最近折腾了Websocket,准备开发一个聊天室应用来练习。在应用开发过程中,我发现这个可以插入表情符号和粘贴图片的富文本输入框其实包含了很多有趣的知识,所以打算记录下来分享给大家。富文本输入框传统的输入框是使用
剪贴板的内容是It保存在DataTransferItemList对象中,可以通过e.clipboardData.items访问:细心的读者会发现,如果在控制台直接点击DataTransferItemList前面的小箭头,会发现该对象的length属性为0。剪贴板的内容呢?其实这是Chrome调试的一个小坑。在开发者工具中,console.log出来的对象是一个引用,会随着原始数据的变化而变化。由于剪贴板数据已经被“粘贴”到输入框中,所以展开小箭头后看到的DataTransferItemList就变成了空的。为此,我们可以使用console.table来显示实时结果。了解剪贴板数据的存储位置后,您可以编写代码来处理它们。由于我们的富文本输入框比较简单,所以只需要处理两类数据,一类是普通文本类型的数据,包括emoji表情;另一个是图像类型数据。新建一个paste.js文件:constonPaste=(e)=>{//如果剪贴板没有数据,直接返回if(!(e.clipboardData&&e.clipboardData.items)){return}//用Promise封装为以后使用returnnewPromise((resolve,reject)=>{//复制的内容在剪贴板中的位置不确定,所以通过遍历for(leti=0,len=e.clipboardData.items来保证数据准确.length;i
{resolve(str)})//图片格式内容处理}elseif(item.kind==='file'){constpasteFile=item.getAsFile()//处理pasteFile//TODO(pasteFile)}else{reject(newError('Notallowtopastethistype!'))}}})}exportdefaultonPaste然后可以直接在onPaste事件中使用:document.querySelector('.editor').addEventListener('paste',async(e)=>{constresult=awaitonPaste(e)console.log(result)})以上代码支持text格式,接下来就是imag的处理e格式。玩过的同学就会知道,包括图片在内的所有文件格式内容都会存储在File对象中,在剪贴板中也是一样的。所以我们可以写一组通用的函数来读取File对象中的图片内容,并转换成base64字符串。粘贴图片为了更好的在输入框中显示图片,必须限制图片的大小,所以这个图片处理函数不仅可以读取File对象中的图片,还可以对其进行压缩。新建一个chooseImg.js文件:/***预览函数**@param{*}dataUrlbase64字符串*@param{*}cb回调函数*/functiontoPreviewer(dataUrl,cb){cb&&cb(dataUrl)}/***图片压缩函数**@param{*}imgimageobject*@param{*}fileTypeimagetype*@param{*}maxWidthimage***width*@returnsbase64string*/functioncompress(img,fileType,maxWidth){letcanvas=document.createElement('canvas')letctx=canvas.getContext('2d')constproportion=img.width/img.heightconstwidth=maxWidthconstheight=maxWidth/proportioncanvas.width=widthcanvas.height=heightctx.fillStyle='#fff'ctx.fillRect(0,0,canvas.width,canvas.height)ctx.drawImage(img,0,0,width,height)constbase64data=canvas.toDataURL(fileType,0.75)canvas=ctx=nullreturnbase64data}/***选择图片函数**@param{*}einput.onchange事件对象*@param{*}cb回调函数*@param{number}[maxsize=200*1024]image***volume*/functionchooseImg(e,cb,maxsize=200*1024){constfile=e.target.files[0]if(!file||!/\/(?:jpeg|jpg|png)/i.test(file.type)){return}constreader=newFileReader()reader.onload=function(){constreult=this.resultlettimg=newImage()if(result.length<=maxsize){toPreviewer(result,cb)return}img.onload=function(){constcompresscompressedDataUrl=compress(img,file.type,maxsize/1024)toPreviewer(compressedDataUrl,cb)img=null}img。src=result}reader.readAsDataURL(file)}exportdefaultchooseImg关于使用canvas压缩图片和使用FileReader读取文件,这里不再赘述。有兴趣的读者可以自行参考上一步中的paste.js函数。TODO()可以重写为chooseImg():constimgEvent={target:{files:[pasteFile]}}chooseImg(imgEvent,(url)=>{resolve(url)})返回到浏览器,如果我们复制一张图片,粘贴到输入框,就可以看到控制台打印出data:image/png;base64开头的图片地址。在输入框中插入内容经过前面两步,我们终于可以读取到剪贴板中的文本内容和图片内容了,接下来就是将它们正确插入到输入框的光标位置。对于插入内容,我们可以直接通过document.execCommand方法来完成。这个方法的详细用法可以在MDN文档中找到,这里我们只需要使用insertText和insertImage即可。document.querySelector('.editor').addEventListener('paste',async(e)=>{constresult=awaitonPaste(e)constimgRegx=/^data:image\/png;base64,/constcommand=imgRegx.test(结果)?'insertImage':'insertText'document.execCommand(command,false,result)})但是在某些版本的Chrome浏览器下,insertImage方法可能无效。这时候可以使用另一种方法,使用Selection来实现。而选择和插入表情符号的操作后面也会用到,先来看看吧。当我们在代码中调用window.getSelection()时,我们会得到一个Selection对象。如果你在页面上选择了一些文本,然后在控制台执行window.getSelection().toString(),你会看到输出的是你选择的那部分文本。对应这部分区域文本的是一个范围对象,可以使用window.getSelection().getRangeAt(0)来访问。range不仅包含选中区域的文本内容,还包含该区域的startOffset和endOffset。我们也可以通过document.createRange()手动创建一个range,将内容写入其中,显示在输入框中。插入图片,首先从window.getSelection()中获取范围,然后将图片插入其中。document.querySelector('.editor').addEventListener('paste',async(e)=>{//读取剪贴板内容constresult=awaitonPaste(e)constimgRegx=/^data:image\/png;base64,///如果是图片格式(base64),通过构造range将
标签插入到正确位置//如果是文本格式,通过document.execCommand('insertText')方法插入文本如果(imgRegx.test(结果)){constsel=window.getSelection()if(sel&&sel.rangeCount===1&&sel.isCollapsed){constrange=sel.getRangeAt(0)constimg=newImage()img.src=resultrange.insertNode(img)range.collapse(false)sel.removeAllRanges()sel.addRange(range)}}else{document.execCommand('insertText',false,result)}})这个方法也可以很好的完成粘贴图片的功能,而且通用性会更好。接下来,我们将使用Selection来完成emoji的插入。无论插入表情符号是粘贴文字还是图片,我们的输入框始终处于焦点状态。而当我们从表情面板中选择emoji表情时,输入框会先失焦(模糊),然后再重新聚焦。由于document.execCommand方法必须在输入框获得焦点时触发,因此不能用于处理表情符号插入。上一节讲到,Selection可以让我们获取到选中文本在聚焦状态下的起始位置startOffset和结束位置endOffset。如果没有选中文本,只是处于聚焦状态,这两个位置的值是相等的(相当于选中文本为空),也就是光标所在的位置。只要我们能够在失去焦点之前记录下这个位置,那么我们就可以通过范围将表情符号插入到正确的位置。先写两个实用方法。新建cursorPosition.js文件:/***获取光标位置*@param{DOMElement}元素输入框dom节点*@return{Number}光标位置*/exportconstgetCursorPosition=(element)=>{letcaretOffset=0constdoc=element.ownerDocument||element.documentconstwin=doc.defaultView||doc.parentWindowconstsel=win.getSelection()if(sel.rangeCount>0){constrange=win.getSelection().getRangeAt(0)constpreCaretRange=range.cloneRange()preCaretRange.selectNodeContents(element)preCaretRange.setEnd(range.endContainer,range.endOffset)caretOffset=preCaretRange.toString().length}returncaretOffset}/***设置光标位置*@param{DOMElement}element输入框dom节点*@param{Number}cursorPosition光标位置值*/exportconstsetCursorPosition=(element,cursorPosition)=>{constrange=document.createRange()range.setStart(element.firstChild,cursorPosition)range.setEnd(element.firstChild,cursorPosition)constsel=window.getSelection()sel.removeAllRanges()sel.addRange(range)}有了这两个方法,就可以在编辑器节点中使用了。首先在节点的keyup和click事件中记录光标位置:letcursorPosition=0consteditor=document.querySelector('.editor')editor.addEventListener('click',async(e)=>{cursorPosition=getCursorPosition(editor)})editor.addEventListener('keyup',async(e)=>{cursorPosition=getCursorPosition(editor)})记录光标位置后,调用insertEmoji()方法即可插入emoji字符。insertEmoji(emoji){consttext=editor.innerHTML//插入emojieditor.innerHTML=text.slice(0,cursorPosition)+emoji+text.slice(cursorPosition,text.length)//在光标位置后移动一个位置保证SetCursorPosition(editor,this.cursorPosition+1)afterthenewinsertedemoji//更新本地保存的光标位置变量(注意emoji占两个字节,所以加1)cursorPosition=getCursorPosition(editor)+1//emoji中涉及的代码完结文章已上传至仓库。为了简单起见,在不影响阅读的情况下,用VueJS做了处理。***我想说的是,这个Demo只完成了输入框最基本的部分,关于复制粘贴还有很多细节需要处理(比如从其他地方复制内联样式等等),所以我不会在这里一一列出。已经展开了,有兴趣的读者可以自己研究,欢迎给我留言~