我写的很垃圾,建议别看。
h5移动端的参见:h5移动端实现在输入框@at搜索并选择人功能_YICONGITSME的博客-CSDN博客
一、整体思路
思路:
利用wangeditor的富文本编辑器功能,在输入框里插入@员工的元素,普通的输入框是没有办法做到的。监听输入@,并阻止默认事件,通过getSelection获取当前的选区,用于确定之后的添加位置,同时获取输入@的位置,定位下拉框的位置。创建一个<span class="fake-at">@</span>的标签插入到当前光标的位置,(插入可以使用range的insertNode也可以使用editor的cmd.do())用于之后的替换,
选择完员工之后,创建两个span标签,<span class="at-text">@员工1</span><span> </span>,使用属性contentEditable设置元素不可编辑,空格的作用主要是为了分割开@员工这个标签,另起输入,并创建一个空白文档,在内存中保存这两个节点,然后获取上一步保存的选区的位置,把节点插入进去,并把光标放在最后,最后查找最初创建的fake-at节点,从父节点中删除此节点。
弊端:
连续输入两个@之后,选择员工会出错,因为一开始创建了一个fake-at元素,不选择,再输入at的话,此时这个标签会创建到之前的fake-at里面,形成一个嵌套,这样会有问题。
如果换行输入@的话,会重新创建一个div元素,里面是一个p标签,p里面是span标签,这样删除时,无法准确的找到当前@元素的父节点,来删除它。
改进:
输入@时,禁用默认事件,输入@时不创建@元素,直接弹出下拉框,选择员工后,创建节点,插入进去,如果输入@后不选择,关闭下拉框,则在光标位置处插入一个文本@,这样既能实现用户@员工的功能又能实现正常的@文本输入,随意插入、删除、换行操作也不会报错。
二、创建dom
<div>
<div ref="editor" class="editor" id="editor" @keydown="enterEv($event)"></div>
<div class="at-someone" :style="{left:left+'px',top:top+'px',visibility:showFlag}">
<div
class="at-someone-box"
ref="atSomeoneBox"
>
<input
class="search-input"
placeholder="请输入要搜索的员工的姓名或手机号"
type="text"
@input="handlerSearchInput($event)"
@keydown="handlerKeyDown"
v-model.trim="searchKeyword"
ref="searchInput"
>
<div class="search-content">
<ul class="search-list-ul">
<li
v-show="!isSearching"
v-if="searchOriginList != null && searchOriginList.length == 0"
class="no-result"
><em>暂无搜索结果...</em></li>
<li v-show="isSearching" class="no-result"><em>正在努力加载...</em></li>
<li class="search-list-li"
v-if="searchOriginList != null && searchOriginList.length != 0"
v-for="(employee,index) in searchOriginList"
@click="selectPerson(employee)"
>
<span class="fl tit-head">{{employee.name[0]}}</span>
</li>
</ul>
</div>
</div>
<div class="at-someone-mask" @click.stop="handlerClickMask" v-show="showFlag == 'visible'"></div>
</div>
<div class="comment-form-error" v-show="overWord">最多输入300个字符</div>
</div>
三、创建editor
// script或者 npm 均可
mounted () {
let E = window.wangEditor;
let editor = new E('#editor')
editor.config.onchange = (html) => {
//判断输入内容是否超出限制长度
this.overWord = editor.txt.text().replace(/ /ig," ").length > 300;
}
//菜单置空
editor.config.menus = [];
//创建
editor.create();
this.editor = editor;
//修改默认的placeHolder
let placeholder = document.getElementsByClassName('placeholder')[0];
placeholder.innerHTML = '请输入评论(尝试@TA,TA将会在APP工作通知中收到消息)';
},
四、触发下拉框
enterEv (e) {
const t = this;
//getSelection是另一种获取用户选择的文本范围或光标的当前位置的方法
let selection = getSelection();
//判断输入的是@符号
if (((e.keyCode === 229 && e.key === '@') || (e.keyCode === 229 && e.code === 'Digit2') || e.keyCode === 50) && e.shiftKey) {
//阻止键盘的输入
e.preventDefault ? e.preventDefault() : e.returnValue = false;
//getRangeAt()用来获取一个包含当前选区内容的区域对象
t.position = {
range: selection.getRangeAt(0), //获取选区区域的引用
selection: selection
}
try {
//初始时,编辑器中元素是<p><br></p>
if (t.editor.$textElem.elems[0].firstChild.firstChild.tagName === "BR") {
//移除br
t.editor.$textElem.elems[0].firstChild.removeChild(t.editor.$textElem.elems[0].firstChild.firstChild);
}
} catch (e) {
console.log(e);
}
//创建一个临时@元素(不建议使用,会有很多问题)
// let fakeNode = document.createElement('span');
// fakeNode.className = 'fake-at';
// fakeNode.innerHTML = '@';
// //在选区起点处插入节点
// t.position.range.insertNode(fakeNode);
// //光标定位在@之后
// t.position.selection.collapse(fakeNode, 1);
// this.editor.cmd.do('insertHTML','<span class="fake-at active">@</span>') ;
//根据输入@的位置定位下拉框的位置
//获取位置
const pos = position(ele);
//定位下拉框位置
t.left = pos.left+10;
t.top = pos.top+10;
//展示下拉框
t.showFlag = 'visible';
//下拉框搜索框自动获取焦点
t.$nextTick(()=>{
t.$refs.searchInput.focus();
})
}
},
五、选择员工
selectPerson (item) {
//插入@人元素
const {name,employeeId:id} = item;
//获取选区对象
let selection = t.position.selection;
let range = t.position.range;
// 生成需要显示的内容,包括一个 span 和一个空格。
let spanNode1 = document.createElement('span');
let spanNode2 = document.createElement('span');
spanNode1.className = 'at-text';
spanNode1.style.color = '#2fd3a1';
spanNode1.innerHTML = '@' + name;
spanNode1.dataset.id = id;
// 设置@人的节点不可编辑
spanNode1.contentEditable = false;
spanNode2.innerHTML = ' ';
// 将生成内容打包放在 Fragment 中,并获取生成内容的最后一个节点,也就是空格。
//创建一个新的空白的文档片段
let frag = document.createDocumentFragment(),
node, lastNode;
frag.appendChild(spanNode1);
while ((node = spanNode2.firstChild)) {
lastNode = frag.appendChild(node);
}
// 将 Fragment 中的内容放入 range 中,并将光标放在空格之后。
range.insertNode(frag);
selection.collapse(lastNode, 1);
//删除刚才添加的@假节点(如果使用上面创建假节点的方式假,就需要删除,有很多问题,换行操作会导致找不到父节点报错)
// let fakeAtNode = document.querySelector('.fake-at');
// if (fakeAtNode) {
// let currEle = this.editor.selection.getSelectionContainerElem().elems[0].parentNode;
// currEle.removeChild(fakeAtNode);
// }
//将当前的选区折叠到最末尾的一个点
selection.collapseToEnd();
},
六、实现效果
七、方法尝试
一开始使用过一个Tribute插件的一个支持vue的vue-tribute组件,但是输入后获取不到内容。
在h5使用过antd 的mentions提及功能,在3版本中使用mentions配合form组件使用能获取到输入的内容,但是项目的react版本太低,不支持3版本,所以尝试了一下2版本的mention功能,但其实mention功能已经废弃了,用mentions来替代,我还是配合表单使用了一下,发现无法获取表单的内容,此方案不行。
再后来使用wangeditor获取不到光标位置时,我又尝试了一个适用于移动端的编辑器quill,因为wangeditor不适用于移动端,这个编辑器提供的api很多,但是想要插入一个节点非常的麻烦,所以也不行。
八、总结
一开始都在找有没有现成的组件使用,但是这些组件会发现他不满足自己的需要,还是需要自己做出一个来,所以开始研究各种DOM元素的属性、事件,Web API的selection对象、range对象,react提供的在元素上的合成事件,充分利用wangeditor提供的api,功夫不负有心人,通过使用原生js解决了很多问题,我才发现自己用原生js实现功能是一件多么快乐的事,就感觉是自己创造出来的,收获非常的多。
一开始做的功能比较简易,一提测发现问题很严重,测试和产品都逼得很紧,在研究了一会发现方案不行时,立马改变方案,在输入框搜索改为在下拉框搜索的样式,这样问题便解决了,用了两个小时,超出了预期,很是开心。
时间很紧张,每天顶着压力解决这些问题,有时候脑子是真的转不动了,想不出一点办法来,但问题总要被解决,最后问题都一一解决了。
所以啊,用啥插件都不如自己用原生js实现,能实现自己想要的功能,出问题了知道去哪解决,最主要的是收获满满。
九、参考文档
参考代码:https://github.com/ThreeStonesLee/At-Someone
选区:Window.getSelection - Web API 接口参考 | MDN
选区区域对象:Selection.getRangeAt() - Web API 接口参考 | MDN
获取光标位置:js获取光标位置_风火一回,一生不毁-CSDN博客_js获取光标位置
空白文档:Document.createDocumentFragment() - Web API 接口参考 | MDN
DOM属性:HTML DOM Document 对象 | 菜鸟教程
DOM的不可编辑属性:HTML DOM contentEditable 属性 | 菜鸟教程
wangeditor文档:Introduction · wangEditor 用户文档
react合成事件:合成事件 – React
十、遗留问题
pc端:在mac的safari浏览器以及windows的浏览器上,在中文输入法的情况下,输入shift+@会多输入一个@,因为阻止不了中文的键盘默认事件,但是英文状态下是可以的。
github地址: https://github.com/YICONGISME/at-someone