最近想在項目上引入一個富文本編輯器,以前引入過summernote,感受並不太適合vue使用,css
而後在網上查了查,vue中使用Tinymce比較適合,html
首先,咱們在vue項目的components文件夾中加入以下幾個文件vue
首先看一下Tinymce/dynamicLoadScript.js的內容:git
let callbacks = [] function loadedTinymce() { // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 // check is successfully downloaded script return window.tinymce } const dynamicLoadScript = (src, callback) => { const existingScript = document.getElementById(src) const cb = callback || function() {} if (!existingScript) { const script = document.createElement('script') script.src = src // src url for the third-party library being loaded. script.id = src document.body.appendChild(script) callbacks.push(cb) const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd onEnd(script) } if (existingScript && cb) { if (loadedTinymce()) { cb(null, existingScript) } else { callbacks.push(cb) } } function stdOnEnd(script) { script.onload = function() { // this.onload = null here is necessary // because even IE9 works not like others this.onerror = this.onload = null for (const cb of callbacks) { cb(null, script) } callbacks = null } script.onerror = function() { this.onerror = this.onload = null cb(new Error('Failed to load ' + src), script) } } function ieOnEnd(script) { script.onreadystatechange = function() { if (this.readyState !== 'complete' && this.readyState !== 'loaded') return this.onreadystatechange = null for (const cb of callbacks) { cb(null, script) // there is no way to catch loading errors in IE8 } callbacks = null } } } export default dynamicLoadScript
再來看一下Tinymce/components/EditorImage.vue中的內容程序員
<template> <div class="upload-container"> <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true"> upload </el-button> <el-dialog :visible.sync="dialogVisible"> <el-upload :multiple="true" :file-list="fileList" :show-file-list="true" :on-remove="handleRemove" :on-success="handleSuccess" :before-upload="beforeUpload" class="editor-slide-upload" action="https://httpbin.org/post" list-type="picture-card" > <el-button size="small" type="primary"> Click upload </el-button> </el-upload> <el-button @click="dialogVisible = false"> Cancel </el-button> <el-button type="primary" @click="handleSubmit"> Confirm </el-button> </el-dialog> </div> </template> <script> // import { getToken } from 'api/qiniu' export default { name: 'EditorSlideUpload', props: { color: { type: String, default: '#1890ff' } }, data() { return { dialogVisible: false, listObj: {}, fileList: [] } }, methods: { checkAllSuccess() { return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess) }, handleSubmit() { const arr = Object.keys(this.listObj).map(v => this.listObj[v]) if (!this.checkAllSuccess()) { this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!') return } this.$emit('successCBK', arr) this.listObj = {} this.fileList = [] this.dialogVisible = false }, handleSuccess(response, file) { const uid = file.uid const objKeyArr = Object.keys(this.listObj) for (let i = 0, len = objKeyArr.length; i < len; i++) { if (this.listObj[objKeyArr[i]].uid === uid) { this.listObj[objKeyArr[i]].url = response.files.file this.listObj[objKeyArr[i]].hasSuccess = true return } } }, handleRemove(file) { const uid = file.uid const objKeyArr = Object.keys(this.listObj) for (let i = 0, len = objKeyArr.length; i < len; i++) { if (this.listObj[objKeyArr[i]].uid === uid) { delete this.listObj[objKeyArr[i]] return } } }, beforeUpload(file) { const _self = this const _URL = window.URL || window.webkitURL const fileName = file.uid this.listObj[fileName] = {} return new Promise((resolve, reject) => { const img = new Image() img.src = _URL.createObjectURL(file) img.onload = function() { _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height } } resolve(true) }) } } } </script> <style lang="scss" scoped> .editor-slide-upload { margin-bottom: 20px; /deep/ .el-upload--picture-card { width: 100%; } } </style>
Tinymce/plugins.js中的內容:github
// Any plugins you want to use has to be imported // Detail plugins list see https://www.tinymce.com/docs/plugins/ // Custom builds see https://www.tinymce.com/download/custom-builds/ const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] export default plugins
Tinymce/toolbar.js的內容:web
// Here is a list of the toolbar // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols const toolbar = ['searchreplace bold fontsizeselect 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'] export default toolbar
在看看Tinymce/index.vue中的內容:npm
<template> <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}"> <textarea :id="tinymceId" class="tinymce-textarea" /> <!-- <div class="editor-custom-btn-container"> <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" /> </div> --> </div> </template> <script> /** * docs: * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce */ import editorImage from './components/EditorImage' import plugins from './plugins' import toolbar from './toolbar' import load from './dynamicLoadScript' // why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one const tinymceCDN = 'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js' export default { name: 'Tinymce', components: { editorImage }, props: { id: { type: String, default: function() { return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '') } }, value: { type: String, default: '' }, toolbar: { type: Array, required: false, default() { return [] } }, menubar: { type: String, default: 'file edit insert view format table' }, height: { type: [Number, String], required: false, default: 360 }, width: { type: [Number, String], required: false, default: 'auto' } }, data() { return { hasChange: false, hasInit: false, tinymceId: this.id, fullscreen: false, languageTypeList: { 'en': 'en', 'zh': 'zh_CN', 'es': 'es_MX', 'ja': 'ja' } } }, computed: { containerWidth() { const width = this.width if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'` return `${width}px` } return width } }, watch: { value(val) { if (!this.hasChange && this.hasInit) { this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(val || '')) } } }, mounted() { this.init() }, activated() { if (window.tinymce) { this.initTinymce() } }, deactivated() { this.destroyTinymce() }, destroyed() { this.destroyTinymce() }, methods: { init() { // dynamic load tinymce from cdn load(tinymceCDN, (err) => { if (err) { this.$message.error(err.message) return } this.initTinymce() }) }, initTinymce() { const _this = this window.tinymce.init({ selector: `#${this.tinymceId}`, language: this.languageTypeList['zh'], height: this.height, body_class: 'panel-body ', object_resizing: false, toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar, menubar: this.menubar, plugins: plugins, end_container_on_empty_block: true, powerpaste_word_import: 'clean', code_dialog_height: 450, code_dialog_width: 1000, advlist_bullet_styles: 'square', advlist_number_styles: 'default', imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], default_link_target: '_blank', link_title: false, nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin init_instance_callback: editor => { if (_this.value) { editor.setContent(_this.value) } _this.hasInit = true editor.on('NodeChange Change KeyUp SetContent', () => { this.hasChange = true this.$emit('input', editor.getContent()) }) }, setup(editor) { editor.on('FullscreenStateChanged', (e) => { _this.fullscreen = e.state }) } // 整合七牛上傳 // images_dataimg_filter(img) { // setTimeout(() => { // const $image = $(img); // $image.removeAttr('width'); // $image.removeAttr('height'); // if ($image[0].height && $image[0].width) { // $image.attr('data-wscntype', 'image'); // $image.attr('data-wscnh', $image[0].height); // $image.attr('data-wscnw', $image[0].width); // $image.addClass('wscnph'); // } // }, 0); // return img // }, // images_upload_handler(blobInfo, success, failure, progress) { // progress(0); // const token = _this.$store.getters.token; // getToken(token).then(response => { // const url = response.data.qiniu_url; // const formData = new FormData(); // formData.append('token', response.data.qiniu_token); // formData.append('key', response.data.qiniu_key); // formData.append('file', blobInfo.blob(), url); // upload(formData).then(() => { // success(url); // progress(100); // }) // }).catch(err => { // failure('出現未知問題,刷新頁面,或者聯繫程序員') // console.log(err); // }); // }, }) }, destroyTinymce() { const tinymce = window.tinymce.get(this.tinymceId) if (this.fullscreen) { tinymce.execCommand('mceFullScreen') } if (tinymce) { tinymce.destroy() } }, setContent(value) { window.tinymce.get(this.tinymceId).setContent(value) }, getContent() { window.tinymce.get(this.tinymceId).getContent() }, imageSuccessCBK(arr) { const _this = this arr.forEach(v => { window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`) }) } } } </script> <style scoped> .tinymce-container { position: relative; line-height: normal; } .tinymce-container>>>.mce-fullscreen { z-index: 10000; } .tinymce-textarea { visibility: hidden; z-index: -1; } .editor-custom-btn-container { position: absolute; right: 4px; top: 4px; /*z-index: 2005;*/ } .fullscreen .editor-custom-btn-container { z-index: 10000; position: fixed; } .editor-upload-btn { display: inline-block; } </style>
最後,在須要引入的頁面內引入;api
<template> <div > <tinymce v-model="content" :height="300" /> </div> </template> <script> import Tinymce from '@/components/Tinymce' export default { components:{ Tinymce } </script>
效果:app