富文本编辑器TinyMCE的使用(React Vue)
一,需求与介绍
1.1,需求
编辑新闻等富有个性化的文本
1.2,介绍
TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。
TinyMCE的优势:
- 开源可商用,基于LGPL2.1
- 插件丰富,自带插件基本涵盖日常所需功能
- 接口丰富,可扩展性强,有能力可以无限拓展功能
- 界面好看,符合现代审美
- 提供经典、内联、沉浸无干扰三种模式
- 对标准支持优秀(自v5开始)
- 多语言支持,官网可下载几十种语言。
二,配置集成并组件化
2.1,通用配置
1,工具栏toolbar
1 // Here is a list of the toolbar 2 // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols 3 4 // const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor']// fullscreen 5 const toolbar = ['code codesample undo redo restoredraft | cut copy paste pastetext | forecolor backcolor searchreplace bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | formatselect fontselect fontsizeselect | blockquote subscript superscript removeformat | table image media charmap emoticons hr pagebreak insertdatetime print preview']// | fullscreen 6 export default toolbar
2,插件plugins
1 // Any plugins you want to use has to be imported 2 // Detail plugins list see https://www.tinymce.com/docs/plugins/ 3 // Custom builds see https://www.tinymce.com/download/custom-builds/ 4 5 // const plugins = ['advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount'] 6 const plugins = ['print preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount imagetools textpattern help paste emoticons autosave'] 7 8 export default plugins
3,常用字体配置fonts
1 // Any font you want to use has to be imported 2 const fontsizeFormats='12px 14px 16px 18px 24px 36px 48px 56px 72px'; 3 const fontFormats= '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats;知乎配置=BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif;小米配置=Helvetica Neue,Helvetica,Arial,Microsoft Yahei,Hiragino Sans GB,Heiti SC,WenQuanYi Micro Hei,sans-serif'; 4 5 export default { 6 fontsizeFormats, 7 fontFormats 8 }
4,准备标签
1 <div> 2 <textarea id="tinymceId"/> 3 div>
在textarea外面需要套一层div,否则会产生一些意想不到的问题
5,初始化标签,生成编辑框
1 window.tinymce.init({ 2 language: 'zh_CN', 3 selector: `#${tinymceId}`, 4 height: height, 5 body_class: 'panel-body ', 6 object_resizing: false, 7 toolbar: toolbar.length > 0 ? toolbar : defaultToolbar, 8 menubar: menubar, 9 plugins: defaultplugins, 10 end_container_on_empty_block: true, 11 fontsize_formats: fontsizeFormats, 12 font_formats: fontFormats, 13 powerpaste_word_import: 'clean', 14 code_dialog_height: 450, 15 code_dialog_width: 1000, 16 advlist_bullet_styles: 'square', 17 advlist_number_styles: 'default', 18 imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], 19 default_link_target: '_blank', 20 link_title: false, 21 nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin 22 init_instance_callback: editor => { 23 if (content) { 24 editor.setContent(content) 25 } 26 editor.on('NodeChange Change KeyUp SetContent', () => { 27 }) 28 }, 29 setup(editor) { 30 editor.on('FullscreenStateChanged', (e) => { 31 }) 32 } 33 })
2.2,React组件化
直接放代码
1 import React from 'react'; 2 import { Button } from 'antd'; 3 import PropTypes from 'prop-types'; 4 import styles from './index.less'; 6 import defaultplugins from './plugins'; 7 import defaultToolbar from './toolbar'; 8 import { 9 fontsizeFormats, 10 fontFormats 11 } from './font'; 12 import UploadImage from './UploadImage'; 13 14 15 class Tinymce extends React.Component { 16 17 static propTypes = { 18 tinymceId: PropTypes.string, 19 content: PropTypes.string, 20 toolbar: PropTypes.array, 21 menubar: PropTypes.string, 22 height: PropTypes.number, 23 getContent: PropTypes.func, 24 }; 25 static defaultProps = { 26 tinymceId: 'react-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + ''), 27 menubar: 'file edit insert view format table', 28 height: 520, 29 toolbar: [] 30 }; 31 constructor(props) { 32 super(props); 33 this.state = { 34 hasChange: false, 35 hasInit: false, 36 fullscreen: false, 37 }; 38 }; 39 40 componentDidMount() { 41 this.initTinymce() 42 43 } 44 componentWillUnmount() { 45 this.destroyTinymce() 46 } 47 initTinymce() { 48 const { tinymceId, menubar, height, toolbar, content, getContent } = this.props 49 const _this = this 50 window.tinymce.init({ 51 language: 'zh_CN', 52 selector: `#${tinymceId}`, 53 height: height, 54 body_class: 'panel-body ', 55 object_resizing: false, 56 toolbar: toolbar.length > 0 ? toolbar : defaultToolbar, 57 menubar: menubar, 58 plugins: defaultplugins, 59 end_container_on_empty_block: true, 60 fontsize_formats: fontsizeFormats, 61 font_formats: fontFormats, 62 powerpaste_word_import: 'clean', 63 code_dialog_height: 450, 64 code_dialog_width: 1000, 65 advlist_bullet_styles: 'square', 66 advlist_number_styles: 'default', 67 imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], 68 default_link_target: '_blank', 69 link_title: false, 70 nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin 71 init_instance_callback: editor => { 72 if (content) { 73 editor.setContent(content) 74 } 75 _this.setState({ 76 hasInit: true 77 }) 78 editor.on('NodeChange Change KeyUp SetContent', () => { 79 _this.setState({ 80 hasChange: true 81 }) 82 }) 83 }, 84 setup(editor) { 85 editor.on('FullscreenStateChanged', (e) => { 86 _this.setState({ 87 fullscreen: e.state 88 }) 89 }) 90 } 91 }) 92 } 93 destroyTinymce() { 94 const { tinymceId } = this.props 95 const { fullscreen } = this.state 96 const tinymce = window.tinymce.get(tinymceId) 97 if (fullscreen) { 98 tinymce.execCommand('mceFullScreen') 99 } 100 101 if (tinymce) { 102 tinymce.destroy() 103 } 104 } 105 // setContent(value) { 106 // const { tinymceId } = this.props 107 // window.tinymce.get(tinymceId).setContent(value) 108 // } 109 saveToGetContent() { 110 const { tinymceId, getContent } = this.props 111 if (getContent && typeof getContent === 'function') { 112 getContent(window.tinymce.get(tinymceId).getContent()) 113 } 114 } 115 116 /** 117 * 上传图片成功回调 118 * */ 119 imageSuccessCBK(arr) { 120 const { tinymceId } = this.props 121 arr.forEach(v => { 122 window.tinymce.get(tinymceId).insertContent(``) 123 }) 124 } 125 render() { 126 const { loading, tinymceId } = this.props 127 const { fullscreen } = this.state 128 const header = ( 129130 ); 131 return ( 132 133147 ); 148 } 149 } 150 151 export default Tinymce;134 135 146136 137145138 139140141143 144{this.imageSuccessCBK(arr)}}/> 142
上传图片组件,使用antd的部分组件:
1 import React from 'react'; 2 import { Button, Modal, Icon, Upload, message } from 'antd'; 3 import PropTypes from 'prop-types'; 4 import styles from './index.less'; 5 6 const Dragger = Upload.Dragger; 7 8 class UploadImage extends React.Component { 9 10 static propTypes = { 11 imageSuccessCBK: PropTypes.func, 12 13 }; 14 static defaultProps = { 15 16 }; 17 constructor(props) { 18 super(props); 19 this.state = { 20 visible: false, 21 listObj: {} 22 }; 23 }; 24 25 /** 26 * 显示弹框 27 * 28 * */ 29 showModal = () => { 30 this.setState({ 31 visible: true, 32 }); 33 } 34 35 /** 36 * 确认 37 * 38 * */ 39 handleOk = (e) => { 40 const { imageSuccessCBK } = this.props 41 const { listObj } = this.state 42 const imagesFileArr = Object.keys(listObj).map(v => listObj[v]) 43 imageSuccessCBK(imagesFileArr) 44 this.setState({ 45 visible: false, 46 listObj: {}, 47 Uploading: false 48 }); 49 } 50 51 handleCancel = (e) => { 52 this.setState({ 53 visible: false, 54 listObj: {} 55 }); 56 } 57 render() { 58 const { loading } = this.props 59 const { visible, listObj, Uploading } = this.state 60 const props = { 61 name: 'file', 62 multiple: true, 63 action: '//jsonplaceholder.typicode.com/posts/', 64 listType: 'picture', 65 onChange: (info) => { 66 const uid = info.file.uid 67 const objKeyArr = Object.keys(listObj) 68 const status = info.file.status; 69 if (status !== 'uploading') { 70 console.log(info.file, info.fileList); 71 } 72 if (status === 'done') {//已成功上传 73 this.setState({ 74 Uploading: false, 75 }) 76 for (let i = 0, len = objKeyArr.length; i < len; i++) { 77 if (listObj[objKeyArr[i]].uid === uid) { 78 listObj[objKeyArr[i]].url = info.file.thumbUrl 79 listObj[objKeyArr[i]].hasSuccess = true 80 message.success(`${info.file.name} file uploaded successfully.`); 81 return 82 } 83 } 84 85 } else if (status === 'error') { 86 this.setState({ 87 Uploading: false, 88 }) 89 message.error(`${info.file.name} file upload failed.`); 90 } 91 if (status === 'removed') {//移除上传的 92 for (let i = 0, len = objKeyArr.length; i < len; i++) { 93 if (listObj[objKeyArr[i]].uid === uid) { 94 delete listObj[objKeyArr[i]] 95 message.success(`${info.file.name} file removed successfully.`); 96 return 97 } 98 } 99 } 100 }, 101 beforeUpload: (file) => { 102 this.setState({ 103 Uploading: true, 104 }) 105 const _self = this 106 const _URL = window.URL || window.webkitURL 107 const fileName = file.uid 108 listObj[fileName] = {} 109 return new Promise((resolve, reject) => { 110 const img = new Image() 111 img.src = _URL.createObjectURL(file) 112 img.onload = function () { 113 listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height } 114 _self.setState({ 115 listObj, 116 }) 117 } 118 resolve(true) 119 }) 120 }, 121 }; 122 123 return ( 124125 <Button 126 style={{ marginTop: 0 }} 127 type="primary" 128 shape="round" 129 icon="upload" 130 onClick={() => { this.showModal() }}> 131 上传 132 133 { 134 visible ? <Modal 135 title="上传图片" 136 visible={visible} 137 onCancel={this.handleCancel} 138 footer={[ 139156 ); 157 } 158 } 159 160 export default UploadImage;140 141 144]} 145 > 146147 153 : null 154 } 155148
150149 Click or drag file to this area to upload
151Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files
152
2.3,Vue组件化
直接放代码
1 2310 11 12 156 1574 56798
上传图片组件,使用elementUI的部分组件:
1 2330 31 32 103 1044 upload 5 67 29upload 8 :multiple="true" 9 :file-list="fileList" 10 :show-file-list="true" 11 :on-remove="handleRemove" 12 :on-success="handleSuccess" 13 :before-upload="beforeUpload" 14 class="editor-slide-upload" 15 action="https://httpbin.org/post" 16 list-type="picture-card" 17 > 18 19 Click upload 20 21 2223 Cancel 24 2526 Confirm 27 28
三,使用
3.1,React
第一步:导入组件
1 import Tinymce from '../../components/Tinymce';
第二步:使用组件
1 <Tinymce 2 content={''} 3 tinymceId='tinymceIdDemo' 4 getContent={(content) => { this.getContent(content) }} 5 />
第三步:获取输入的富文本
1 getContent(content) { 2 console.log('content===',content) 3 this.setState({ 4 content 5 }) 6 }
第四步:文本渲染
1 {/* 渲染标签字符串 */} 2 <div dangerouslySetInnerHTML={{ __html: content }}>div>
第五步:效果
图片上传成功效果
完成效果展示
3.2,Vue
第一步:引入组件
1 import Tinymce from '@/components/Tinymce'
第二步:注册组件
1 components: { Tinymce },
第三步:使用组件
123
第四步:获取内容
利用vue的数据双向绑定
1 getContent() { 2 console.log(this.content) 3 this.hasContent = this.content 4 }
第五步:渲染获取的内容
1
第六步:效果图
图片上传成功效果
完整效果