BRAFT EDITOR 自定义 block

1,563 阅读4分钟

需求: 当前项目需要在编辑器内实现从内置组件选择插入图片,所以需要自定block 来渲染,下面会在代码添加注释方便阅读,由于官方文档讲的模模糊糊,自己记录一下,方便参考

BRAFT EDITOR文档

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结构

entityMapMap结构,key对应typeatomicblockentityRanges 中对应的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" />