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

    html 复制代码
    <!-- 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)

    选中表情部分如下:

    js 复制代码
    // 点击选中 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

    html 复制代码
    <!-- 因为正常 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 相关事件

    js 复制代码
    // 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) {
            // IE
            text = 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);
        }
    }

    选中表情相关事件

    js 复制代码
    // 接收"选中 emoji 表情"事件
    handleCheckEmoji(val) {
        // 获取待插入表情 Node
        let 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 元素内部的实体字符为表情

    js 复制代码
    mounted() {
        // 监听 DOM 变化, 变化时使用 twemoji 库转化 emoji 实体字符为 twemoji emoji
        this.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 的那个单元格内容, 暂未解决

相关推荐
brief of gali2 分钟前
记录一个奇怪的前端布局现象
前端
前端拾光者41 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
Json_181790144801 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网1 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子02041 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing1 小时前
React核心功能详解(一)
前端·react.js·前端框架
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
深度混淆2 小时前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China2 小时前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q2 小时前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge