关于javascript:在Web中实现表情符号的输入

47次阅读

共计 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.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…)

正文完
 0