pc端实现在输入框@at搜索并选择人功能

我写的很垃圾,建议别看。

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>&nbsp;</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(/&nbsp;/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


版权声明:本文为qq_42114918原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。