react-draft-wysiwyg富文本组件

import React, { useState, useEffect } from 'react'
import { EditorState, convertToRaw, ContentState, Modifier } from 'draft-js'
import { Input, message } from 'antd'
// https://jpuri.github.io/react-draft-wysiwyg/#/docs
import { Editor } from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import draftToHtml from 'draftjs-to-html'
import htmlToDraft from 'html-to-draftjs'
import urls from '../../api/urls'
import axios from 'axios'
import { htmlFormat, addUploadToken } from '../../utils/tools'
const { TextArea } = Input

//image position is lost when converting from html to EditorState
//参考链接:https://github.com/jpuri/html-to-draftjs/issues/101
// https://github.com/jpuri/html-to-draftjs/blob/master/src/library/index.js
export default function MyEditor(props) {
  const { value = {}, onChange } = props

  //把html字符传转换成富文本要求的格式
  const formatEditorState = (text) => {
    //解决image位置无法保存的bug
    const html = text ? htmlFormat(text) : ''
    const contentBlock = htmlToDraft(html, (nodeName, node) => {
      if (nodeName === 'img' && node instanceof HTMLImageElement) {
        const entityConfig = {}
        entityConfig.src = node.getAttribute
          ? node.getAttribute('src') || node.src
          : node.src
        entityConfig.alt = node.alt
        entityConfig.height = node.style.height
        entityConfig.width = node.style.width
        if (node.style.float) {
          entityConfig.alignment = node.style.float
        } else {
          if (node.style.textAlign) {
            entityConfig.alignment = node.style.textAlign
          }
        }

        return {
          type: 'IMAGE',
          mutability: 'MUTABLE',
          data: entityConfig,
        }
      }
    })
    const contentState = ContentState.createFromBlockArray(
      contentBlock.contentBlocks
    )
    const result = EditorState.createWithContent(contentState)
    return result
  }
  const [editorState, setEditorState] = useState(formatEditorState(value.text))

  const handleChange = (value) => {
    setEditorState(value)
    const html = draftToHtml(convertToRaw(editorState.getCurrentContent()))
    //提交给后端的是html字符串
    onChange({
      isChange: false,
      text: html,
    })
  }

  //图片上传
  const uploadImageCallBack = (file) => {
    return new Promise((resolve, reject) => {
      let formData = new FormData()
      formData.append('file', file)
      formData.append('ctype', 'course')
      axios(urls.light.uploadFile, {
        method: 'POST',
        data: formData,
        headers: {
          ...addUploadToken()
        },
      })
        .then((res) => {
          if (res.data.state === 1) {
            resolve({ data: { link: res.data.data.fileImg } })
          } else {
            message.error('图片上传失败', 2)
            reject(res)
          }
        })
        .catch((err) => {
          reject(err)
        })
    })
  }


  useEffect(() => {
    //课程图片列表上传的图片尾部有一个加号,点击加号会自动把图片插入到富文本框光标所在的位置
    if (value.isChange) {
      const contentState = Modifier.replaceText(
        editorState.getCurrentContent(),
        editorState.getSelection(),
        value.text,
      );
      const temp = EditorState.push(editorState, contentState, 'insert-characters')
      let html = draftToHtml(convertToRaw(temp.getCurrentContent()))
      html = html.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
      setEditorState(formatEditorState(html))
      //提交给后端的是html字符串
      onChange({
        isChange: false,
        text: html,
      })
    }
  }, [value])

  return (
    <div>
      <Editor
        editorState={editorState}
        wrapperClassName="m-my-editor-wrapper"
        editorClassName="demo-editor"
        localization={{
          locale: 'zh',
        }}
        onEditorStateChange={handleChange}
        toolbar={{
          image: {
            urlEnabled: true,
            uploadEnabled: true,
            alignmentEnabled: true, // 是否显示排列按钮 相当于text-align
            uploadCallback: uploadImageCallBack,
            previewImage: true,
            inputAccept: 'image/*',
            alt: { present: false, mandatory: false, previewImage: true },
          },
        }}
      />
      <TextArea
        disabled
        value={draftToHtml(convertToRaw(editorState.getCurrentContent()))}
        autoSize={{ minRows: 5, maxRows: 10 }}
      ></TextArea>
    </div>
  )
}

htmlFormat:

import { html2json, json2html } from 'html2json'


//富文本html格式转换
const htmlFormat = (data) => {
  const json = html2json(data)
  Array.isArray(json.child) &&
    json.child.forEach((item) => {
      if (item.tag === 'div') {
        item.tag = 'img'
        if (
          item.attr &&
          Array.isArray(item.child) &&
          item.child.length === 1 &&
          item.child[0].attr &&
          Array.isArray(item.child[0].attr.style)
        ) {
          const style = item.attr.style + item.child[0].attr.style.join('')
          item.attr = {
            ...item.child[0].attr,
          }
          item.attr.style = style
        }
      }
    })

  const html = json2html(json)
  return html
}

原文地址:https://www.cnblogs.com/xutongbao/p/15264343.html