一、 emoji-mart
import './App.css';
import React, { Component } from 'react'
import 'emoji-mart/css/emoji-mart.css'
import { Picker } from 'emoji-mart'
import { Input, Tooltip } from 'antd';
import "antd/dist/antd.css";
import { SmileOutlined } from '@ant-design/icons';
import _ from "lodash"
class App extends Component {constructor(props) {super(props);this.state = {chatContent: "",showEmojiModal:false}}onChatContentChange = (value) => {this.setState({ chatContent: value })};searchEmoji(emoji, event) {console.log("emoji, event=====", emoji, event)this.setState({ emoji: emoji, showEmojiModal: false })let { chatContent } = this.state;chatContent = !_.isEmpty(chatContent) ? (chatContent + emoji.native) : emoji.native;this.setState({ chatContent });}render() {return (<div className="App"><div className="emoji_container">{this.state.showEmojiModal&&<Picker set='apple' emoji=''showPreview={false} onClick={(emoji, event)=>this.searchEmoji(emoji, event)}/>}</div>{/* emoji=''设置preview默认状态下不显示图片 */}<div style={
{ padding: 15, paddingBottom: 10, cursor: 'pointer' }} onClick={() => this.setState({ showEmojiModal: !this.state.showEmojiModal })}><Tooltip title="表情"><SmileOutlined style={
{ fontSize: 20, color: '#787878' }} /></Tooltip></div><div style={
{ display: 'flex', flex: 1, alignItems: 'center', paddingLeft: 10, paddingRight: 10, paddingBottom: 10 }}><Input.TextAreastyle={
{ display: 'flex', flex: 1, height: '100%', boxShadow: "none", fontSize: 16 }}placeholder="请输入...您可按Enter键发送消息"value={this.state.chatContent}onPressEnter={e => { }}onKeyUp={this.onKeyUp}onChange={e => this.onChatContentChange(e.target.value)}/></div></div>);}
}export default App;
.App{padding: 20px;}
.emoji-mart-search{display: none;}/* 隐藏搜索框 */
.emoji-mart-preview{display: none;}
/* 隐藏解釋當前點擊的表情框 */
按照如上代码
https://github.com/missive/emoji-mart/issues/465
必须对输入框的font-family进行样式控制,否则有些电脑查看,表情会变成方框
{font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "Android Emoji", -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif!important ;}
二、emoji-picker-react
import React, { useState } from 'react';
import Picker from 'emoji-picker-react';const App = () => {const [chosenEmoji, setChosenEmoji] = useState(null);const onEmojiClick = (event, emojiObject) => {setChosenEmoji(emojiObject);};return (<div>{chosenEmoji ? (<span>You chose: {chosenEmoji.emoji}</span>) : (<span>No emoji Chosen</span>)}<Picker onEmojiClick={onEmojiClick} /></div>);
};
export default App;
必须对输入框的font-family进行样式控制,否则有些电脑查看,表情会变成方框
{font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "Android Emoji", -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif!important ;}
手机端实现的话,比较简单,有现成的组件:aurora-imui-react-native
三、纯js实现,emoji图片从本地加载
区别于上面几个,它是将emoji以图片的形式展现出来,使用可编辑的div来替代输入框。注意:图片需要在public内也放置一份,不然img不能直接通过链接获取图片资源
Div + contenteditable去掉外框
#charInput:focus { border: none;outline:none; }
注意提交文本的时候,若是直接回车发送文案的,绑定的发送事件内,下列这行代码需要更改
let { chatContent, messageList } = this.state;chatContent = this.formatInputCon().replace(/<br>/g, '\r\n')
更换成
let { chatContent, messageList } = this.state;chatContent = this.formatInputCon().replace(/<br>/g, '\r\n')
还有注意,需要将内容中的div标签替换成空
切记,火狐上面兼容光标时,这个可编辑的div不能设置样式display:flex
若是给回车事件绑定了发送事件,那么在回车事件内,应该阻止一下默认事件
if (e.preventDefault) {e.preventDefault()} else {e.retuenValue = false}this.onSendText();
完整源码:https://github.com/MeiJunNa/emoji
import React, { Component } from 'react'
import { Tooltip, Button, message } from 'antd';
import { SmileOutlined } from '@ant-design/icons';
import "antd/dist/antd.css";
import _ from "lodash";
import "./sendEmoji.css";
const emojiData = require('./assets/emoji/emoji.json')
class App extends Component {constructor(props) {super(props);this.state = {chatContent: "", // 发出的内容showEmojiModal: false,emojiIcon: emojiData.icon, // 导入的emoji表情配置文件内容inputRange: '', // 光标showEmotions: true,showDingbats: false,showPerson: false,showUncategorized: false,}this.saveRangeLocal = this.saveRangeLocal.bind(this);this.inputSend = this.inputSend.bind(this);}// 将输入框中的图片替换为emoji表情formatInputCon() {let inputValue = document.getElementById('charInput').innerHTMLinputValue = inputValue.replace(/<img.*?(?:>|\/>)/gi, (val) => {let unicode = val.match(/unicode=[\'\"]?([^\'\"]*)[\'\"]?/i)[1]let icon = this.state.emojiIconlet iPic = ''// 遍历查找Unicode表情for (const key in icon) {if (icon.hasOwnProperty(key)) {const iType = icon[key]let flag = falsefor (let index = 0; index < iType.length; index++) {const element = iType[index]if (element.unicode == unicode) {iPic = element.emojiflag = truebreak}}if (flag) { break }}}return iPic})console.log("inputValue", inputValue)this.setState({ chatContent: inputValue })return inputValue}// 发送消息inputSend(e) {let { chatContent, messageList } = this.state;chatContent = this.formatInputCon();// this.state.chatContent = this.formatInputCon().replace(/<br>/g, '\r\n')// 是否为图片const isImg = (/^\<img src\=/g).test(chatContent)let imgSrc = ''chatContent = chatContent.replace(/<br>/g, '')chatContent = chatContent.replace(/ /g, '')this.setState({ showEmojiModal: false, showEmotions: true })//发送消息后清空输入框let inputValue = document.getElementById('charInput')inputValue.innerHTML = ""if (_.isEmpty(chatContent)) {return message.warn("请输入消息内容");}// 如果是图片 正则匹配获取src地址if (isImg) {chatContent = chatContent.replace(/^\<img src\=/, "")chatContent = chatContent.replace(/\>$/, "")chatContent = chatContent.replace(/alt=""/, "")chatContent = chatContent.replace(/\"|\'/g, "")// console.log(chatContent , '==========')}// 如果是base64图片// 需要上传服务器 base64格式字节太大 会导致socket断开if (/data:image/.test(chatContent)) {vararr = chatContent.split(','),mime = arr[0].match(/:(.*?);/)[1],bstr = atob(arr[1]),n = bstr.length,u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}var obj = new Blob([u8arr], { type: mime });var fd = new FormData();fd.append("upfile", obj, "image.png");const xhr = new XMLHttpRequest();// 图片上传服务器地址// xhr.open("POST", (process.env.NODE_ENV === 'development' ? config.baseUrl.dev : config.baseUrl.pro) + "media/upload");// xhr.send(fd);// xhr.onreadystatechange = () => {// if (xhr.readyState === 4 && xhr.status === 200) {// // 获取响应文件// let str = xhr.responseText// // 将json解析为对象// str = JSON.parse(str)// // 图片读取地址// imgSrc = (process.env.NODE_ENV === 'development' ? config.baseFileUrl.dev : config.baseFileUrl.pro) + str.ret// let nMessage = {// // 是否为图片// isImg: isImg,// imgSrc: imgSrc,// msgId: msgId,// status: "send_succeed",// msgType: "text",// text: isImg ? `<img src=${imgSrc} alt="" />` : chatContent,// };// }// };}else {let nMessage = {// 是否为图片isImg: isImg,// base64图片地址imgSrc: imgSrc,msgType: "text",text: isImg ? `<img src=${chatContent} alt="" />` : chatContent,};// if (global.isURL(chatContent)) {// nMessage.type = "url";// nMessage.mediaPath = chatContent;// }}}// 延时记录光标到位置saveRangeLocal() {setTimeout(() => {this.state.inputRange = window.getSelection().getRangeAt(0)}, 0)}// 点击表情,将表情添加到输入框selectEmojiIcon(emoji) {this.setState({ showEmojiModal: false, showEmotions: true })let inputNode = document.getElementById('charInput')const imgUrl = './assets/emoji/icon/' + emoji.unicode + '.png'let html = "<img src='" + imgUrl + "' unicode = '" + emoji.unicode + "' alt='' class='iconImgDiv'>"let sel = window.getSelection()let range = this.state.inputRangelet el = document.createElement("div")let frag = document.createDocumentFragment(), node, lastNodeif (!inputNode) {return}if (!range) {inputNode.focus()range = window.getSelection().getRangeAt(0)}range.deleteContents()el.innerHTML = htmlwhile ((node = el.firstChild)) {lastNode = frag.appendChild(node)}range.insertNode(frag)if (lastNode) {range = range.cloneRange()range.setStartAfter(lastNode)range.collapse(true)sel.removeAllRanges()sel.addRange(range)}}// 将emoji表情转换为图片changeEmojiCon(str) {let patt = /[\ud800-\udbff][\udc00-\udfff]/g // 检测utf16字符正则str = str.replace(patt, (char) => {let H, L, codeif (char.length === 2) {H = char.charCodeAt(0) // 取出高位L = char.charCodeAt(1) // 取出低位code = (H - 0xD800) * 0x400 + 0x10000 + L - 0xDC00 // 转换算法return "&#" + code + ";"} else {return char}})str = str.replace(/&#{1}[0-9]+;{1}/ig, (a) => {let unicode = a.replace(/^&#{1}/ig, '')unicode = unicode.replace(/;{1}$/ig, '')unicode = 'U+' + (parseFloat(unicode).toString(16).toUpperCase())const imgUrl = './assets/emoji/icon/' + unicode + '.png'return "<img src='" + imgUrl + "'/>"})return str}render() {return (<div className="App"><div style={
{ padding: 15, paddingBottom: 10, cursor: 'pointer' }} onClick={() => this.setState({ showEmojiModal: !this.state.showEmojiModal })}><Tooltip title="表情"><SmileOutlined style={
{ fontSize: 20, color: '#787878' }} /></Tooltip></div><div style={
{ display: 'flex', flex: 1, alignItems: 'center', paddingLeft: 10, paddingRight: 10, paddingBottom: 10 }}><divid="charInput"style={
{ display: 'flex', flex: 1, boxShadow: "none", height: '100px', border: "1px solid #eee", fontSize: 16 }}className="text_emojs_box"contentEditable="true"onClick={this.saveRangeLocal}onFocus={this.saveRangeLocal}onInput={this.saveRangeLocal}onPaste={(e)=>this.pasteEvent(e)}/><Button type="primary" style={
{ marginLeft: 10 }} onClick={() => this.inputSend()}>发送</Button></div>{this.state.showEmojiModal &&<div className="chatframe_input_con scrollbar"><div className="chatframe-icon"><span className="iconfont emoji_pane_tab"onClick={() => this.setState({ showEmotions: true, showDingbats: false, showPerson: false, showUncategorized: false })}style={
{ color: this.state.showEmotions ? "#333" : "#ccc" }}></span><span className="iconfont emoji_pane_tab"onClick={() => this.setState({ showDingbats: true, showEmotions: false, showPerson: false, showUncategorized: false })}style={
{ color: this.state.showDingbats ? "#333" : "#ccc" }}></span><span className="iconfont emoji_pane_tab"onClick={() => this.setState({ showPerson: true, showEmotions: false, showDingbats: false, showUncategorized: false })}style={
{ color: this.state.showPerson ? "#333" : "#ccc" }}></span><span className="iconfont emoji_pane_tab"onClick={() => this.setState({ showUncategorized: true, showEmotions: false, showDingbats: false, showPerson: false })}style={
{ color: this.state.showUncategorized ? "#333" : "#ccc" }}></span></div><ul>{this.state.showEmotions && this.state.emojiIcon.Emotions.map((emotions, index) => {const imgUrl = './assets/emoji/icon/' + emotions.unicode + '.png'return (<li key={"emotions" + index} className="chat_emoji_li"><img src={imgUrl} className="chat_emoji_item" onClick={() => this.selectEmojiIcon(emotions)} /></li>)})}{this.state.showDingbats && this.state.emojiIcon.Dingbats.map((emotions, index) => {const imgUrl = './assets/emoji/icon/' + emotions.unicode + '.png'return (<li key={"emotions" + index} className="chat_emoji_li"><img src={imgUrl} className="chat_emoji_item" onClick={() => this.selectEmojiIcon(emotions)} /></li>)})}{this.state.showPerson && this.state.emojiIcon.Person.map((emotions, index) => {const imgUrl = './assets/emoji/icon/' + emotions.unicode + '.png'return (<li key={"emotions" + index} className="chat_emoji_li"><img src={imgUrl} className="chat_emoji_item" onClick={() => this.selectEmojiIcon(emotions)} /></li>)})}{this.state.showUncategorized && this.state.emojiIcon.Uncategorized.map((emotions, index) => {const imgUrl = './assets/emoji/icon/' + emotions.unicode + '.png'return (<li key={"emotions" + index} className="chat_emoji_li"><img src={imgUrl} className="chat_emoji_item" onClick={() => this.selectEmojiIcon(emotions)} /></li>)})}</ul></div>}{/* <!-- 显示内容区 --> */}<div>发送的消息:</div><div className="chatframe-text text_emoji" dangerouslySetInnerHTML={
{ __html: this.changeEmojiCon(this.state.chatContent) }}></div></div>)}
}export default App;
Vue可参考这位大佬的代码https://github.com/zhazhanitian/Emoji-ChatRoom