富文本編輯器TinyMCE的使用(React Vue)

富文本編輯器TinyMCE的使用(React Vue)

一,需求與介紹

 1.1,需求

     編輯新聞等富有個性化的文本css

 1.2,介紹

TinyMCE是一款易用、且功能強大的所見即所得的富文本編輯器。html

TinyMCE的優點:vue

  • 開源可商用,基於LGPL2.1
  • 插件豐富,自帶插件基本涵蓋平常所需功能
  • 接口豐富,可擴展性強,有能力能夠無限拓展功能
  • 界面好看,符合現代審美
  • 提供經典、內聯、沉浸無干擾三種模式
  • 對標準支持優秀(自v5開始)
  • 多語言支持,官網可下載幾十種語言。

二,配置集成並組件化

2.1,通用配置

1,工具欄toolbarreact

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,插件pluginsweb

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,經常使用字體配置fontsjson

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,準備標籤api

1   <div>
2               <textarea id="tinymceId"/>
3             </div>

在textarea外面須要套一層div,不然會產生一些意想不到的問題antd

5,初始化標籤,生成編輯框cors

 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 &nbsp; 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組件化

 直接放代碼less

  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 &nbsp; 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(`<img class="wscnph" src="${v.url}" >`)
123     })
124   }
125   render() {
126     const { loading, tinymceId } = this.props
127     const { fullscreen } = this.state
128     const header = (
129       <Page.Header breadcrumb={['富文本實例']} title={'富文本實例'} />
130     );
131     return (
132       <div className={styles['tinymce-components']}>
133         <Page header={header} loading={!!loading}>
134 
135           <div className={fullscreen ? "tinymce-container mce-fullscreen" : "tinymce-container"}>
136 
137             <div>
138               <textarea id={tinymceId} className={styles['tinymce-textarea']} />
139             </div>
140             <div className="editor-custom-btn-container">
141               <UploadImage className="editor-upload-btn" imageSuccessCBK={(arr)=>{this.imageSuccessCBK(arr)}}/>
142             </div>
143             <Button type="primary" onClick={() => { this.saveToGetContent() }}>保存</Button>
144           </div>
145         </Page>
146       </div>
147     );
148   }
149 }
150 
151 export default Tinymce;

上傳圖片組件,使用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 (
124       <div>
125         <Button
126           style={{ marginTop: 0 }}
127           type="primary"
128           shape="round"
129           icon="upload"
130           onClick={() => { this.showModal() }}>
131           上傳
132            </Button>
133         {
134           visible ? <Modal
135             title="上傳圖片"
136             visible={visible}
137             onCancel={this.handleCancel}
138             footer={[
139               <div key="1">
140                 <Button onClick={() => this.handleCancel()} loading={!!Uploading}>取消</Button>
141                 <Button type="primary" style={{ marginLeft: 8 }} onClick={() => this.handleOk()} loading={!!Uploading}>
142                   肯定
143                 </Button>
144               </div>]}
145           >
146             <Dragger {...props}>
147               <p className="ant-upload-drag-icon">
148                 <Icon type="inbox" />
149               </p>
150               <p className="ant-upload-text">Click or drag file to this area to upload</p>
151               <p className="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>
152             </Dragger>
153           </Modal> : null
154         }
155       </div>
156     );
157   }
158 }
159 
160 export default UploadImage;

2.3,Vue組件化

  直接放代碼

  1 <template>
  2   <div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
  3     <div>
  4     <textarea :id="tinymceId" class="tinymce-textarea" />
  5     </div>
  6     <div class="editor-custom-btn-container">
  7       <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
  8     </div>
  9   </div>
 10 </template>
 11 
 12 <script>
 13 import editorImage from './components/EditorImage'
 14 import plugins from './plugins'
 15 import toolbar from './toolbar'
 16 import font from './font';
 17 
 18 export default {
 19   name: 'Tinymce',
 20   components: { editorImage },
 21   props: {
 22     id: {
 23       type: String,
 24       default: function() {
 25         return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
 26       }
 27     },
 28     value: {
 29       type: String,
 30       default: ''
 31     },
 32     toolbar: {
 33       type: Array,
 34       required: false,
 35       default() {
 36         return []
 37       }
 38     },
 39     menubar: {
 40       type: String,
 41       default: 'file edit insert view format table'
 42     },
 43     height: {
 44       type: Number,
 45       required: false,
 46       default: 520
 47     }
 48   },
 49   data() {
 50     return {
 51       hasChange: false,
 52       hasInit: false,
 53       tinymceId: this.id,
 54       fullscreen: false,
 55       languageTypeList: {
 56         'en': 'en',
 57         'zh': 'zh_CN'
 58       }
 59     }
 60   },
 61   computed: {
 62     language() {
 63       return this.languageTypeList[this.$store.getters.language]
 64     }
 65   },
 66   watch: {
 67     value(val) {
 68       if (!this.hasChange && this.hasInit) {
 69         this.$nextTick(() =>
 70           window.tinymce.get(this.tinymceId).setContent(val || ''))
 71       }
 72     },
 73     language() {
 74       this.destroyTinymce()
 75       this.$nextTick(() => this.initTinymce())
 76     }
 77   },
 78   mounted() {
 79     this.initTinymce()
 80   },
 81   activated() {
 82     this.initTinymce()
 83   },
 84   deactivated() {
 85     this.destroyTinymce()
 86   },
 87   destroyed() {
 88     this.destroyTinymce()
 89   },
 90   methods: {
 91     initTinymce() {
 92       const _this = this
 93       window.tinymce.init({
 94         language: 'zh_CN',
 95         selector: `#${this.tinymceId}`,
 96         height: this.height,
 97         body_class: 'panel-body ',
 98         object_resizing: false,
 99         toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
100         menubar: this.menubar,
101         plugins: plugins,
102         end_container_on_empty_block: true,
103         fontsize_formats: font.fontsizeFormats,
104         font_formats: font.fontFormats,
105         powerpaste_word_import: 'clean',
106         code_dialog_height: 450,
107         code_dialog_width: 1000,
108         advlist_bullet_styles: 'square',
109         advlist_number_styles: 'default',
110         imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
111         default_link_target: '_blank',
112         link_title: false,
113         nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
114         init_instance_callback: editor => {
115           if (_this.value) {
116             editor.setContent(_this.value)
117           }
118           _this.hasInit = true
119           editor.on('NodeChange Change KeyUp SetContent', () => {
120             this.hasChange = true
121             this.$emit('input', editor.getContent())
122           })
123         },
124         setup(editor) {
125           editor.on('FullscreenStateChanged', (e) => {
126             _this.fullscreen = e.state
127           })
128         }
129       })
130     },
131     destroyTinymce() {
132       const tinymce = window.tinymce.get(this.tinymceId)
133       if (this.fullscreen) {
134         tinymce.execCommand('mceFullScreen')
135       }
136 
137       if (tinymce) {
138         tinymce.destroy()
139       }
140     },
141     setContent(value) {
142       window.tinymce.get(this.tinymceId).setContent(value)
143     },
144     getContent() {
145       window.tinymce.get(this.tinymceId).getContent()
146     },
147     imageSuccessCBK(arr) {
148       const _this = this
149       arr.forEach(v => {
150         window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
151       })
152     }
153   }
154 }
155 </script>
156 
157 <style scoped>
158 .tinymce-container {
159   position: relative;
160   line-height: normal;
161 }
162 .tinymce-container>>>.mce-fullscreen {
163   z-index: 10000;
164 }
165 .tinymce-textarea {
166   visibility: hidden;
167   z-index: -1;
168 }
169 .editor-custom-btn-container {
170   position: absolute;
171   right: 4px;
172   top: 4px;
173   /*z-index: 2005;*/
174 }
175 .fullscreen .editor-custom-btn-container {
176   z-index: 10000;
177   position: fixed;
178 }
179 .editor-upload-btn {
180   display: inline-block;
181 }
182 </style>

上傳圖片組件,使用elementUI的部分組件:

  1 <template>
  2   <div class="upload-container">
  3     <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
  4       upload
  5     </el-button>
  6     <el-dialog :visible.sync="dialogVisible">
  7       <el-upload
  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         <el-button size="small" type="primary">
 19           Click upload
 20         </el-button>
 21       </el-upload>
 22       <el-button @click="dialogVisible = false">
 23         Cancel
 24       </el-button>
 25       <el-button type="primary" @click="handleSubmit">
 26         Confirm
 27       </el-button>
 28     </el-dialog>
 29   </div>
 30 </template>
 31 
 32 <script>
 33 // import { getToken } from 'api/qiniu'
 34 
 35 export default {
 36   name: 'EditorSlideUpload',
 37   props: {
 38     color: {
 39       type: String,
 40       default: '#1890ff'
 41     }
 42   },
 43   data() {
 44     return {
 45       dialogVisible: false,
 46       listObj: {},
 47       fileList: []
 48     }
 49   },
 50   methods: {
 51     checkAllSuccess() {
 52       return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
 53     },
 54     handleSubmit() {
 55       const arr = Object.keys(this.listObj).map(v => this.listObj[v])
 56       if (!this.checkAllSuccess()) {
 57         this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
 58         return
 59       }
 60       this.$emit('successCBK', arr)
 61       this.listObj = {}
 62       this.fileList = []
 63       this.dialogVisible = false
 64     },
 65     handleSuccess(response, file) {
 66       const uid = file.uid
 67       const objKeyArr = Object.keys(this.listObj)
 68       for (let i = 0, len = objKeyArr.length; i < len; i++) {
 69         if (this.listObj[objKeyArr[i]].uid === uid) {
 70           this.listObj[objKeyArr[i]].url = response.files.file
 71           this.listObj[objKeyArr[i]].hasSuccess = true
 72           return
 73         }
 74       }
 75     },
 76     handleRemove(file) {
 77       const uid = file.uid
 78       const objKeyArr = Object.keys(this.listObj)
 79       for (let i = 0, len = objKeyArr.length; i < len; i++) {
 80         if (this.listObj[objKeyArr[i]].uid === uid) {
 81           delete this.listObj[objKeyArr[i]]
 82           return
 83         }
 84       }
 85     },
 86     beforeUpload(file) {
 87       const _self = this
 88       const _URL = window.URL || window.webkitURL
 89       const fileName = file.uid
 90       this.listObj[fileName] = {}
 91       return new Promise((resolve, reject) => {
 92         const img = new Image()
 93         img.src = _URL.createObjectURL(file)
 94         img.onload = function() {
 95           _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
 96         }
 97         resolve(true)
 98       })
 99     }
100   }
101 }
102 </script>
103 
104 <style lang="scss" scoped>
105 .editor-slide-upload {
106   margin-bottom: 20px;
107   /deep/ .el-upload--picture-card {
108     width: 100%;
109   }
110 }
111 </style>

三,使用

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 },

  第三步:使用組件

1  <div>
2       <tinymce v-model="content" :height="300" />
3     </div>

  第四步:獲取內容

利用vue的數據雙向綁定

1  getContent() {
2       console.log(this.content)
3       this.hasContent = this.content
4     }

  第五步:渲染獲取的內容

1 <div class="editor-content" v-html="hasContent" />

  第六步:效果圖

圖片上傳成功效果

完整效果

相關文章
相關標籤/搜索