需求: 当前项目需要在编辑器内实现从内置组件选择插入图片,所以需要自定block 来渲染,下面会在代码添加注释方便阅读,由于官方文档讲的模模糊糊,自己记录一下,方便参考
import React, { useEffect, useState } from 'react';
import type { ControlType, EditorState, ExtendControlType } from 'braft-editor';
import BraftEditor from 'braft-editor';
import { ContentUtils } from 'braft-utils';
import { Button, Modal } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import FileBrowser from '../FileBrowser';
import type { FileURL } from '@/services/typings';
const controls: ControlType[] = [
'font-size',
'bold',
'italic',
'underline',
'text-color',
'separator',
];
interface RichTextEditorProps {
value?: any;
onChange?: (value: any) => void;
}
// 这里定义我们当前编辑器内要实时渲染组件表现形式 以及提供一个删除方法
const BlockImgComponent: React.FC<any> = (props) => {
const removeBlock = () => {
props.blockProps.editor.setValue(
ContentUtils.removeBlock(props.blockProps.editorState, props.block),
);
};
const blockData = props.contentState.getEntity(props.block.getEntityAt(0)).getData();
return (
<div style={{ position: 'relative' }}>
<img src={blockData.dataURL} />
<Button
onClick={() => removeBlock()}
icon={<DeleteOutlined />}
style={{ position: 'absolute', bottom: 0, left: 0 }}
/>
</div>
);
};
interface RendererFnProps {
editor: any;
editorState: any;
}
// 根据editorState 渲染逻辑,也是处理当前编辑器内渲染
const blockRendererFn = function (block: any, props: RendererFnProps): any {
if (block.getType() === 'atomic') {
const { editor, editorState } = props;
const entity = editorState.getCurrentContent().getEntity(block.getEntityAt(0));
if (entity.getType() === 'block-img') {
return {
component: BlockImgComponent,
editable: false,
props: { editor, editorState },
};
}
}
};
// 自定义html 转 raw 函数,当前用不到
const blockImportFn = (nodeName: string, node: any) => {
};
// 导出函数 通过type辨别 具体需要展示什么内容
const blockExportFn = (_: any, block: any) => {
if (block.type === 'block-img') {
const { dataURL, dataID } = block.data;
return {
start: `<img src=${dataURL} data-id=${dataID} />`,
end: '',
};
}
};
// 当前组件是封装称form表单使用 提供一个value 和 onChange方法
const RichTextEditor: React.FC<RichTextEditorProps> = ({ value, onChange }) => {
const [editorState, setEditorState] = useState<EditorState>(
BraftEditor.createEditorState('', { blockImportFn, blockExportFn }),
);
const [visible, setVisible] = useState<boolean>(false);
useEffect(() => {
setEditorState(BraftEditor.createEditorState(value, { blockImportFn, blockExportFn }));
}, [value]);
const handleChange = (state: EditorState) => {
setEditorState(state);
};
// 处理ctrl + s 保存逻辑,本打算放到handleChange处理,这样state刷新会导致光标乱跳,体验不好
const handleSave = (state: EditorState) => {
const raw = state.toRAW();
// 回调表单onChange
if (onChange) onChange(raw);
};
// 自定义扩展顶部 按钮,插入图片(从内置组件选择)
const extendControls: ExtendControlType[] = [
'separator',
{
key: 'my-button',
type: 'button',
title: '点击选择相册图片',
className: 'my-button',
html: null,
text: '图片',
onClick: () => setVisible(true),
},
];
const onSelect = ([file]: FileURL[]) => {
const { id, url } = file;
// 当我们选择图片的时候插入到当前编辑器 params (editorState,定义类型, _, 数据)
setEditorState(
ContentUtils.insertAtomicBlock(editorState, 'block-img', true, {
dataURL: url,
dataID: id,
}),
);
setVisible(false);
};
return (
<div>
<BraftEditor
className="my-editor"
value={editorState}
extendControls={extendControls}
onChange={handleChange}
style={{ border: '1px solid #999', borderRadius: '4px' }}
controls={controls}
onSave={handleSave}
blockRendererFn={blockRendererFn}
converts={{ blockImportFn, blockExportFn }}
placeholder="请输入商品详情描述"
/>
{visible && (
<Modal
title="选择图片"
centered
visible={visible}
width={1000}
bodyStyle={{ height: '480px', padding: '0' }}
footer={null}
onCancel={() => setVisible(false)}
>
<FileBrowser selectMode={true} multiSelect={false} onSelected={onSelect} />
</Modal>
)}
</div>
);
};
export default RichTextEditor;
注: 由于我们C断基于uniapp开发并不能用当前编辑器提供方法渲染 raw,由于我们图片需要id及url,商量之后后端去处理生成 html,前端处理会存在性能问题。
下面是相关转换规则整理(raw => html)
富文本编辑数据结构
1、支持编辑类型
- 字体字号
- 加粗字体
- 斜体
- 下划线字体
- 字体颜色
- 插入图片
2、基本结构
{
"blocks": [], // 很多行组成的数组
"entityMap": {}
}
3、block结构
blocks:数组数据结构,每行就是一个独立的block
{
"key": "", // 随机生成的一个值
"text": "hello world!", // 当前行的文本内容,
"type": "", // 目前有两个值 1. unstyled (默认值)按文本处理 2. atomic 自定义图片类型
"depth": "", // 目前默认值是0,暂时没用到
"inlineStyleRanges" :[
{
"offset":0, // 偏移起始位置
"length":10, // 偏移量
"style":"FONTSIZE-30" // 对偏移串加样式: 加粗、字号、斜体、下划线、颜色
// 详细介绍参考下面
}
],
"entityRanges": [ // 附加entity的偏移及其 key,key通过entityMap 查找对应的entity数据,目前只有 type = atomic 该数组才会有值
{
"offset":0,
"length":1,
"key":0
}
],
"data": { // 目前没有使用
}
}
style:
- 字号大小:FONTSIZE-30 ,其中30代表字号大号,渲染成
html style对应 font-size: 30px; - 加粗字体:BOLD,渲染
html style对应 font-weight: bold; - 斜体:ITALIC,渲染
html style对应 font-style: italic; - 下划线:UNDERLINE,渲染
html style对应 text-decoration: underline; - 颜色:COLOR-FDDA00,后六位为十六进制颜色,渲染
html style对应 color: #FDDA00;
注意: 如果 text 为空并且type 为 unstyled,则此时应该渲染一个空行, 作为html 输出 p标签
<p></p>
4、entityMap结构
entityMap:Map结构,key对应type为atomic的block中 entityRanges 中对应的key,用于一一对应查找需要的数据
{
"0":{ // key值 entityRanges key一一对应
"type":"block-img", // 渲染插入图片的类型, 目前规范为 block-img
"mutability":"IMMUTABLE", // 规定可变性,暂时用不到
"data":{ // 图片的 url和id 对应 dataURL dataID
"dataURL":"https://ututes.oss-cn-chengdu.aliyuncs.com/106414083233812480.jpg",
"dataID":"106414083233812480"
}
}
}
结构详解:
- key: entityRanges
key一一对应 - type:block-img 目前只有一种类型,渲染图片
- mutability:渲染逻辑暂时用不到
- data: 渲染图片所需的数据
- dataURL:图片的url
- dataID:图片的id
渲染成html:
<img src="dataURL" />