如果你筹备在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.rangeCountSelection的属性,它示意Selection正在利用的Range数目。当它大于0时,阐明以后是“有抉择”的状态。

range.commonAncestorContainerRange的属性,它示意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的两个边界点变成雷同的,也就是折叠的状态。如果参数toStarttrue则取起始边界点的地位,如果为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"。个别对应的状况是指鼠标拖拽抉择时是从前向后,还是从后向前,又或者是双击选中。

通过这些属性,就能够实现对“抉择”状态的读取和写入,而无需应用SelectionRange

当初,点击表情时,执行插入表情的办法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的办法,能够将索引地位从startend的文本,替换成replacement的文本。而如果start等于end,就相当于闪动光标的状态,没有文本会被替换,变成了插入文本的成果。开端参数selectionMode决定了在文本替换(或插入)操作结束后,input如何更新抉择状态。这里取"end"示意将抉择状态设定为“闪动光标,地位在新插入文本的前方”,从而反对表情间断输出。

应用input.setRangeText(),无论以后状态是闪动光标,还是曾经抉择了一些文本,都会以合乎咱们输出习惯的形式插入表情文本。

对于input.setRangeText()的更具体的阐明,同样举荐浏览这篇Selection And Range。

这段代码中的input.focus()input.blur(),是因为仅在<input>元素被focus的状况下进行文本编辑操作,能力确保input.selectionStartinput.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...)