編輯新聞等富有個性化的文本css
TinyMCE是一款易用、且功能強大的所見即所得的富文本編輯器。html
TinyMCE的優點:vue
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 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 })
直接放代碼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 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;
直接放代碼
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 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>
第一步:導入組件
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>
第五步:效果
圖片上傳成功效果
完成效果展現
第一步:引入組件
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" />
第六步:效果圖
圖片上傳成功效果
完整效果