功能描述
实现在 Braft Editor 中添加和显示自定义的 emoji 图标(给出的是一系列的图片地址,形如:https: xxx/emoji1.png)。主要功能点包括:点击对应的 emoji 后在编辑器中渲染对应的 emoji 图标,以及支持将发送的消息“重新编辑”回填到编辑框中。
开发过程
初始尝试
最初,我试图使用自定义的 EmojiSelect
组件来实现这个功能。但这种方法只能向编辑框插入类似 [emoji1]
这样的 emoji 描述文本,而不能显示为实际的 emoji 图标。
const EmojiSelect = ({ onEmojiClick }) => {
const [visibleObj, setVisibleObj] = useState({});
const handleClickEmoji = item => {
onEmojiClick(item);
// 其他操作如关闭弹窗
};
return (
<Popover
overlayClassName={styles['custom-emoji-popover']}
{
...其他配置
}
value={replyEditorState}
content={
<div className={styles['icons-container']}>
<div>默认表情</div>
<div className={styles['icon-list']}>
{sortEmoji.map((item, index) => {
return (
<img
key={index}
onClick={() => handleClickEmoji(item)}
src={item.url}
alt="emoji"
/>
);
})}
</div>
</div>
}
>
😄
</Popover>
);
};
BraftEditor组件处以下面的方式使用:
<BraftEditor
{
...其他配置
}
controls={[
{
key: 'custom-emoji', // 使用key来指定控件类型
title: '表情', // 自定义控件title
text: (
<EmojiSelect
onEmojiClick={emoji => {
const newEditorState = ContentUtils.insertText(
replyEditorState,
`[${emoji.name}]`
);
setReplyEditorState(newEditorState);
}}
/>
),
type: 'button' // ,
},
'bold',
'text-color',
'link'
]}
/>
尝试使用装饰器
为了让插入的 emoji 描述文本能在编辑器里显示为 emoji 图标,我试图使用装饰器来实现。但这种方法遇到了一些问题,例如无法选中 emoji、不能删除 emoji、只能在 emoji 前面输入文本、光标位置错误等,踩坑记录和这位哥们相似: Draft.js实现微信emoji功能。因此,我放弃了这种方法。
const Emoji = (props) => {
const {decoratedText } = props;
const name = decoratedText.substring(1, decoratedText.length - 1);
const emoji = sortEmoji.find(item => item.name === name);
return <img src={emoji.url} width={20} height={20}/>;
};
// 找到所有的 "[emoji1]" 字符串
const emojiStrategy = (contentBlock, callback, contentState) => {
const text = contentBlock.getText();
let matchArr, start, end;
const regExp = /\[emoji\d+\]/g;
while ((matchArr = regExp.exec(text)) !== null) {
start = matchArr.index;
end = start + matchArr[0].length;
callback(start, end);
}
};
// 创建装饰器
const emojiDecorator = new CompositeDecorator([
{
strategy: emojiStrategy,
component: Emoji,
},
]);
其他方法尝试
参考了下面的一些实现方法,但效果均不太想理想(亦或是我自己实现方法不佳),遂未采用。
项目实践,一文秒懂Braft Editor编辑器扩展自定义Block
基于Draftjs实现的Electron富文本聊天输入框(三) —— Emoji
使用 Braft Editor 的表情包插件
最后,我找到了 Braft Editor 的表情包插件 https://braft.margox.cn/demos/emoticond ,并根据插件的源码 https://github.com/margox/braft-extensions/blob/master/src/emoticon/index.jsx,自定义了我的 EmojiSelect/index.js 文件。
// EmojiSelect/index.js文件
import React from 'react';
import { ContentUtils } from 'braft-utils';
import './index.less';
import { emoticonsArr } from '@/constants/emoji';
const insertEmoticon = (editor, editorState, src) => {
editor.setValue(
ContentUtils.insertText(editorState, ' ', null, {
type: 'EMOTICON',
mutability: 'IMMUTABLE',
data: { src }
})
);
};
let controlRef = null;
const bindControlRef = ref => (controlRef = ref);
export default options => {
options = {
emoticons: emoticonsArr,
closeOnSelect: true,
closeOnBlur: true,
...options
};
const {
emoticons,
closeOnSelect,
closeOnBlur,
includeEditors = ['demo-editor-with-emoticon'],
excludeEditors
} = options;
const genericEmoticonsList = (emoticonsList, props) => {
return emoticonsList.map((item, index) => (
<img
onClick={() => {
insertEmoticon(props.editor, props.editorState, item);
closeOnSelect && controlRef && controlRef.hide();
}}
key={index}
src={item}
/>
));
};
return {
type: 'entity',
includeEditors,
excludeEditors,
name: 'EMOTICON',
control: props => ({
key: 'EMOTICON',
replace: 'emoji',
type: 'dropdown',
text: <i className="bfi-emoji"></i>,
showArrow: false,
ref: bindControlRef,
autoHide: closeOnBlur,
component: (
<div className="braft-emoticon-picker">
<div className="braft-emoticon-title">默认表情</div>
<div className="braft-emoticons-list">
{genericEmoticonsList(emoticons, props)}
</div>
</div>
)
}),
mutability: 'IMMUTABLE',
component: props => {
const entity = props.contentState.getEntity(props.entityKey);
const { src } = entity.getData();
return (
<span className="braft-emoticon-in-editor">
<img src={src} />
{props.children}
</span>
);
},
importer: (nodeName, node) => {
if (
nodeName.toLowerCase() === 'span' &&
node.classList &&
node.classList.contains('braft-emoticon-wrap')
) {
const imgNode = node.querySelector('img');
const src = imgNode.getAttribute('src');
// 移除img节点以避免生成atomic block
node.removeChild(imgNode);
return {
type: 'EMOTICON',
mutability: 'IMMUTABLE',
data: { src }
};
}
},
// 调用toHTML方法时,表情可转化为[emoji]格式
exporter: entityObject => {
const { src } = entityObject.data;
const emojiName = src
.split('/')
.pop()
.replace('.png', '');
return `[${emojiName}]`;
}
};
};
BraftEditor组件处以下面的方式使用:
import createEmoticonPlugin from '../../../../../components/EmojiSelect';
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/index.css';
import { emoticonsArr } from '@/constants/emoji';
BraftEditor.use(
createEmoticonPlugin({
includeEditors: ['demo-editor-with-emoticon'],
emoticons: emoticonsArr,
closeOnBlur: true,
closeOnSelect: true
})
);
<BraftEditor
id="demo-editor-with-emoticon"
value={replyEditorState}
onChange={editOnChange}
controls={['emoji', 'bold', 'text-color', 'link']}
/>
回显问题的解决
我还解决了一个问题,即如何将 <p>[emoji38][emoji25]文本[emoji27][emoji36]</p>
这样的格式(其中可能会有其他的a标签之类的),解析出其中的emoji表情并在富文本编辑器中以正常的 emoji 显示。我通过解析 HTML,将其中的 emoji 文本转换为实际的 emoji 图片,利用ContentUtils.insertText 在原来的editorState进行修改,从而实现了这个功能。
附:
// 这个方法主要是识别节点类型,利用ContentUtils.insertText 在原来的editorState进行修改
const convertNodeToEditorState = (node, editorState) => {
node.childNodes.forEach(childNode => {
if (childNode.nodeName === 'P') {
if (childNode.textContent.trim() === '') {
// 如果 <p> 标签为空,添加换行
editorState = ContentUtils.insertText(editorState, '\n');
} else {
// 如果 <p> 标签包含文本或其他元素,递归处理
editorState = convertNodeToEditorState(childNode, editorState);
}
editorState = ContentUtils.insertText(editorState, '\n');
} else if (
childNode.nodeName === 'SPAN' &&
childNode.classList.contains('braft-emoticon-wrap')
) {
// 如果节点是一个表情符号,添加对应的表情符号标记
const imgNode = childNode.querySelector('img');
const src = imgNode.getAttribute('src');
const emojiName = src
.split('/')
.pop()
.replace('.png', '');
editorState = ContentUtils.insertText(
editorState,
`[${emojiName}]`
);
}
// 如果节点是a标签
else if (childNode.nodeName === 'A') {
editorState = ContentUtils.insertText(
editorState,
childNode.textContent,
'',
{
type: 'LINK',
data: { href: childNode.getAttribute('href') }
}
);
} else if (childNode.nodeName === '#text') {
// 如果节点是文本节点,则对文本进行切割:
// 如 '你好[emoji1]再见[emoji2]了' 处理为 ['你好', '[emoji1]', '再见', '[emoji2]', '了']
// 使用正则表达式匹配和分割字符串
const str = childNode.textContent;
const regex = /((?:\[[^\]]*\])|(?:[^\[]+))/g;
const result = str.match(regex).map(item => {
// 识别是否 [emoji + 数字(1~300)+] 的格式
const isEmoji = /\[emoji(300|[1-2]?\d{1,2})\]/.test(item);
return {
str: isEmoji ? item.replace(/\[|\]/g, '') : item,
isEmoji: isEmoji
};
});
result.forEach(temp => {
if (temp.isEmoji) {
editorState = ContentUtils.insertText(
editorState,
' ',
null,
{
type: 'EMOTICON',
mutability: 'IMMUTABLE',
data: {
src: `${emojiImgUrl}${temp.str.replace(
/\[|\]/g,
''
)}.png`
}
}
);
} else {
editorState = ContentUtils.insertText(
editorState,
temp.str
);
}
});
} else if (childNode.nodeName === 'BR') {
editorState = ContentUtils.insertText(editorState, '\n');
} else {
// 如果节点是其他类型,递归处理
editorState = convertNodeToEditorState(childNode, editorState);
}
});
return editorState;
};
const convertHtmlToEditorState = (html, editorState) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return convertNodeToEditorState(doc.body, editorState);
};
// 只能用当前的editorState去setValue,不然无法再次插入emoji
const editorState = ContentUtils.clear(braftEditorRef.current.getValue());
const text = convertHtmlToEditorState(val, editorState);
text && braftEditorRef.current.setValue(text);
样式处理
.bf-container {
padding-bottom: 100px;
.bf-controlbar {
box-shadow: unset;
.bf-dropdown:first-child {
.dropdown-content {
.dropdown-arrow {
display: none;
}
top: -900%;
border: 1px solid #d9d9d9;
.dropdown-content-inner {
background-color: white;
.braft-emoticon-picker::before {
background-image: none;
}
.braft-emoticon-picker::after {
background-image: none;
}
.braft-emoticon-picker {
width: 420px;
overflow: auto;
height: 300px;
padding: 8px 8px 0 8px;
.braft-emoticon-title {
margin: 4px 8px;
}
.braft-emoticon-list-common {
max-height: 74px;
min-height: 44px;
height: max-content !important;
overflow: hidden !important;
}
.braft-emoticons-list {
padding: 0;
width: 410px;
overflow: visible;
height: 200px;
img {
cursor: pointer;
}
}
}
}
}
}
}
.bf-content {
height: 100%;
.public-DraftEditor-content {
margin-top: -15px;
}
.public-DraftEditor-content > div {
padding-bottom: 5px;
}
}
.customer-button {
margin: 5px 0;
padding: 0 5px;
font-size: 13px;
.ant-upload {
width: 34px;
}
}
.customer-button-disabled {
cursor: not-allowed;
}
.bf-media {
display: contents;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。