如果你筹备在Web中开发一个能够聊天互动的利用,那么一个反对表情符号的输入框很可能会是必备的内容项。但具体到Web环境来说,咱们晓得,表单元素<input>
和<textarea>
只能输出纯文本,这样的话,表情符号的反对具体要如何做呢?
让咱们从相熟的货色开始。
来自微博和微信的两种格调
下图是微信里的聊天:
下图是微博里的写微博:
综合以上微信和微博的表情输出设计,咱们能够看出有两种格调能够采纳:
- 一种是像微信这样,只用纯文本,通过相似
[旺柴]
这样的符号标识来代替表情,最初输入时再显示成真正的表情。 - 另一种是像微博这样,所见即所得,输入框自身就是表情和文字混合在一起。
看起来仿佛微博这种格调要简单一点,咱们就从微博的这种开始吧。
表情图和文字在一起的场景
对HTML来说,文字和图片放在一起是十分根底的能力。然而,咱们还要求它能够作为输入框来应用,这就须要用到HTML属性contenteditable
。它可能不太罕用,但其实是一个反对范畴很广,历史悠久的HTML属性。
应用contenteditable
,就能够失去这个简略却满足要求的输入框元素:
<div contenteditable="true"></div>
表情的显示
当初的输入框曾经是一个<div>
,所以,你能够用任意的HTML标签来显示表情,而其中最为罕用的就是图片<img>
。以后面的微博内容为例,它和输入框一起,应该形成像上面这样的HTML代码:
<div contenteditable="true"> 一条带表情<img src="/path/to/emoji/3.gif"><img src="/path/to/emoji/3.gif">的微博<img src="/path/to/emoji/9.gif"></div>
能够看出,插入表情理论就是插入一段HTML代码。HTML代码和残余的纯文本一起,独特形成带表情符号的输出内容。
表情输出性能的要点
接下来,咱们参照以下示例界面,来实现微博格调的输入框。
这个界面两头的横线,就是contenteditable
的<div>
输入框元素。联合这个界面,咱们能够剖析出接下来的两个实现要点:
- 点击下方的表情,就将该表情对应的HTML代码插入到输入框
<div>
。 - 表情HTML代码插入的地位要合乎输入框
<div>
的以后光标地位。
显然,只是第一个要点的话是很容易的,要害是第二个。
这第二个要点,就须要理解Selection和Range的概念了。
Selection 和 Range
在网页中,你肯定很相熟下图展现的两种状态:
- 一种是有一个一直闪动的光标,示意着以后正在输出或筹备输出的地位。它个别只呈现在网页的能够输出的元素内,比方文本输入框。
- 另一种是一部分内容出现蓝底白字(这个色彩能够批改,但默认是这个色彩)的状态,示意以后被选中。它能够呈现在任意的网页元素内,咱们也罕用来局部复制网页内容。
以上两种状态尽管表现形式不同,但它们在Web畛域都叫做Selection。Selection形容的正是网页中的“以后抉择”。闪动的光标也算作一种非凡的抉择,称为已折叠(Collapsed)的抉择。
Selection在JavaScript中对应的是Selection
对象,它能够通过window.getSelection()
或document.getSelection()
获取到。
任何时候,当网页中“以后抉择”产生扭转时,都会触发document.onselectionchange
事件。这个事件处理函数仅存在于document
。
Range的设计意义
Selection曾经示意了“以后抉择”,那Range是做什么的呢?简略来说,Range是Selection的“准备军”,它和Selection相似,都能够具体形容网页中的“抉择”状态,只是Selection是可见的,Range是不可见的。
通过Selection的和Range无关的办法,能够把Range利用到Selection,这时候就能够看到Range的抉择成果了。这就如同Selection代表了舞台,Range则是一个又一个幕后的演员,它们能够轮换上场和登场。
除Firefox外,其余浏览器的Selection都只反对单个Range,因而,咱们个别在同一时间只能利用一个Range到Selection。
一个Range由两个边界点组成,别离是起始边界点和结尾边界点。这两个边界点在一起,就能够形容任意的“抉择”状态。当两个边界点完全相同时,这个“抉择”状态就称为折叠的(Collapsed),也就是闪动光标的状态。
Selection的和Range无关的办法很重要,具体如下:
getRangeAt(i)
- 按索引获取Selection的以后Range。除Firefox外,其余浏览器只固定应用索引0
。addRange(range)
- 将range
利用到Selection。除Firefox外,如果Selection以后曾经有其余Range,将疏忽此办法调用。removeRange(range)
- 从Selection中勾销利用range
。removeAllRanges()
- 勾销利用所有Range。empty()
- 等同于removeAllRanges()
。
对于Selection和Range的更具体的介绍和阐明,举荐浏览这篇Selection And Range。
合乎光标地位的表情插入
理解了Selection和Range的基础知识后,咱们持续来实现微博格调的表情输出。后面说过,要点是表情HTML代码的插入地位要合乎输入框的光标地位,所以咱们首先要做的就是记录这个光标地位。
先标记输入框为inputBox
(本示例应用Vue):
<div ref="inputBox" class="input-box" contenteditable="true"></div>
而后应用后面提到的document.onselectionchange
监听抉择变动事件:
document.onselectionchange = () => { let selection = document.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); if (vmEmoji.$refs.inputBox.contains(range.commonAncestorContainer)) { rangeOfInputBox = range; } }};
这段代码的作用是,在“以后抉择”发生变化(鼠标点击或触摸动作等)后,如果变动后的Selection位于输入框inputBox
外部,就用变量rangeOfInputBox
保留它。这里也能够看到,Selection是用Range来保留的。
selection.rangeCount
是Selection
的属性,它示意Selection正在利用的Range数目。当它大于0
时,阐明以后是“有抉择”的状态。
range.commonAncestorContainer
是Range
的属性,它示意Range的两个边界点的间隔最近的独特父元素。这里用于判断Range产生在inputBox
内。
最初,当点击表情时,执行插入表情的办法insertEmoji
:
insertEmoji (name) { let emojiEl = document.createElement("img"); emojiEl.src = `${this.emoji.path}${name}${this.emoji.suffix}`; if (!rangeOfInputBox) { rangeOfInputBox = new Range(); rangeOfInputBox.selectNodeContents(this.$refs.inputBox); } if (rangeOfInputBox.collapsed) { rangeOfInputBox.insertNode(emojiEl); } else { rangeOfInputBox.deleteContents(); rangeOfInputBox.insertNode(emojiEl); } rangeOfInputBox.collapse(false);}
这段代码中,参数name
代表了不同表情,从而生成不同表情对应的不同HTML元素(都是<img>
)。
如果rangeOfInputBox
不存在,阐明还没有过任何产生在输入框内的抉择事件,此时就指定一个默认的Range。selectNodeContents(node)
是Range
的办法,将一个Range设定为选中整个node
元素内容。
insertNode(node)
是Range
的办法,能够将node
元素插入到Range的起始边界点。它是本示例的要害办法,用于实现表情HTML元素插入。这里须要对Range的状态做判断,如果Range是折叠的(闪动光标),直接插入表情元素,如果Range不是折叠的(选中了一部分输入框内容),就先删除选中的内容,再插入表情元素(相当于替换内容的成果)。deleteContent()
也是Range
的办法,能够将Range蕴含的内容从网页文档中删除。
结尾调用的collapse(toStart)
依然是Range
的办法,它能够将Range的两个边界点变成雷同的,也就是折叠的状态。如果参数toStart
为true
则取起始边界点的地位,如果为false
则是取结尾边界点。这里取的是结尾边界点,这样就如同是在插入一个表情后,主动将光标挪动到刚插入的表情元素前方,从而反对表情的间断输出。
到此,微博格调的表情输出就曾经实现了:
把输入框内的内容作为HTML代码(富文本),就能够提交给后盾,或者像图里这样简略展现在上方的聊天窗口内。
欠缺点击表情时的光标置位
这种文字和表情图混合在一起的格调还存在一个待欠缺的中央:如果点击文字,光标会正确定位到选中的文字后方,而点击表情图,就没有任何动作。这个光标置位的性能咱们能够手动补全。
为输入框减少click
事件处理:
<div ref="inputBox" @click="handleBoxClick" class="input-box" contenteditable="true"></div>
对应的handleBoxClick()
事件处理办法如下:
handleBoxClick (event) { let target = event.target; this.setCaretForEmoji(target);},setCaretForEmoji (target) { if (target.tagName.toLowerCase() === "img") { let range = new Range(); range.setStartBefore(target); range.collapse(true); document.getSelection().removeAllRanges(); document.getSelection().addRange(range); }},
setStartBefore(node)
是Range
的办法,能够设定边界起始点的地位到一个元素之前。这段代码整体来说就是,如果以后click
的是<img>
元素,就创立一个Range,设定它为折叠状态,地位在方才点击的表情图之前,而后利用这个Range到Selection,变成实在可见的抉择成果。
用纯文本符号来代替表情的场景
当初,咱们从新开始,来实现微信格调的表情输出。
后面说过,微信是应用相似[旺柴]
这样的符号标识来代替表情的格调。这种格调全副应用纯文本,因而,输入框会很容易实现,能够间接应用表单元素的文本输入框:
<input ref="formInput" @keydown="handleFormInputKeydown" class="form-input" type="text">
这里预留的handleFormInputKeydown()
输出事件处理办法,将在后文中应用。
和微博格调相似,接下来也是能够分成两个实现要点:
- 点击下方的表情,就将该表情对应的纯文本符号插入到输入框
<input>
。 - 纯文本符号的插入地位要合乎输入框
<input>
的以后光标地位。
尽管同样是联合Selection和Range的概念,按光标地位来插入纯文本符号,但<input>
会更加简略。
按光标地位来插入纯文本
表单元素<input>
本身有以下3个属性是对于“抉择”的:
input.selectionStart
- 抉择的起始地位。它的值是一个索引数字,比方6
。input.selectionEnd
- 抉择的结尾地位。值的格局同上。input.selectionDirection
- 抉择的方向。可选值"forward"
,"backward"
和"none"
。个别对应的状况是指鼠标拖拽抉择时是从前向后,还是从后向前,又或者是双击选中。
通过这些属性,就能够实现对“抉择”状态的读取和写入,而无需应用Selection
和Range
。
当初,点击表情时,执行插入表情的办法insertEmojiText
:
insertEmojiText (name) { let input = this.$refs.formInput; let emojiText = `[${name}]`; input.focus(); input.setRangeText(emojiText, input.selectionStart, input.selectionEnd, "end"); input.blur();}
能够看到纯文本的表情插入非常简单。这里也是用[name]
的符号来示意表情。
input.setRangeText(replacement, [start], [end], [selectionMode])
是input
的办法,能够将索引地位从start
到end
的文本,替换成replacement
的文本。而如果start
等于end
,就相当于闪动光标的状态,没有文本会被替换,变成了插入文本的成果。开端参数selectionMode
决定了在文本替换(或插入)操作结束后,input
如何更新抉择状态。这里取"end"
示意将抉择状态设定为“闪动光标,地位在新插入文本的前方”,从而反对表情间断输出。
应用input.setRangeText()
,无论以后状态是闪动光标,还是曾经抉择了一些文本,都会以合乎咱们输出习惯的形式插入表情文本。
对于input.setRangeText()
的更具体的阐明,同样举荐浏览这篇Selection And Range。
这段代码中的input.focus()
和input.blur()
,是因为仅在<input>
元素被focus的状况下进行文本编辑操作,能力确保input.selectionStart
和input.selectionEnd
两个值正确更新。同时,这里又并不心愿<input>
元素被真地focus,所以又用了input.blur()
来勾销。
到这里,微信格调的表情输出就根本可用了。然而,这种纯文本符号的格调也有一个应欠缺的中央:用退格键(Backspace)来删除文本时,代表一个表情的纯文本符号应该以作为一个整体被删除。比方[旺柴]
这样的表情符号,在光标位于]
的前方时,一个退格键就应该删除这一整段文本。这也是微信里存在的性能。
退格键反对 - 以表情符号为整体删除文本
前文示例中为<input>
元素预留的handleFormInputKeydown()
办法,就是用于实现这一性能:
handleFormInputKeydown (event) { let input = this.$refs.formInput; let chatString = input.value; // "Backspace" and selection type "Caret" if (event.keyCode === 8 && input.selectionStart === input.selectionEnd) { let indexEnd = input.selectionStart - 1; let charToDelete = chatString.charAt(indexEnd); // delete the whole [***] if (charToDelete === "]") { event.preventDefault(); let indexStart = chatString.lastIndexOf("[", indexEnd); input.setRangeText("", indexStart, indexEnd + 1, "end"); } }}
这段代码是判断当抉择状态为闪动光标,且刚好位于字符]
后按下了退格键的时候,就找出整个[name]
表情文本,应用input.setRangeText()
实现整段删除。
到此,微信格调的表情输出也就实现了:
在提交给后盾或者图中这样展现在上方聊天窗口内的时候,取输入框内的纯文本,而后将所有[name]
格局的文本符号,替换成对应表情的HTML(比方[1]
变成<img src="/path/to/emoji/1.gif">
)即可。
残缺代码示例
两种格调的残缺代码示例:
- 微博格调(表情图和文字一起)
- 微信格调(表情用纯文本符号代替)
补充
光标色彩
Selection在可输出元素内的折叠状态,也就是闪动光标,它的色彩也是能够批改的,比方:
input { caret-color: red;}
会将闪动光标批改为红色。更具体的阐明请查看MDN上的caret-color。
输入法里的表情字符
在手机上,你可能留神到像搜狗这样的输入法也给你提供了一套表情(上图中的Emoji),它们在微信中也能够应用,而且能够间接显示在微信的输入框内。这种不依赖其余货色就能够应用的表情,实质上是Unicode字符,你能够到Unicode Character Table上查找更多的表情字符。
Unicode字符表情最终出现的样子取决于它所处的环境。比方不同手机,不同操作系统,都可能有不同的外观。
定义虚构键盘的动作键
手机上的输入法键盘,右下角的动作键能够通过HTML属性enterkeyhint
设置为不同的类型:
<div ref="inputBox" enterkeyhint="send" contenteditable="true"></div>
这里值send
对应的就是后面图中的“发送”。其余可用的值能够参考MDN上的enterkeyhint。
如果想要像微信那样,点击虚构键盘右下角的“发送”就能够发送音讯(而不是点击网页上的按钮),监听输出元素的键盘事件,并确认按键为enter
键即可。
结语
“能够输出表情”对于聊天交换而言能够说是十分棒的一项加强。不论具体用哪一种格调实现,最终都是让大家能够表白出更多。
心愿本文的表情性能开发指南能够帮到你。
(从新编辑自我的博客,原文地址:http://acgtofe.com/posts/2021...)