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 的那个单元格内容, 暂未解决

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js