最近公司的PC端即時通訊工具需要添加@功能莉测,整體軟件采用的是Electron+Node.js來編寫的熙涤,其實功能并不難惋砂,困難的是前段界面上的體驗,說白了就是怎樣利用js來實現(xiàn)@功能厢洞。
為了做這個也是踩了不少的坑笛匙,目前來說界面已經(jīng)做得差不多了吧,也算能用犀变,還是總結(jié)一下吧,對別人來講秋柄,總歸是有點用的获枝。
整個界面的CSS樣式還是很好寫的,這里就不再詳述了骇笔,首先遇到的一個問題就是輸入@的時候彈出一個@列表省店,像圖一那樣的,雖然樣式丑了些笨触,但是功能算是實現(xiàn)了懦傍。
彈出一個窗口還是很簡單的,無非就是一個div的顯示隱藏嗎芦劣,但最主要的是彈出div的位置粗俱,要緊挨著@字符,也就是如何計算@字符的位置虚吟,這是遇到的第一個難題寸认。
通過萬能的Google签财,我發(fā)現(xiàn)了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這個玩意兒偏塞,一看名字就能夠想到這肯定就是專門為@功能做的一個插件唱蒸,事實也確實是這樣的,這個庫很強大灸叼,基本上幾行代碼就實現(xiàn)了一個@功能神汹,但是我并沒有直接用這個庫,而是只用了Caret.js這個庫來進行定位古今,為啥不直接用At.js呢屁魏,簡單粗暴,省時省力沧卢。
說實話蚁堤,我確實試了試,結(jié)果不容樂觀但狭,出現(xiàn)了幾個錯誤披诗,在簡單嘗試解決未果之后,果斷決定自己做一個立磁,然后就是呈队,自己走的路,跪著也要走完唱歧。宪摧。。欲哭無淚
好了颅崩,不多說了几于,上代碼
<!-- 整個聊天界面的布局 -->
<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沿彭,然后設(shè)置contenteditable
屬性為true,這樣就可以實現(xiàn)一個簡單的輸入框效果了尖滚,但是這還只是一個渣渣喉刘,啥都不能干,還得用現(xiàn)成的基于contenteditable的編輯器才行漆弄,我這里選擇了wysiwygjs這個編輯器睦裳,雖然坑也挺多,但是用起來還可以撼唾。
// 初始化輸入框
editor = wysiwyg({
element: 'chat-input-area', // or: document.getElementById('chat-input-area')
onKeyPress: function (key, character, shiftKey, altKey, ctrlKey, metaKey) {
// 只要有按鍵廉邑,就關(guān)閉@彈窗
$('#at-container').css('display', 'none');
// 這里省略一坨代碼
if (character === '@') {
// 獲取當(dāng)前光標(biāo)的位置信息
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);
// 當(dāng)右側(cè)的空間不夠顯示@彈窗時,顯示在左側(cè)
// 所以需要對右側(cè)剩余空間的大小進行判斷
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);
}
}
});
這樣,就解決了彈窗的位置問題鬓催,其他問題也就出現(xiàn)了肺素,比如說點擊非彈窗區(qū)域自動關(guān)閉彈窗,按刪除鍵刪除@時宇驾,自動關(guān)閉彈窗倍靡,wysiwyg編輯器的onKeyPress事件大部分按鍵都能夠監(jiān)聽,但是Backspace(或delete)鍵監(jiān)聽不到课舍,原因沒有細查塌西,只能額外的去監(jiān)聽Backspace(或delete)鍵,然后關(guān)閉彈窗筝尾。
// 按下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');
}
}
});
// 點擊@窗體外的區(qū)域時捡需,關(guān)閉@彈窗
$(document).mouseup(function(e) {
var pop = $('#at-container');
if(!pop.is(e.target) && pop.has(e.target).length === 0) {
$('#at-container').css('display', 'none');
}
});
完成之后,整個彈窗的位置和顯示問題基本上已經(jīng)解決了筹淫,當(dāng)然站辉,@彈窗里面的內(nèi)容加載以及布局都比較容易,這里就不再詳述了损姜。
點擊@彈窗里面的列表時饰剥,需要在下邊的輸入框中顯示@了某人,這里就遇到了一個問題摧阅,一般來說汰蓉,你@了某人,那這個 @+用戶名
應(yīng)該算作一個整體棒卷,一個塊顾孽,刪除的話需要整體刪除,所以輸入框中的內(nèi)容不能簡單的是 @+用戶名 這種寫法比规,需要做某種處理才行若厚。
可以參考下面這篇博文,講的很清楚蜒什,遇到的各種問題都有對應(yīng)的解決辦法盹沈,就是有些復(fù)雜。
由于我自己所用的環(huán)境是Electron吃谣,只是基于Chromium引擎,所以需不要考慮Firefox以及IE和Edge瀏覽器做裙,瞬間感覺輕松了很多岗憋。
當(dāng)你輸入了@之后,彈窗才會顯示锚贱,這時@字符已經(jīng)顯示在輸入框中了仔戈,而我們要做的就是當(dāng)選擇了@對象之后,需要在輸入框中插入@的數(shù)據(jù),這個數(shù)據(jù)作為一個整體监徘,是可以整體被刪除的晋修,看了上面的那篇博文之后,你會知道凰盔,需要在輸入框中插入<button contenteditable="false">@someone</button>
這樣的代碼墓卦,contenteditable="false"可以保證button不會被編輯,即可以實現(xiàn)整體刪除的效果户敬,但是隨之而來的是兩個問題:
- 由于@已經(jīng)輸入了落剪,需要被刪除掉,否則會出現(xiàn)
@@someone
這種情況 - document.execCommand('insertHTML', false, '<button contenteditable="false" onclick="return false;" class="at" >@${name}</button>')
這種寫法插入html之后尿庐,contenteditable="false"
會被Chromium自動過濾掉忠怖,也就是最終插入的數(shù)據(jù)中是沒有該屬性的,這就會導(dǎo)致這個button中的內(nèi)容可以被編輯抄瑟,這可能是Chromium的一個bug吧
針對上面的兩個問題凡泣,有人會說,那我直接把@someone中的someone用button包裹起來不得了嗎皮假,這樣就不會去刪除@了鞋拟,這樣也是可以的,但是如果這樣做的話钞翔,就會出現(xiàn)其他體驗上的問題严卖,比如說你刪掉someone之后,只剩下了@布轿,那@彈窗應(yīng)該會立即彈出哮笆,要不然你就沒辦法再次觸發(fā)彈窗了,只能將@手動刪除汰扭,然后再輸入稠肘,而且如果你@完之后,將輸入框焦點點到@字符后面萝毛,要不要重新彈出呢项阴,如果重新彈出的話,那之前已經(jīng)@成功的那個呢笆包,要不要取消掉呢环揽,隨之而來的是一系列的問題,倒不如直接將@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之后粥谬,找到該節(jié)點,添加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)
但是解決完這兩個問題后辫塌,最頭痛的問題又來了:@完之后漏策,輸入框焦點不見了,這個問題如果你看了上面提到的博客之后應(yīng)該會有了解臼氨,由于我比較懶掺喻,也沒有去下載他的demo來看,只是單純的看博文一也,所以代碼片段上有些斷片巢寡,在嘗試了他的方案之后,問題還是沒有解決椰苟,怎么辦抑月,自己想辦法吧。
問題的原因就在于Chromium只會在文本中才會顯示焦點舆蝴,如果你輸入框里面只輸入純文本谦絮,那沒問題,插入了button洁仗,而且button還不能被編輯层皱,能顯示焦點就怪了,無論你怎么點赠潦,焦點都不會出現(xiàn)叫胖,除非你再輸入純文本,焦點才會出現(xiàn)她奥。
知道原因了瓮增,問題也就好解決了,在插入html的時候給它后面添加一個文本內(nèi)容就好了哩俭,順便在前面也添加一個绷跑,這樣無論后退還是前進都可以顯示焦點了,上完整代碼:
$('#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();
});
今天就先寫到這里吧凡资,一個@功能竟然能夠出現(xiàn)這么多的坑砸捏,而且還只是界面上的,后臺功能實現(xiàn)上估計也不會少隙赁。
希望本篇文章會對有同樣需求的小伙伴們有所幫助垦藏!