赞
踩
之前做项目实现聊天功能,有几个功能点我觉得挺复杂的。今天我来说一下,我是如何实现图片小表情在输入框中显示,发送给后端时只发送一个含义字符串如:[emoji],然后正常回显在页面上。
此demo使用vue3
实现效果图:
输入自定义表情发送并回显
声明:这只是个demo,不涉及与后端交互,不过会在该交互的地方标记,如需实际应用于项目,请根据实际情况进行改造完善!
父组件dom定义如下,其中,输入框需要使用开启contenteditable
的div,不能使用input或者textarea。chatMsgEl为子组件,用来回显我们发送的消息结果。
<div id="app">
<div class="msg-box">
<!-- 消息输入框 -->
<div class="msg-input" ref="msgInput" contenteditable @blur="getAfterBlurIndex" @keydown.enter.prevent="sendMsg"></div>
<!-- 表情列表 -->
<div class="emoji-list">
<p>表情列表</p>
<img v-for="(emoji, key) in emojiList" :key="key" :src="emoji" @click="selectEmoji(key)" />
</div>
<button @click="sendMsg">发送</button>
</div>
<p>数据本体:{{ chatMsgRecord }}</p>
<!-- 消息回显框 -->
<chatMsgEl :msg="chatMsgRecord" :emojiList="emojiList"></chatMsgEl>
</div>
定义表情包列表,我这里只有一个本地图片,根据这个格式本地添加或者后端返回都行
const emojiList = {
"[vueLogo]": require("./assets/logo.png"),
};
然后定义几个变量,用来后续操作
let msgInput = ref(); // 输入框ref绑定
let chatMsgRecord = ref(""); // 发送的消息体
let focusNode = reactive({}); // 存储光标聚焦节点
let focusOffset = ref(0); // 存储光标聚焦偏移量
let chatInputOffset = reactive({}); // 存储光标聚焦的元素
想通过点击图片插入到输入框光标对应位置,最重要的就是在输入框失焦时获取到失焦前的光标位置,函数如下,具体不展开讲,不明白的话可以去搜一下关于getSelection的知识。
// 聊天输入框失焦时获取失焦前的光标位置
function getAfterBlurIndex() {
if (window.getSelection) {
let sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
focusNode = sel.focusNode;
focusOffset.value = sel.focusOffset;
chatInputOffset = sel.getRangeAt(0);
console.log("focusNode:", focusNode);
console.log("focusOffset:", focusOffset.value);
console.log("chatInputOffset:", chatInputOffset);
}
}
}
然后我们点击表情添加到输入框中,这个步骤分为:先获取输入框焦点(如果输入框从来都没有获取过焦点的话),然后创建图片标签追加到输入框div的子节点中(图片标签中的alt是后面发送时需要取出的表情字符串定义),最后输入框聚焦并将光标后移
// 获取输入框选取 function getInputSelection() { let sel = window.getSelection(); // 插入内容之前输入框是否已经聚焦获得选区 if ( !chatInputOffset.endContainer || (chatInputOffset.endContainer.className != "msg-input" && chatInputOffset.endContainer.parentNode.className != "msg-input" && chatInputOffset.endContainer.parentNode.parentNode.className != "msg-input") ) { chatInputOffset = document.createRange(); //用于设置 Range,使其包含一个 Node的内容。 chatInputOffset.selectNodeContents(document.querySelector(".msg-input")); //将包含着的这段内容的光标设置到最后去,true 折叠到 Range 的 start 节点,false 折叠到 end 节点。如果省略,则默认为 false . chatInputOffset.collapse(false); sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(chatInputOffset); } } // 点击选择表情 function selectEmoji(emoji) { getInputSelection(); // 先获得一下输入框焦点 // 创建图片元素,回显到输入框内 const img = `<img src="${emojiList[emoji]}" alt='${emoji}' class="emoji" style="width: 36px;height: 36px;object-fit: contain" >`; chatInputOffset.collapse(false); //光标移至最后 // 创建节点并插入 const node = chatInputOffset.createContextualFragment(img); let c = node.lastChild; chatInputOffset.insertNode(node); if (c) { chatInputOffset.setEndAfter(c); chatInputOffset.setStartAfter(c); } let j = window.getSelection(); j.removeAllRanges(); j.addRange(chatInputOffset); }
这个时候就可以发送了,发送的步骤为:先清空发送的上一条消息,然后循环输入框子节点,查找追加进去的图片标签,取出标签上的alt赋值给chatMsgRecord变量,如果是普通文本消息就直接追加赋值。最后清空输入框中的内容即可
function sendMsg() { console.log("msgInput:", msgInput); chatMsgRecord.value = '' // 先清空一下旧消息 msgInput.value.childNodes.forEach((element) => { console.log(element); // 如果是emoji表情图片的话,则转义 if (element.nodeName === "IMG" && element.className === "emoji") { chatMsgRecord.value += element.alt; } else { chatMsgRecord.value += element.wholeText; } }); // 清空输入框中的内容 msgInput.value.innerHTML = ""; msgInput.value.innerText = ""; //在这里使用websocket把数据chatMsgRecord发给后端 // socket.send({msg:chatMsgRecord}) }
子组件这边的话就比较简单了,先定义好dom结构,下面讲为什么循环的消息变量需要使用split(/(<[^>]+>)/g)分割
<template v-for="text in chatMsg.split(/(<[^>]+>)/g)">
<template v-if="!text.startsWith('<emoji')">{{ text }}</template>
<img v-else :src="imgSrc(text)[1]" class="emoji" />
</template>```
拿到消息体,父组件把表情包定义传递给子组件,然后循环表情包列表,通过replace方法匹配到相对应的表情赋值给消息结果。假设此时的消息体为:文本[emoji]文本,则消息结果为:文本<emoji src=“http://xxx”>文本。
const props = defineProps(["msg", "emojiList"]); // 注意:msg为后端返回给你的消息内容 const { msg, emojiList } = toRefs(props); let chatMsg = ref(msg.value) watch( msg, () => { // 核心语句 for (const key in emojiList.value) { const reg = new RegExp("(\\" + key + ")", "g"); chatMsg.value = msg.value.replace(reg, `<emoji src="${emojiList.value[key]}">`); } }, { immediate: true } );
这个时候来讲一下为什么上面循环的消息变量要使用split(/(<[^>]+>)/g)了,因为此时的消息为“文本<emoji src=“http://xxx”>文本”,通过分割就会变为[‘文本’,‘<emoji src=“http://xxx”>’,'文本],这样才能正确循环渲染。
在dom定义中,img的src使用的imgSrc函数,就是取出这个<emoji>标签里的src路径用的
function imgSrc(txt) {
return txt.match(/<emoji.*?src="(.*?)".*?>/);
}
这样就实现了输入回显、发送转义、结果回显功能了。
之前也试过直接把匹配到的表情包变成img标签,通过v-html直接回显的,但是这样做会有XSS风险!
后面我会更新如何实现@功能、复制图片到输入框中并发送,以及撤回等功能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。