美文网首页
JS实现@功能

JS实现@功能

作者: 路过麦田 | 来源:发表于2017-02-22 10:27 被阅读1229次

    最近公司的PC端即时通讯工具需要添加@功能,整体软件采用的是Electron+Node.js来编写的,其实功能并不难,困难的是前段界面上的体验,说白了就是怎样利用js来实现@功能。

    为了做这个也是踩了不少的坑,目前来说界面已经做得差不多了吧,也算能用,还是总结一下吧,对别人来讲,总归是有点用的。

    @之前.png @之后.png

    整个界面的CSS样式还是很好写的,这里就不再详述了,首先遇到的一个问题就是输入@的时候弹出一个@列表,像图一那样的,虽然样式丑了些,但是功能算是实现了。

    弹出一个窗口还是很简单的,无非就是一个div的显示隐藏吗,但最主要的是弹出div的位置,要紧挨着@字符,也就是如何计算@字符的位置,这是遇到的第一个难题。

    通过万能的Google,我发现了Caret.js这个库,下面是作者的描述

    Get caret position or offset from inputor
    This is the core function that working in At.js.Now, It just become an simple jquery plugin so that everybody can use it.And, of course, At.js is using this plugin too.
    support iframe context

    描述中还提到了At.js这个玩意儿,一看名字就能够想到这肯定就是专门为@功能做的一个插件,事实也确实是这样的,这个库很强大,基本上几行代码就实现了一个@功能,但是我并没有直接用这个库,而是只用了Caret.js这个库来进行定位,为啥不直接用At.js呢,简单粗暴,省时省力。
    说实话,我确实试了试,结果不容乐观,出现了几个错误,在简单尝试解决未果之后,果断决定自己做一个,然后就是,自己走的路,跪着也要走完。。。欲哭无泪
    好了,不多说了,上代码

    <!-- 整个聊天界面的布局 -->
    <div class="chat-record-area"></div>
    <div class="chat-drag-area"></div>
    <div class="chat-function-area">
        <a href="#" id="smile" data-placement="vertical">
            ![](../../assets/icon/face.png)
        </a>
        <a href="#" id="upload-file" data-placement="vertical">
            ![](../../assets/icon/folder.png)
        </a>
        ![](../../assets/icon/record.png)
        <div id="smile-container" style="display:none; width: 480px;"></div>
    </div>
    <div id="at-container"><ul></ul></div>
    <div id="chat-input-area" contenteditable="true"></div>
    

    其中,at-container就是@弹出框的div,整个输入框采用的是div,然后设置contenteditable属性为true,这样就可以实现一个简单的输入框效果了,但是这还只是一个渣渣,啥都不能干,还得用现成的基于contenteditable的编辑器才行,我这里选择了wysiwygjs这个编辑器,虽然坑也挺多,但是用起来还可以。

    
    // 初始化输入框
    editor = wysiwyg({
        element: 'chat-input-area', // or: document.getElementById('chat-input-area')
        onKeyPress: function (key, character, shiftKey, altKey, ctrlKey, metaKey) {
                    
            // 只要有按键,就关闭@弹窗
            $('#at-container').css('display', 'none');
                    
            // 这里省略一坨代码
            if (character === '@') {
    
                // 获取当前光标的位置信息
                let offset = $('#chat-input-area').caret('offset');
                let position = $('#chat-input-area').caret('position');
                // console.log(offset);
                // console.log(position);
    
                let editorWidth = $('#chat-input-area').width();
                let atWidth = $('#at-container').width();
                // console.log('editor width = ' + editorWidth);
                // console.log('at div width = ' + atWidth);
                
                // 当右侧的空间不够显示@弹窗时,显示在左侧
                // 所以需要对右侧剩余空间的大小进行判断
                if (editorWidth - position.left < atWidth) {
                    $('#at-container').css('left', offset.left - atWidth);
                } else {
                    $('#at-container').css('left', offset.left + 20);
                }
                $('#at-container').css('top', offset.top - 185);
                $('#at-container').css('display', 'block');
                $('#at-container').css("position", "absolute");
                $('#at-container').scrollTop(0);
            }
        }
    });
    
    

    这样,就解决了弹窗的位置问题,其他问题也就出现了,比如说点击非弹窗区域自动关闭弹窗,按删除键删除@时,自动关闭弹窗,wysiwyg编辑器的onKeyPress事件大部分按键都能够监听,但是Backspace(或delete)键监听不到,原因没有细查,只能额外的去监听Backspace(或delete)键,然后关闭弹窗。

    
    // 按下Backspace(或delete)键时
    $('#chat-input-area').keyup(function (e) {
        if (process.platform === 'darwin') {
            if (e.keyCode == 8) {
                $('#at-container').css('display', 'none');
            }
        } else {
            if (e.keyCode == 46) {
                $('#at-container').css('display', 'none');
            }
        }
    });
    
    // 点击@窗体外的区域时,关闭@弹窗
    $(document).mouseup(function(e) { 
        var pop = $('#at-container');    
        if(!pop.is(e.target) && pop.has(e.target).length === 0) { 
            $('#at-container').css('display', 'none');
        }  
    }); 
    
    

    完成之后,整个弹窗的位置和显示问题基本上已经解决了,当然,@弹窗里面的内容加载以及布局都比较容易,这里就不再详述了。

    点击@弹窗里面的列表时,需要在下边的输入框中显示@了某人,这里就遇到了一个问题,一般来说,你@了某人,那这个 @+用户名 应该算作一个整体,一个块,删除的话需要整体删除,所以输入框中的内容不能简单的是 @+用户名 这种写法,需要做某种处理才行。
    可以参考下面这篇博文,讲的很清楚,遇到的各种问题都有对应的解决办法,就是有些复杂。

    js实现@提到好友

    由于我自己所用的环境是Electron,只是基于Chromium引擎,所以需不要考虑Firefox以及IE和Edge浏览器,瞬间感觉轻松了很多。

    当你输入了@之后,弹窗才会显示,这时@字符已经显示在输入框中了,而我们要做的就是当选择了@对象之后,需要在输入框中插入@的数据,这个数据作为一个整体,是可以整体被删除的,看了上面的那篇博文之后,你会知道,需要在输入框中插入<button contenteditable="false">@someone</button>这样的代码,contenteditable="false"可以保证button不会被编辑,即可以实现整体删除的效果,但是随之而来的是两个问题:

    1. 由于@已经输入了,需要被删除掉,否则会出现 @@someone这种情况
    2. document.execCommand('insertHTML', false, '<button contenteditable="false" onclick="return false;" class="at" >@${name}</button>')
      这种写法插入html之后,contenteditable="false"会被Chromium自动过滤掉,也就是最终插入的数据中是没有该属性的,这就会导致这个button中的内容可以被编辑,这可能是Chromium的一个bug吧

    针对上面的两个问题,有人会说,那我直接把@someone中的someone用button包裹起来不得了吗,这样就不会去删除@了,这样也是可以的,但是如果这样做的话,就会出现其他体验上的问题,比如说你删掉someone之后,只剩下了@,那@弹窗应该会立即弹出,要不然你就没办法再次触发弹窗了,只能将@手动删除,然后再输入,而且如果你@完之后,将输入框焦点点到@字符后面,要不要重新弹出呢,如果重新弹出的话,那之前已经@成功的那个呢,要不要取消掉呢,随之而来的是一系列的问题,倒不如直接将@someone作为一个整体,要删一起删,要留一起留,这样整体简单了很多,不用考虑太多的情况。

    针对第一种情况的解决方案:

    // @完之后自动删除@字符
    if (window.getSelection) {
            let range = window.getSelection();
            if (range.rangeCount > 0) {
                // let sel = range.getRangeAt(0);
                // let startOffset = sel.startOffset;
                let startOffset = range.extentOffset;
                // if (range.extentNode) {
                range.extentNode.replaceData(startOffset - 1, 1, '');
                // } else if(range.anchorNode) {
                // range.anchorNode.replaceData(startOffset - 1, 1, '');
                // }
            }
        }
    

    第二种情况的解决方案:

    // 插入html之后,找到该节点,添加contenteditable属性
    let html = `<button contenteditable="false" onclick='return false;' class="at" data-id="${id}">@${name}</button>`;
    document.execCommand('insertHTML', false, html)
    $(`#chat-input-area button[data-id="${id}"]`).attr('contenteditable', false)
    
    

    但是解决完这两个问题后,最头痛的问题又来了:@完之后,输入框焦点不见了,这个问题如果你看了上面提到的博客之后应该会有了解,由于我比较懒,也没有去下载他的demo来看,只是单纯的看博文,所以代码片段上有些断片,在尝试了他的方案之后,问题还是没有解决,怎么办,自己想办法吧。

    问题的原因就在于Chromium只会在文本中才会显示焦点,如果你输入框里面只输入纯文本,那没问题,插入了button,而且button还不能被编辑,能显示焦点就怪了,无论你怎么点,焦点都不会出现,除非你再输入纯文本,焦点才会出现。

    知道原因了,问题也就好解决了,在插入html的时候给它后面添加一个文本内容就好了,顺便在前面也添加一个,这样无论后退还是前进都可以显示焦点了,上完整代码:

    $('#at-container ul li a').unbind('click').click(function (e) {
        e.stopPropagation();
        let peer = $(this).data('peer');
        $('#at-container').css('display', 'none');
            if (window.getSelection) {
                let range = window.getSelection();
                if (range.rangeCount > 0) {
                    let sel = range.getRangeAt(0);
                    let startOffset = sel.startOffset;
                    range.anchorNode.replaceData(startOffset - 1, 1, '');
                }
            }
            let html = `<button contenteditable="false" onclick='return false;' class="at" data-peerid="${peer.peerid}">@${peer.name}</button>`;
            // 在插入的html前后分别插入一个 ‍ 防止焦点丢失的问题
            document.execCommand('insertHTML', false, '‍' + html + '‍')
            $(`#chat-input-area button[data-peerid="${peer.peerid}"]`).attr('contenteditable', false)
            $('#chat-input-area').focus();
        });
    
    

    今天就先写到这里吧,一个@功能竟然能够出现这么多的坑,而且还只是界面上的,后台功能实现上估计也不会少。

    希望本篇文章会对有同样需求的小伙伴们有所帮助!

    相关文章

      网友评论

          本文标题:JS实现@功能

          本文链接:https://www.haomeiwen.com/subject/jbppwttx.html