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


pc端的参见https://blog.csdn.net/qq_42114918/article/details/109446160

一、整体思路

思路:

使用的同样是wangeditor编辑器,其实最主要使用的是他获取当前选区的方法和在光标位置插入节点的方法,想和pc端一样实现在输入框里输入@然后跳转选择员工界面,但是由于在手机上监听不到@键的输入,所以只能使用一个按钮的方式,选择员工后,插入到输入框的光标的位置。删除通过监听键盘的删除键,获取当前选区的节点的父节点,然后移除子节点。

弊端:

wangeditor在h5输入完中文后会监听不到输入了内容,也获取不到最后光标的位置,这样就没办法正确的插入元素。

使用contentEditable属性禁用at-text标签不可编辑,会导致光标点击不了,引发很严重的问题。

删除时删掉at-text之后的空格输入的话, 会进入到at-text标签中,导致输入有颜色,删除的话会删除标签之前的一个元素。

改进:

使用react的合成事件onCompositionEnd来监听中文的输入,在输入完之后保存当前的selection和range,在选择员工插入时,因为英文都是有key键值的,所以输入英文是可以获取到光标的,中文就不能了,所以获取之前保存的selection和range,就能插入到正确的位置,英文可以用editor.cmd.do的方式插入。

此时又有一个新的问题,输入完中文后移动光标位置怎么办,最初我使用onTouchEnd事件来重新赋值selection和range,但是发现获取的值是上一次操作的值,不准确,所以使用onClick事件,就能实时的获取当前的range了,这样就可以插入到正确的位置。

二、创建dom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
           <div>
                <div className="comment-at">
                    <div ref="editor" className="editor" id="editor" onKeyDown={(e)=>{this.onKeyDown(e)}}  onCompositionEnd={this.onCompositionEnd} onClick={this.onClick} />
                    {
                        maxLength ?
                            <div className="count-wrap">
                            <span
                                className={classnames('count', 'count-num',
                                    {'green': valueLength > 0 && valueLength <= maxLength},
                                    {'red': valueLength > maxLength}
                                )}
                            >
                                {valueLength}
                            </span>/
                                <span className="count count-max">{maxLength}</span>
                            </div>
                            :
                            null
                    }
                </div>

                <MultipleSearch
                    searchType='employee'
                    visible={visible}
                    placeholderText=""
                    includeSelf={false}
                    onCancel={this.onCancel}
                    onConfirm={this.onConfirm}
                    isMulti="true"
                    multiArr=''
                >
                    <div>
                        <a className="at-select" onClick={this.onEnter}>@提醒谁看</a>
                    </div>
                </MultipleSearch>
            </div>

三、创建editor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    componentDidMount() {
        let editor = new E('#editor');

        //菜单置空
        editor.config.menus = [];
        //创建
        editor.create();
        this.editor = editor;

        //修改默认的placeHolder
        let placeholder = document.getElementsByClassName('placeholder')[0];
        placeholder.innerHTML = '请输入评论(尝试@TA,TA将会在APP工作通知中收到消息)';

    }

四、键盘输入事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    //键盘输入事件
    onKeyDown(e){
        //由于要通过异步访问事件,所以需要保留事件属性,参见react的合成属性
        e.persist();
        //获取当前选区所在的dom节点
        let currentSelection = this.editor.selection.getSelectionContainerElem().elems[0];
        //通过类名来判断是不是@人dom节点
        let className = currentSelection.getAttribute('class');
        if(className == 'at-text'){
            //如果是删除则删除整个@dom节点
            // 使用 e.key === 'Backspace' 会删除整个span标签
            // 使用 e.keyCode === '8' 会一个字的删除
            if(e.keyCode === '8' || e.key === 'Backspace'){
                //获取当前选区所在的dom节点的父节点
                let ele = currentSelection.parentNode;
                ele.removeChild(currentSelection);
            }
        }
        //只有有key的输入就设置中文输入标识为false
        this.isComEnd = false;
    }

四、光标定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    //中文输入完之后获取光标的位置
    onCompositionEnd(){
        this.selection = getSelection();
        this.range = this.selection.getRangeAt(0);
        //并设置中文输入标识
        this.isComEnd = true;
    }

    // 光标点击时需要重新获取光标的位置
    onClick(){
        let selection = getSelection();
        let range = selection.getRangeAt(0);
        if(this.range && this.range.startOffset !== range.startOffset){
            this.selection = selection;
            this.range = range;
        }
    }

五、选择员工

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
            //如果是中文输入则在光标位置插入
            if(this.isComEnd){
                let insertNode = document.createElement('span');
                insertNode.innerHTML = '@'+item.name;
                insertNode.className = 'at-text';
                let insertNode2 = document.createElement('span');
                insertNode2.innerHTML = ' ';

                this.range.insertNode(insertNode2);
                this.range.insertNode(insertNode);
                this.selection.collapseToEnd();

            }else{
                insertNodes += '<span class="at-text">@'+item.name +'</span><span> </span>';
            this.editor.cmd.do('insertHTML',insertNodes);
            }


        //遍历插入的@人节点,设置颜色、不可编辑
        let atTexts = document.getElementsByClassName('at-text');
        for(let i=0; i<atTexts.length; i++){
            atTexts[i].style.color = '#2fd3a1';
            // //设置@人的节点不可编辑  在h5有很多弊端。。光标会定位不准确
            // atTexts[i].contentEditable = false;
        }

六、实现效果

七、方法尝试

一开始使用过一个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

选区:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getSelection

选区区域对象:https://developer.mozilla.org/zh-CN/docs/Web/API/Selection/getRangeAt

获取光标位置:https://blog.csdn.net/mafan121/article/details/78519348

空白文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createDocumentFragment

DOM属性:https://www.runoob.com/jsref/dom-obj-document.html

DOM的不可编辑属性:https://www.runoob.com/jsref/prop-html-contenteditable.html

wangeditor文档:http://www.wangeditor.com/doc/

react合成事件:https://react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/events.html