JS实现@功能


最近公司的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();
    });

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • 《裕语言》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 10...
    叶染柒丶阅读 23,982评论 5 18
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,577评论 25 707
  • 一个四岁孩子,家长发朋友圈说晚上把孩子揍了一顿,总算老老实实写拼音作业了,接着反思说毕竟才上幼儿园啊,要求事事应对...
    王明鹏阅读 261评论 0 2
  • 今天是星期四,晴天。 病魔缠身的日子快要到头了。今天是昨天坏习惯的改正的开始,也是发现自己做出自己的开始。今天晚上...
    吉一木阅读 100评论 0 0
  • 最近室友要离开,忍不住先在校友群发了招合租的邮件。于是被“夸”语言能力好以及萌……于是我默默的再把自己的邮件看了一...
    墨韵书香阅读 920评论 10 8