功能描述
實現在 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
draft-js-plugins
使用 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;
}
}