使用braft-editor实现简易版即时写文档并预览的富文本编辑器

最近在看各个富文本编辑库的时候,有点好奇,于是对比了一些,有draft-js,Slate.js。但是这些基本都是英文库,简单demo无法满足需求,比如插入截图和表格,这些复杂结构的数据,实现这些,必须自定义扩展,要想做到这些,通读一遍文档基本是必须,且内含的天坑,估计也是数不胜数。市面上做的好的富文本编辑器,基本上各家我都使用了一下,语雀的文档就做的蛮好的,有道云也不错,当然人家是吃这口饭的,就单独说插入表格这个吧,(浅吐槽一句csdn写文章的插入表格都还蛮low的)还有插入模板,语雀做的非常棒了,选中表格也能即时删除某列,更别说插入模板了。富文本编辑器这东西,怎么说呢,简单的调用是很简单,复杂使用也是巨复杂。

好了言归正传,使用braft-editor库我简易的实现了富文本编辑器的效果,能够插入图片和表格,点击图片也能放大查看

具体效果如图:
使用braft-editor实现简易版即时写文档并预览的富文本编辑器_第1张图片
使用braft-editor实现简易版即时写文档并预览的富文本编辑器_第2张图片
具体代码如下:

import React, { useState, useEffect,useRef } from 'react';
import 'braft-editor/dist/index.css';
import 'braft-extensions/dist/table.css';
import BraftEditor from 'braft-editor';
import Table from 'braft-extensions/dist/table';
import {Image } from 'antd';
import './index.less'

BraftEditor.use(Table({
  defaultColumns: 2, // 默认列数
  defaultRows: 1, // 默认行数
  withDropdown: false, // 插入表格前是否弹出下拉菜单
  columnResizable: false, // 是否允许拖动调整列宽,默认false
  exportAttrString: 'border="1" style="border-collapse: collapse"', // 指定输出HTML时附加到table标签上的属性字符串
  includeEditors: ['editor-id-1'], // 指定该模块对哪些BraftEditor生效,不传此属性则对所有BraftEditor有效
  excludeEditors: ['editor-id-2']  // 指定该模块对哪些BraftEditor无效
}));

const RichTextEditor = () => {
  const [editorState, setEditorState] = useState(BraftEditor.createEditorState('

'
)); const [content, setContent] = useState(''); const editorRef = useRef(null); // 预览图片的显示隐藏 const [visible, setVisible] = useState(false); // 预览图片的src const [imgSrc, setImgSrc] = useState(''); const handleEditorChange = (editorState) => { setEditorState(editorState); setContent(editorState.toHTML()); console.log('editorState=====',JSON.stringify(editorState.toHTML())) console.log('editorState-------',editorState.toRAW()) }; useEffect(() => { const fetchData = async () => { const response = await fetch('your-backend-api-url'); const data = await response.json(); const editorState = BraftEditor.createEditorState(data.content); setEditorState(editorState); }; fetchData(); }, []); const saveContent = async () => { const htmlContent = editorState.toHTML(); await fetch('your-backend-api-url', { method: 'POST', body: JSON.stringify({ content: htmlContent }), headers: { 'Content-Type': 'application/json', }, }); console.log('内容保存成功'); }; const tableOptions = { defaultColumns: 1, defaultRows: 1, withDropdown: true, }; const handleImg = (e) => { if (e.target.nodeName === 'IMG') { setVisible(true); setImgSrc(e.target.currentSrc); } }; return ( <> <div style={{display:'flex',justifyContent:'space-around'}}> <div style={{width:'800px',height:'700px'}} onClick={handleImg}> <BraftEditor placeholder="请输入正文内容" style={{ border: '1px solid #d9d9d9', height: 700,background:'#FAFAFA' }} ref={editorRef} value={editorState} onChange={handleEditorChange} id="editor-id-1" extendControls={[ 'separator', { key: 'my-modal', type: 'modal', title: '这是一个自定义的下拉组件', // 指定鼠标悬停提示文案 className: 'my-modal', // 指定触发按钮的样式名 html: null, // 指定在按钮中渲染的html字符串 text: 'Hello', // 指定按钮文字,此处可传入jsx,若已指定html,则text不会显示 onClick: () => {}, // 指定触发按钮点击后的回调函数 modal: { id: 'my-modal', // 必选属性,传入一个唯一字符串即可 title: '我的弹窗', // 指定弹窗组件的顶部标题 className: 'my-modal', // 指定弹窗组件样式名 width: 500, // 指定弹窗组件的宽度 height: 500, // 指定弹窗组件的高度 showFooter: true, // 指定是否显示弹窗组件底栏 showCancel: true, // 指定是否显示取消按钮 showConfirm: true, // 指定是否显示确认按钮 confirmable: true, // 指定确认按钮是否可用 showClose: true, // 指定是否显示右上角关闭按钮 closeOnBlur: true, // 指定是否在点击蒙层后关闭弹窗(v2.1.24) closeOnConfirm: true, // 指定是否在点击确认按钮后关闭弹窗(v2.1.26) closeOnCancel: true, // 指定是否在点击取消按钮后关闭弹窗(v2.1.26) cancelText: '取消', // 指定取消按钮文字 confirmText: '确定', // 指定确认按钮文字 bottomText: null, // 指定弹窗组件底栏左侧的文字,可传入jsx onConfirm: () => {}, // 指定点击确认按钮后的回调函数 onCancel: () => {}, // 指定点击取消按钮后的回调函数 onClose: () => {}, // 指定弹窗被关闭后的回调函数 onBlur: () => {}, // 指定蒙层被点击时的回调函数 children: <>这是一个弹窗</>, // 指定弹窗组件的内容组件 } } ]} /> <Image width={400} style={{ display: 'none', }} src={imgSrc} preview={{ visible, src: imgSrc, onVisibleChange: (value) => { setVisible(value); }, }} /> </div> <div onClick={handleImg} dangerouslySetInnerHTML={{ __html: content }} style={{width:'800px',height:'700px',border: '1px solid #d9d9d9',padding:'20px'}} className='cont'></div> </div> <button onClick={saveContent}>保存</button> </> ); }; export default RichTextEditor;

index.less 文件如下:

.cont {
  table{
    width: 100%;
    border-color: #d9d9d9;
    td{
      height: 36px;
    }
  }
  pre {
    max-width: 100%;
    max-height: 100%;
    margin: 10px 0;
    padding: 15px;
    overflow: auto;
    background-color: #f1f2f3;
    border-radius: 3px;
    color: #666;
    font-family: monospace;
    font-size: 14px;
    font-weight: 400;
    line-height: 16px;
    word-wrap: break-word;
    white-space: pre-wrap;
  }
  ul{
    list-style-type:disc;
     list-style-position:inside;
    } 
  ol{
    list-style-type:decimal; 
    list-style-position:inside;
  }  
}

友情提示
gif文件是不能直接粘贴进去编辑器里面的,对于此我又额外去研究了一下。ContentUtils是Braft Editor基础工具包链接在此,在通过受控组件的形式使用编辑器时,你可以通过操作editorState来达到一些特定的需求。这种使用extendControls自定义控件,需要借助上传文件的方法让后端返回url再回显到编辑器里面。语雀也是这么做的
为此我们需要自定义一个自定义控件

  extendControls={[
          'separator',
          {
            key: 'my-modal',
            type: 'modal',
            title: '插入文件', // 指定鼠标悬停提示文案
            className: 'my-modal', // 指定触发按钮的样式名
            html: null, // 指定在按钮中渲染的html字符串
            text: '插入文件', // 指定按钮文字,此处可传入jsx,若已指定html,则text不会显示
            onClick: () => {
              setIsVisible(true);
            }, // 指定触发按钮点击后的回调函数
          },
        ]}

点击插入文件弹出modal框,这一步就跟普通的上传文件一样
使用braft-editor实现简易版即时写文档并预览的富文本编辑器_第3张图片
紧接着在上传文件成功时,拿到URL处理一下再返回编辑器里面,

 const uploadFiles = {
    name: 'file',
    multiple: false,
    action: toHandleToken(),
    showUploadList: false,
    beforeUpload: (file) => {
      console.log(file, '文件');
      const fileType = file.name.split('.').pop();
      console.log(fileType);
      if (fileType !== 'gif' && fileType !== 'jpg' && fileType !== 'mp4') {
        noticeError(`上传失败:上传文件格式非.jpg,.gif,.mp4`);
        return false;
      }
      const arrData = fileList;
      for (let i = 0; i < arrData.length; i++) {
        if (arrData[i].name == file.name) {
          noticeError(`上传失败:禁止上传重复的文件`);
          return false;
        }
      }
      return true;
    },
    onChange: (info) => {
      console.log(info);
      if (info.file.status === 'done') {
        // 图片上传成功返回url
        const newEditorState = ContentUtils.insertMedias(editorState, [
          {
            type: info?.file?.type?.includes('image')
              ? 'IMAGE'
              : info?.file?.type?.includes('mp4')
              ? 'VIDEO'
              : 'AUDIO',
            url: info.file.response.fileUrl, // 图片url,
          },
        ]);
        setEditorState(newEditorState);
        setFileList(info.fileList);
        setIsVisible(false);
      } else if (info.file.status === 'error') {
      }
    },
  };

另外存储编辑器内容最好以raw形式存储。具体见官方文档

下面贴出全部代码:

import React, { useState } from 'react';
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/index.css';
import { ContentUtils } from 'braft-utils';
import { Button, message, Upload, Modal } from 'antd';
import { noticeError, noticeSuccess, noticeWarn } from '@/utils/noticeUtil';
import { CloudUploadOutlined } from '@ant-design/icons';
import baseUrl from '@/utils/globalUrl';
const { Dragger } = Upload;
const MyEditor = () => {
  const [editorState, setEditorState] = useState(
    BraftEditor.createEditorState(null),
  );
  const [isVisible, setIsVisible] = useState(false);
  const [fileList, setFileList] = useState([]);

  const handleEditorChange = (editorState) => {
    //console.log(editorState)
    setEditorState(editorState);
    //console.log('editorState=====',JSON.stringify(editorState.toHTML()))
    console.log('editorState-------', editorState.toRAW());
  };

  const insertGif = () => {
    const gifUrl = 'https://dev-xxxxxxx.gif'; // 替换为你的 GIF 图片 URL
    const newEditorState = ContentUtils.insertMedias(editorState, [
      {
        type: 'IMAGE',
        url: gifUrl,
        meta: {
          id: 'gifId',
          gif: true,
        },
      },
    ]);
    setEditorState(newEditorState);
  };

  //处理导入文件的url
  const toHandleToken = () => {
    let baseurl = `${baseUrl}/api/auto_devops/script/upload?`;
    let url;
    let resInfo = sessionStorage.getItem('oauth');
    if (resInfo != null) {
      let resJsons = JSON.parse(resInfo);
      let token = resJsons.access_token;
      url = baseurl + 'access_token=' + token;
    } else {
      url = baseurl + '';
    }
    return url;
  };
  const uploadFiles = {
    name: 'file',
    multiple: false,
    action: toHandleToken(),
    showUploadList: false,
    beforeUpload: (file) => {
      console.log(file, '文件');
      const fileType = file.name.split('.').pop();
      console.log(fileType);
      if (fileType !== 'gif' && fileType !== 'jpg' && fileType !== 'mp4') {
        noticeError(`上传失败:上传文件格式非.jpg,.gif,.mp4`);
        return false;
      }
      const arrData = fileList;
      for (let i = 0; i < arrData.length; i++) {
        if (arrData[i].name == file.name) {
          noticeError(`上传失败:禁止上传重复的文件`);
          return false;
        }
      }
      return true;
    },
    onChange: (info) => {
      console.log(info);
      if (info.file.status === 'done') {
        // 图片上传成功返回url
        const newEditorState = ContentUtils.insertMedias(editorState, [
          {
            type: info?.file?.type?.includes('image')
              ? 'IMAGE'
              : info?.file?.type?.includes('mp4')
              ? 'VIDEO'
              : 'AUDIO',
            url: info.file.response.fileUrl, // 图片url,
          },
        ]);
        setEditorState(newEditorState);
        setFileList(info.fileList);
        setIsVisible(false);
      } else if (info.file.status === 'error') {
      }
    },
  };

  return (
    <div>
      <button onClick={insertGif}>插入 GIF</button>
      <BraftEditor
        value={editorState}
        onChange={handleEditorChange}
        extendControls={[
          'separator',
          {
            key: 'my-modal',
            type: 'modal',
            title: '插入文件', // 指定鼠标悬停提示文案
            className: 'my-modal', // 指定触发按钮的样式名
            html: null, // 指定在按钮中渲染的html字符串
            text: '插入文件', // 指定按钮文字,此处可传入jsx,若已指定html,则text不会显示
            onClick: () => {
              setIsVisible(true);
            }, // 指定触发按钮点击后的回调函数
          },
        ]}
      />
      <Modal
        closable={false}
        visible={isVisible}
        onCancel={() => {
          setIsVisible(false);
        }}
        title={'插入文件'}
        width={400}
        footer={null}
      >
        <Dragger {...uploadFiles}>
          <p className="ant-upload-drag-icon">
            <CloudUploadOutlined />
          </p>
          <p className="ant-upload-text">
            将文件拖拽到此处,或<a>点击上传</a>
          </p>
          <p className="ant-upload-hint">文件大小不能超过 5 MB</p>
        </Dragger>
      </Modal>
    </div>
  );
};

const App = () => {
  return (
    <div>
      <h1>富文本编辑器示例</h1>
      <MyEditor />
    </div>
  );
};

export default App;

你可能感兴趣的:(富文本编辑器,react,braft-editor,前端,javascript,react.js,编辑器)