上一篇文章中,我们已经初步实现了聊天输入框,但其@功能是不完善的,例如无法整体删除、无法获取除用户名以外的数据(假设用户名不是唯一的)。有问题就要想办法解决,在网上百度了一圈后,倒是有一些收获。本文就着重解决@的整体删除以及获取额外数据。
准备工作
聊天框的实现是基于div+contenteditable
的。一旦元素开启了可编辑属性,就可以像输入框一样输入内容。不同的是,我们可以传入HTML格式的代码,这也是可以渲染出来的。
<div contenteditable><p style="color: red;">hello</p></div><div contenteditable>hello</div>
回到我们之前实现的@功能,是直接插入的特殊字符串,删除时也只能一个一个字符的删除,当然也可以通过监听backspace
和delete
按键,结合正则表达式,手动移动光标来删除,这种方式过于复杂,笔者也没有搞明白要怎么操作;根据上述的渲染结果,我们是不是可以考虑将@xxx
也当作一个HTML标签插入到输入框中,然后保证删除时能整体删除就可以了。
初步解决方案
在插入HTML标签前,我们先来了解替换元素
和非替换元素
。
替换元素是指浏览器会元素的标签和属性,来决定元素的具体显示内容,如果未指定属性则显示的将是一个空标签,传入不同的属性值其在页面上渲染出来的结果也不一样。常见的替换元素包括:img
、input
、textarea
、select
、object
非替换元素是指其内容可以直接展现给浏览器,HTML中大多数元素是不可替换元素。
了解了这个有什么用呢,我们可以试试在可编辑元素中插入一个替换元素标签:
<div contenteditable> <input value="@xxx" readonly /></div><div contenteditable> <img src="xxx" alt="@xxx" /></div>
可以发现,在输入框内是可以整体删除@xxx
,基本功能是可以实现的,但是有几个问题需要解决:
input
有默认的宽度,并且需要指定为只读,由于我们的@xxx
宽度是不定的,需要使得input
的宽度自适应,实现起来比较负责,笔者未实现
img
标签需要指定有效的src属性,如果指定的src无效,即使有alt属性值,也会有一个裂开的图片。为了解决这个裂开的图片,笔者尝试了多种方法都没有解决,真是要裂开了。解决不了裂开的图片,笔者就尝试将@xxx
通过html2canvas
和canvas2img
的方式将其转换为base64的图片,效果也还行,就是转换的过程有点慢,也被pass掉了。
难道除了这种方式就没有别的了嘛,答案是有的。
最终解决方案
不是说使用input
、img
标签不行,只是用起来有点麻烦,于是乎笔者就将目光转向了非替换元素。先来一个a
标签试试水:
<div contenteditable> <a contenteditable="false">@xxx</a></div>
可编辑元素的子元素默认也是可编辑的,因此需要设置a标签为不可编辑,不然就无法实现整体删除。运行上面的例子,可以发现基本上符合我们的预期效果,想要完美实现,还得考虑几点:
除了@xxx
本身外,还应该携带唯一的标识,这样才能区分艾特了哪些人
如果是输入@,然后选择了具体的成员,那么之前输入的@应该被删除掉
如何保证光标是在@xxx
后面
有了具体的问题,我们就来逐一解决:
额外参数
a
标签可以在value中携带唯一标识,然后我们在发送文本时,从文本内容中取出被艾特的人即可。
<div contenteditable> <a name="at" :value="username" contenteditable="false">@xxx</a></div>
删除之前输入的@
因为我们在a标签中已经包含了@,因此之前输入的@就需要删除。也许有人会想,我要通过调用backspce
或者delete
来实现,可惜这种方式并不可行。正确的思路应该是通过字符串的替换,来模拟实现删除功能。我们这里采用正则表达式的方式来处理,因为我们只需要将@xxx
左边最近的一个@删除即可:
if (/@<a name="at"/.test(this.$refs.editor.innerHTML)) { this.$refs.editor.innerHTML = this.$refs.editor.innerHTML.replace(/@<a name="at"/, '<a name="at"');}
我们使用name="at"
只是替换在输入框中的@,如果有其他的a标签我们将不处理。使用直接替换的方式,会导致光标默认跑到开头,这显然不符合要求,接下来处理光标
处理光标位置
我们需要将光标插入到@xxx
后面,具体是哪一个@xxx
就需要通过getElementById()
来查找,然后将光标移动到此元素后面,因此我们在插入a标签时,还应该指定每一个a标签的id,使用一个递增的全局变量即可。
// DivEditable.vueif (/@<a name="at"/.test(this.$refs.editor.innerHTML)) { // 使用正则替换,将已经输入的@替换掉 // 如果直接赋值修改innerHTML,则光标默认会回到开头。因此需要额外处理光标 this.$refs.editor.innerHTML = this.$refs.editor.innerHTML.replace(/@<a name="at"/, '<a name="at"'); // id表示哪一个@ let el = document.getElementById(id); range = document.createRange(); sel = window.getSelection(); // 将光标重新定位到自定义的a标签后面 range.setStartAfter(el); range.setEndAfter(el); sel.removeAllRanges(); sel.addRange(range);}
// InputBox.vueonSelect(item) { this.atIndex++; // 使用a标签表示@的成员 let at = `<a name="at" value="${item.userName}" tabindex="-1" id="${this.atIndex}" contenteditable="false" href="javascript:void(0)">@${item.name}</a>​`; this.$refs.inputBox.insertContent(at, this.atIndex); console.log('onSelect', item); // this.$refs.inputBox.insertContent(`${item.name} `); // 有空格 this.isShowAt = false;},
获取@的成员
我们通过正则表达式来获取
let atIds = [];this.$refs.editor.innerHTML.replace(/<a [^>]*value=['"]([^'"]+)[^>]*/gi, function(match, capture) { atIds.push(capture);})
这样我们就基本实现了使用a标签来完成@的整体删除,这里有一个小细节。一般我们都会在@xxx
后面有一个空格,我们可以使用
,也可以使用零宽字符​
。笔者发现在不同的浏览器上,使用空格和零宽字符的效果还是有所差异的。
总结
本文介绍了使用a标签来完成@功能的整体删除,当然除了使用a标签,span、button、img等标签都是可以选择的技术方案,实现的原理都差不多。不管是采用哪种方案,都需要注意几点:
可编辑元素的子元素默认也是可编辑的,因此插入标签时需要设置为不可编辑
插入标签时,需要将已经输入的@字符删除
注意光标位置的处理
标签自身的样式需要部分覆盖,具体的看使用情况
若有其他解决方案,欢迎在评论区补充。最后,完整代码可参考 项目地址
原文:https://juejin.cn/post/7101539945809969188