在 Web 中实现表情符号的输入

2023-11-06 01:30
文章标签 实现 输入 web 表情符号

本文主要是介绍在 Web 中实现表情符号的输入,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

关注公众号 前端开发博客,回复“加群”

加入我们一起学习,天天进步

作者:EdwardUp

来源:SegmentFault 思否社区


如果你准备在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,变成真实可见的选择效果。


用纯文本符号来替代表情的场景

现在,我们重新开始,来实现微信风格的表情输入。

前面说过,微信是使用类似[旺柴]这样的符号标识来替代表情的风格。这种风格全部使用纯文本,因此,输入框会很容易实现,可以直接使用表单元素的文本输入框:

<inputref="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">)即可。


完整代码示例

两种风格的完整代码示例:

  • 微博风格(表情图和文字一起)

  • https://codesandbox.io/s/emoji-input-contenteditable-75qe8

  • 微信风格(表情用纯文本符号替代)

  • https://codesandbox.io/s/emoji-input-text-yqfe3



补充

光标颜色

Selection在可输入元素内的折叠状态,也就是闪烁光标,它的颜色也是可以修改的,比如:

input {caret-color: red;
}

会将闪烁光标修改为红色。更详细的说明请查看MDN上的caret-color。

输入法里的表情字符

在手机上,你可能注意到像搜狗这样的输入法也给你提供了一套表情(上图中的Emoji),它们在微信中也可以使用,而且可以直接显示在微信的输入框内。这种不依赖其他东西就可以使用的表情,本质上是Unicode字符,你可以到Unicode Character Table上查找更多的表情字符。

Unicode字符表情最终呈现的样子取决于它所处的环境。比如不同手机,不同操作系统,都可能有不同的外观。

定义虚拟键盘的动作键

手机上的输入法键盘,右下角的动作键可以通过HTML属性enterkeyhint设置为不同的类型:

<divref="inputBox" enterkeyhint="send"contenteditable="true"></div>

这里值send对应的就是前面图中的“发送”。其他可用的值可以参考MDN上的enterkeyhint。

如果想要像微信那样,点击虚拟键盘右下角的“发送”就可以发送消息(而不是点击网页上的按钮),监听输入元素的键盘事件,并确认按键为enter键即可。


结语

“可以输入表情”对于聊天交流而言可以说是非常棒的一项增强。不管具体用哪一种风格实现,最终都是让大家可以表达出更多。

希望本文的表情功能开发指南可以帮到你。

- EOF -

推荐阅读  点击标题可跳转

Chrome 开发者工具各种骚技巧

这是我面试42场拿下微软、蚂蚁、头条offer的方法论

工作两年前端经验分享

觉得本文对你有帮助?请分享给更多人

推荐关注「前端开发博客」,提升前端技能

如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

这篇关于在 Web 中实现表情符号的输入的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/353717

相关文章

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

PyCharm中配置PyQt的实现步骤

《PyCharm中配置PyQt的实现步骤》PyCharm是JetBrains推出的一款强大的PythonIDE,结合PyQt可以进行pythion高效开发桌面GUI应用程序,本文就来介绍一下PyCha... 目录1. 安装China编程PyQt1.PyQt 核心组件2. 基础 PyQt 应用程序结构3. 使用 Q

Python实现批量提取BLF文件时间戳

《Python实现批量提取BLF文件时间戳》BLF(BinaryLoggingFormat)作为Vector公司推出的CAN总线数据记录格式,被广泛用于存储车辆通信数据,本文将使用Python轻松提取... 目录一、为什么需要批量处理 BLF 文件二、核心代码解析:从文件遍历到数据导出1. 环境准备与依赖库