react 项目结合 braft-editor 实现精致的富文本组件
简介: 基于 react 的富文本组件本来就不多,braft-editor 是 react 里人气相当高的富文本,这里基于他打造一个精致的富文本。
项目主要依赖:(先安装,步骤略)
create-react-app:3.0.0
1 | { |
2 | "react": "16.8.6", |
3 | "react-router-dom": "5.0.0", |
4 | "antd": "^3.19.2", |
5 | "axios": "^0.19.0", |
6 | "braft-editor": "^2.3.6", |
7 | "braft-polyfill": "^0.0.1", |
8 | "prop-types": "^15.7.2" |
9 | } |
1.组件
文件目录
src/components/AppEditor/index.jsx
1 | import React, { Component } from "react"; |
2 | // 添加ie10支持 |
3 | import "braft-polyfill"; |
4 | import BraftEditor from "braft-editor"; |
5 | import "braft-editor/dist/index.css"; |
6 | import PropTypes from "prop-types"; |
7 | import axios from "axios"; |
8 | import { message } from "antd"; |
9 | |
10 | import "./style.less"; // 见下文 |
11 | import config from "./config"; // 见下文 |
12 | import mediaBaseconfig from "./media"; // 见下文 |
13 | import { |
14 | uploadHtml, |
15 | uploadImage, |
16 | maxFileSize, |
17 | isImageFile |
18 | } from "@/utils/upload"; // 见下文 uploadHtml和uploadImage是定义的上传文件到后端的接口,根据自己的项目更换 |
19 | |
20 | class AppEditor extends Component { |
21 | state = { |
22 | // 创建一个空的editorState作为初始值 |
23 | editorState: BraftEditor.createEditorState(null) |
24 | }; |
25 | |
26 | static propTypes = { |
27 | // 富文本初始内容 |
28 | html: PropTypes.string, |
29 | // 限制图片文件大小 |
30 | fileMaxSize: PropTypes.number |
31 | }; |
32 | |
33 | // 默认的props |
34 | static defaultProps = { |
35 | fileMaxSize: 2 // 单位默认是Mb, |
36 | }; |
37 | |
38 | componentWillUnmount() { |
39 | this.setState = (state, callback) => { |
40 | return; |
41 | }; |
42 | } |
43 | |
44 | // 初始化编辑器内容 |
45 | async componentWillReceiveProps(props) { |
46 | try { |
47 | const { html } = props; |
48 | const { data } = await axios.get(html); |
49 | this.setState({ |
50 | editorState: BraftEditor.createEditorState(data) |
51 | }); |
52 | return data; |
53 | } catch (error) { |
54 | console.log(error); |
55 | } |
56 | } |
57 | |
58 | // 子组件上传方法(供父组件调用) |
59 | handleSubmitContent = async () => { |
60 | const htmlContent = this.state.editorState.toHTML(); |
61 | try { |
62 | const url = await uploadHtml(htmlContent); |
63 | return url; |
64 | } catch (error) { |
65 | console.log(error); |
66 | } |
67 | }; |
68 | |
69 | // 监听编辑器内容变化同步内容到state |
70 | handleEditorChange = editorState => { |
71 | this.setState({ editorState }); |
72 | }; |
73 | |
74 | // 媒体上传校验 |
75 | mediaValidate = file => { |
76 | const { fileMaxSize } = this.props; |
77 | |
78 | // 类型限制 |
79 | if (!isImageFile(file)) { |
80 | return false; |
81 | } |
82 | |
83 | // 大小限制 |
84 | if (fileMaxSize && !maxFileSize(file, fileMaxSize)) { |
85 | return false; |
86 | } |
87 | |
88 | return true; |
89 | }; |
90 | |
91 | // 媒体上传 |
92 | mediaUpload = async ({ file, success, error }) => { |
93 | message.destroy(); |
94 | try { |
95 | const url = await uploadImage(file); |
96 | success({ url }); |
97 | message.success("上传成功"); |
98 | } catch (err) { |
99 | error(err); |
100 | message.error(err.message || "上传失败"); |
101 | } |
102 | }; |
103 | |
104 | render() { |
105 | const { editorState } = this.state; |
106 | |
107 | // 媒体配置 |
108 | const media = { |
109 | // 上传校验 |
110 | validateFn: this.mediaValidate, |
111 | // 上传 |
112 | uploadFn: this.mediaUpload, |
113 | // 基本配置 |
114 | ...mediaBaseconfig |
115 | }; |
116 | |
117 | return ( |
118 | <div |
119 | className="custom-editor" |
120 | style={{ marginTop: 20, ...this.props.style }} |
121 | > |
122 | <BraftEditor |
123 | style={{ height: 600, overflow: "hidden" }} |
124 | {...config} |
125 | media={media} |
126 | value={editorState} |
127 | onChange={this.handleEditorChange} |
128 | /> |
129 | </div> |
130 | ); |
131 | } |
132 | } |
133 |
|
134 | export default AppEditor; |
src/components/AppEditor/config.js
1 | const config = { |
2 | placeholder: "请输入内容", |
3 | |
4 | // 编辑器工具栏的控件列表 |
5 | controls: [ |
6 | "headings", |
7 | "font-size", |
8 | "letter-spacing", |
9 | "separator", // 分割线 |
10 | |
11 | "text-color", |
12 | "bold", |
13 | "italic", |
14 | "underline", |
15 | "strike-through", |
16 | "remove-styles", |
17 | "separator", |
18 | "text-align", |
19 | "separator", |
20 | "emoji", |
21 | "media" |
22 | ], |
23 | |
24 | // 字号配置 |
25 | fontSizes: [12, 14, 16, 18, 20, 24, 28, 30, 32, 36], |
26 | |
27 | // 图片工具栏的可用控件 |
28 | imageControls: [ |
29 | "float-left", // 设置图片左浮动 |
30 | "float-right", // 设置图片右浮动 |
31 | "align-left", // 设置图片居左 |
32 | "align-center", // 设置图片居中 |
33 | "align-right", // 设置图片居右 |
34 | "link", // 设置图片超链接 |
35 | "size", // 设置图片尺寸 |
36 | "remove" // 删除图片 |
37 | ] |
38 | }; |
39 | export default config; |
src/components/AppEditor/media.js
1 | const mediaBaseconfig = { |
2 | // 文件限制 |
3 | accepts: { |
4 | image: "image/png,image/jpeg,image/gif,image/webp,image/apng,image/svg", |
5 | video: false, |
6 | audio: false |
7 | }, |
8 | |
9 | // 允许插入的外部媒体的类型 |
10 | externals: { |
11 | // 是否允许插入外部图片, |
12 | image: false, |
13 | // 是否允许插入外部视频, |
14 | video: false, |
15 | // 是否允许插入外部视频, |
16 | audio: false, |
17 | // 是否允许插入嵌入式媒体,例如embed和iframe标签等, |
18 | embed: false |
19 | } |
20 | }; |
21 | |
22 | export default mediaBaseconfig; |
src/components/AppEditor/style.less
1 | :global(.custom-editor .bf-controlbar) { |
2 | background-color: #fff; |
3 | } |
4 | |
5 | :global(.custom-editor .bf-container) { |
6 | border: 1px solid #c5c5c5; |
7 | } |
8 | |
9 | :global(.custom-editor .bf-content) { |
10 | min-height: 600px; |
11 | } |
src/utils/upload.js
1 | import { message } from "antd"; |
2 | |
3 | /** |
4 | * |
5 | * @param {file} file 源文件 |
6 | * @desc 限制为图片文件 |
7 | * @retutn 是图片文件返回true否则返回false |
8 | */ |
9 | |
10 | export const isImageFile = (file, fileTypes) => { |
11 | const types = fileTypes || [ |
12 | "image/png", |
13 | "image/gif", |
14 | "image/jpeg", |
15 | "image/jpg", |
16 | "image/bmp", |
17 | "image/x-icon", |
18 | "image/webp", |
19 | "image/apng", |
20 | "image/svg" |
21 | ]; |
22 | |
23 | const isImage = types.includes(file.type); |
24 | if (!isImage) { |
25 | message.error("上传文件非图片格式!"); |
26 | return false; |
27 | } |
28 | |
29 | return true; |
30 | }; |
31 | |
32 | /** |
33 | * |
34 | * @param {file} file 源文件 |
35 | * @param {number} fileMaxSize 图片限制大小单位(MB) |
36 | * @desc 限制为文件上传大小 |
37 | * @retutn 在限制内返回true否则返回false |
38 | */ |
39 | |
40 | export const maxFileSize = (file, fileMaxSize = 2) => { |
41 | const isMaxSize = file.size / 1024 / 1024 < fileMaxSize; |
42 | if (!isMaxSize) { |
43 | message.error("上传头像图片大小不能超过 " + fileMaxSize + "MB!"); |
44 | return false; |
45 | } |
46 | return true; |
47 | }; |
48 | |
49 | /** |
50 | * |
51 | * @param {file} file 源文件 |
52 | * @desc 读取图片文件为base64文件格式 |
53 | * @retutn 返回base64文件 |
54 | */ |
55 | // 读取文件 |
56 | export const readFile = file => { |
57 | return new Promise((resolve, reject) => { |
58 | const reader = new FileReader(); |
59 | reader.onload = e => { |
60 | const data = e.target.result; |
61 | resolve(data); |
62 | }; |
63 | reader.onerror = () => { |
64 | const err = new Error("读取图片失败"); |
65 | reject(err.message); |
66 | }; |
67 | |
68 | reader.readAsDataURL(file); |
69 | }); |
70 | }; |
71 | |
72 | /** |
73 | * |
74 | * @param {string} src 图片地址 |
75 | * @desc 加载真实图片 |
76 | * @return 读取成功返回图片真实宽高对象 ag: {width:100,height:100} |
77 | */ |
78 | |
79 | export const loadImage = src => { |
80 | return new Promise((resolve, reject) => { |
81 | const image = new Image(); |
82 | image.src = src; |
83 | image.onload = () => { |
84 | const data = { |
85 | width: image.width, |
86 | height: image.height |
87 | }; |
88 | resolve(data); |
89 | }; |
90 | image.onerror = () => { |
91 | const err = new Error("加载图片失败"); |
92 | reject(err); |
93 | }; |
94 | }); |
95 | }; |
96 | |
97 | /** |
98 | * |
99 | * @param {file} file 源文件 |
100 | * @param {object} props 文件分辨率的宽和高 ag: props={width:100, height :100} |
101 | * @desc 判断图片文件的分辨率是否在限定范围之内 |
102 | * @throw 分辨率不在限定范围之内则抛出异常 |
103 | * |
104 | */ |
105 | export const isAppropriateResolution = async (file, props) => { |
106 | try { |
107 | const { width, height } = props; |
108 | const base64 = await readFile(file); |
109 | const image = await loadImage(base64); |
110 | if (image.width !== width || image.height !== height) { |
111 | throw new Error("上传图片的分辨率必须为" + width + "*" + height); |
112 | } |
113 | } catch (error) { |
114 | throw error; |
115 | } |
116 | }; |
117 | |
118 | // 上传图片 根据自己项目更换 |
119 | export const uploadImage = file => { |
120 | return new Promise((resolve, reject) => {}); |
121 | }; |
122 | |
123 | // 上传html 根据自己项目更换 |
124 | export const uploadHtml = editorContent => { |
125 | return new Promise((resolve, reject) => {}); |
126 | }; |
2.使用
1 | import AppEditor from "@/components/AppEditor"; |
2 | import React, { Component } from "react"; |
3 | export default class HotelSetting extends Component { |
4 | state = { |
5 | html: "" |
6 | }; |
7 | |
8 | componentDidMount() { |
9 | this.setState({ |
10 | html: |
11 | "https://hotel-1259479103.cos.ap-chengdu.myqcloud.com/admin/html/r2yyo8dyjrla71bn8qm5g-2019-07-11-10-34-19.html" |
12 | }); |
13 | } |
14 | |
15 | handleSubmit = async () => { |
16 | try { |
17 | const res = await this.editor.handleSubmitContent(); |
18 | console.log(res); |
19 | } catch (error) { |
20 | console.log(error); |
21 | } |
22 | }; |
23 | |
24 | render() { |
25 | const { html } = this.state; |
26 | return ( |
27 | <AppEditor |
28 | html={html} |
29 | style={{ marginLeft: 100 }} |
30 | ref={e => { |
31 | this.editor = e; |
32 | }} |
33 | /> |
34 | ); |
35 | } |
36 | } |
3.使用效果