共计 7710 个字符,预计需要花费 20 分钟才能阅读完成。
如果你筹备在 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…)