当前位置: 首页 > news >正文

HTML 跨平台使用同一套 emoji (twemoji) + 实现 emoji 选择

背景:

网页需要显示和发送带 emoji 表情的文本消息(为方便理解, 以 whatsapp 为例, 实际开发中待定)
同时, 要求不同系统打开网页时, 看到的都是同一套 emoji , 避免同一个 emoji 在不同电脑上显示不同

概述:

  1. 引入 twemoji 库文件
  2. 把网页版 wa 的 emoji 全部复制下来
  3. 新增 emoji 组件, 点击表情图标弹出表情框, 框内显示与 wa 一致
  4. 点选框中表情, 根据点击前光标在输入框(contentEditable 的 div)的位置, 插入 twemoji.parse 转换过的表情(图片)
  5. 给各处可能显示 twemoji 的 div 加上特定 class(比如 twemoji-convert), 在程序主界面(Main.vue)新增 MutationObserver , 在 DOM 变化时选取此类 class 元素, 使用 twemoji.parse 转换元素, 使显示 emoji

实现过程:

  1. 引入 twemoji

    <!-- Start twemoji 库文件 -->
    <script src="https://twemoji.maxcdn.com/v/13.1.0/twemoji.min.js" integrity="sha384-gPMUf7aEYa6qc3MgqTrigJqf4gzeO6v11iPCKv+AP2S4iWRWCoWyiR+Z7rWHM/hU" crossorigin="anonymous"></script>
    <!-- End twemoji 库文件 -->
    
  2. 复制 wa emoji

    在网页版 whatsapp 上聊天, 一栏栏地点选表情, 发送, 复制下来, 此时接收到的内容已经是字符了, 把这些字符按顺序提取为数组;

    这个需要耐心, 这些个字符千奇百怪, 有的字符电脑系统不支持不能渲染出来, 有的字符后面需要接一个空格, 有的字符看上去只有一位但实际占了多位, 最多的还是由多种字符组合显示成一个表情的(字符人, 可加修饰字符: 性别, 发型, 职业, with another one …), 千万别弄错了

  3. 新增 emoji 组件

    渲染表情部分由全局的 MutationObserver 负责(twemoji.parse)

    选中表情部分如下:

    // 点击选中 emoji
    handleClickEmoji(e) {// 取选中的 emoji DOM 标签let emojiImg;if (e.target.classList.contains('emoji-item')) {emojiImg = e.target.querySelector('img.emoji');} else if (e.target.classList.contains('emoji')) {emojiImg = e.target;}// 取标签上的 alt (实体字符, twemoji 转换后自带)传给外部if (emojiImg) {this.$emit('checkEmoji', emojiImg.getAttribute('alt'));}
    }
    
  4. 输入框接收选中表情, 加入到输入框中

    输入框 div

    <!-- 因为正常 textarea 无法显示 emoji img , 现在将输入框改为 contentEditable div -->
    <div :contentEditable="true"ref="sendMsg"@click="save_range"@keyup="save_range"@keydown="inputOnKeyDown"@paste="handlePaste":placeholder="$t('chat.inputbox')":class="{'waInputDiv__disabled': inputDisabled}"class="waInputDiv"></div>
    

    输入框 div 相关事件

    // inputOnKeyDown 处理回车, ctrl 等事件, 与表情主逻辑无关, 略过// 离开焦点时先保存状态(光标等信息)
    save_range() {let range = null;if (window.getSelection) {const sel = window.getSelection();if (sel.getRangeAt && sel.rangeCount) {range = sel.getRangeAt(0);}} else if (document.selection && document.selection.createRange) {range = document.selection.createRange();}this.lastEditRange = range;
    }// 粘贴内容到可编辑 div (参考 https://www.zhangxinxu.com/wordpress/2016/01/contenteditable-plaintext-only/)
    handlePaste(e) {e.preventDefault();let text;if (window.clipboardData && window.clipboardData.setData) {// IEtext = window.clipboardData.getData('text');} else {text = (e.originalEvent || e).clipboardData.getData('text/plain');}if (document.body.createTextRange) {let textRange;if (document.selection) {textRange = document.selection.createRange();} else if (window.getSelection) {const sel = window.getSelection();const range = sel.getRangeAt(0);// 创建临时元素,使得TextRange可以移动到正确的位置const tempEl = document.createElement('span');tempEl.innerHTML = '&#FEFF;';range.deleteContents();range.insertNode(tempEl);textRange = document.body.createTextRange();textRange.moveToElementText(tempEl);tempEl.parentNode.removeChild(tempEl);}textRange.text = text;textRange.collapse(false);textRange.select();} else {// Chrome之类浏览器document.execCommand('insertText', false, text);}
    }
    

    选中表情相关事件

    // 接收"选中 emoji 表情"事件
    handleCheckEmoji(val) {// 获取待插入表情 Nodelet dom_insert = document.createElement('span');dom_insert.innerHTML = twemoji.parse(val);dom_insert = dom_insert.childNodes[0];// 插入 Node 到输入框this.insertInputMsg(dom_insert);
    }// 插入 emoji 表情到输入框
    insertInputMsg(val) {// 获取待插入结点let dom_insert;if (val instanceof Node) {// 是 Node 结点, 不用做处理dom_insert = val;} else {// 否则当做文本结点处理dom_insert = document.createTextNode(String(val || ''));}// 获取编辑框对象const dom_input = this.$refs.sendMsg;// 编辑框设置焦点dom_input.focus();// 获取选定对象let selection = null;if (window.getSelection) {selection = window.getSelection();} else if (window.document.getSelection) {selection = window.document.getSelection();} else if (window.document.selection) {selection = window.document.selection.createRange().text;}// 如果获取不到, 退出流程if (!selection) {this.$Message.error(this.$t('whatsapp_manage.browserError'));return false;}// 判断是否有最后光标对象存在if (this.lastEditRange) {// 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态selection.removeAllRanges();selection.addRange(this.lastEditRange);}// 根据所在位置的不同以不同的方式插入结点if (this.lastEditRange) {// 有光标对象, 直接插入this.lastEditRange.insertNode(dom_insert);} else if (selection.anchorNode == dom_input) {// 焦点就在文本框, 则直接 append node 到最后dom_input.appendChild(dom_insert);} else if (selection.anchorNode.nodeName != '#text') {// 焦点在非文本结点, 则插入到焦点节点后面dom_input.insertBefore(dom_insert, selection.anchorNode.nextSibling);}// 创建新的光标对象const range = document.createRange();// 光标对象的范围界定为新建的内容节点range.setStartAfter(dom_insert);// 插入空格, 否则光标可能不显示// dom_input.insertBefore(document.createTextNode(' '), dom_insert.nextSibling);// range.setStart(dom_insert.nextSibling, 1);// 使光标开始和光标结束重叠range.collapse(true);// 清除选定对象的所有光标对象selection.removeAllRanges();// 插入新的光标对象selection.addRange(range);// 无论如何都要记录最后光标对象this.lastEditRange = selection.getRangeAt(0);
    }
    
  5. 主界面监听 DOM 变动, twemoji.parse 转化指定 class 元素内部的实体字符为表情

    mounted() {// 监听 DOM 变化, 变化时使用 twemoji 库转化 emoji 实体字符为 twemoji emojithis.observer = new MutationObserver(function(mutations, observe) {const domList = document.querySelectorAll('.twemoji-convert');for (let i = 0; i < domList.length; i++) {twemoji.parse(domList[i]);}});this.observer.observe(document.body, {'childList': true,'characterData': true,'subtree': true});
    }
    

补充:

配合 Vue 使用时, 表情和 emoji 混杂的文本, 使用 twemoji.parse 后会破坏 vue 的响应式监听, 导致视图不随数据的更新而更新; 解决方法 — 给需要更新的地方加上 key , key 上绑定原数据, 这样, 当原数据变化时, 组件会重新渲染
另外, 如果使用虚拟滚动表格, 表格内有 emoji 文字, 仍然有问题, 滚动后其他纯文字单元格也显示成了带 emoji 的那个单元格内容, 暂未解决

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Lc62---3024.三角形类型(排序)--java版
  • create-vue项目的README中文版
  • 使用 Python 进行马尔可夫链职业路径建模
  • 数据结构【有头双向链表】
  • WinUI vs WPF vs WinForms: 三大Windows UI框架对比
  • Windows FreeCAD 导入ODA File Converter 插件
  • 分布式智能:Mojo模型在分布式系统中的动态使用策略
  • 【java】BIO,NIO,多路IO复用,AIO
  • 强化学习笔记
  • 视觉机械臂抓取——流程总览
  • 如何在测试中保护用户隐私!
  • Golang | Leetcode Golang题解之第300题最长递增子序列
  • Github2024-07-29 开源项目周报Top15
  • easyui 点击单元格的时候,获取该行另外一个字段的值
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • 2017 前端面试准备 - 收藏集 - 掘金
  • Angular2开发踩坑系列-生产环境编译
  • CentOS7简单部署NFS
  • Git同步原始仓库到Fork仓库中
  • Linux后台研发超实用命令总结
  • Objective-C 中关联引用的概念
  • 闭包--闭包之tab栏切换(四)
  • 前端性能优化--懒加载和预加载
  • 如何编写一个可升级的智能合约
  • 提升用户体验的利器——使用Vue-Occupy实现占位效果
  • Play Store发现SimBad恶意软件,1.5亿Android用户成受害者 ...
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • 扩展资源服务器解决oauth2 性能瓶颈
  • #162 (Div. 2)
  • #考研#计算机文化知识1(局域网及网络互联)
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • (03)光刻——半导体电路的绘制
  • (1)Jupyter Notebook 下载及安装
  • (16)Reactor的测试——响应式Spring的道法术器
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (Java数据结构)ArrayList
  • (Oracle)SQL优化基础(三):看懂执行计划顺序
  • (Redis使用系列) Springboot 在redis中使用BloomFilter布隆过滤器机制 六
  • (附源码)ssm基于jsp的在线点餐系统 毕业设计 111016
  • (接上一篇)前端弄一个变量实现点击次数在前端页面实时更新
  • (三)uboot源码分析
  • (原创)可支持最大高度的NestedScrollView
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • *_zh_CN.properties 国际化资源文件 struts 防乱码等
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .NET 4.0中的泛型协变和反变
  • .net core使用ef 6
  • .NET Standard / dotnet-core / net472 —— .NET 究竟应该如何大小写?
  • .Net 访问电子邮箱-LumiSoft.Net,好用
  • .net 托管代码与非托管代码
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .NET开源纪元:穿越封闭的迷雾,拥抱开放的星辰
  • .NET连接数据库方式
  • @Builder注释导致@RequestBody的前端json反序列化失败,HTTP400