环境:VUE2.x + Tinymce + Tinymce-Vue + element
安装tinymce-vue
npm i @tinymce/tinymce-vue@3.2.8
注意:因为tinymce-vue4.x版本旨在支持Vue3
![](https://img.haomeiwen.com/i15535470/63639b571f66879c.png)
安装tinymce
npm i tinymce@5.7.1
复制icons、skins
安装完成后,在 public文件夹 下创建 tinymce文件夹,然后在node_modules下找到 tinymce,注意不是 @tinymce,复制 icons、skins 文件夹到 public/tinymce 下
下载中文语言包
下载地址:https://www.tiny.cloud/get-tiny/language-packages/,下载 zh_CN,下载后,把解压后的 langs文件夹 放到 public/tinymce 下
![](https://img.haomeiwen.com/i15535470/423824aa0fb09241.png)
tinymceEditor组件
在 components文件夹 下TinymceEditor.vue文件,复制以下代码
<template>
<div>
<Editor v-model="myValue" :id="tinymceId" :init="init" :disabled="disabled"></Editor>
<input type="file" hidden :id="'file-' + tinymceId"/>
</div>
</template>
<script>
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons' ////解决了icons.js 报错Unexpected token '<'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/print'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/code'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/code'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'
export default {
name: 'TinymceEditor',
components: {Editor},
props: {
id: {
type: String,
default: function () {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
height: {
type: [Number, String],
required: false,
default: 200
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
tinymceId: this.id,
myValue: this.value,
oldValue: '',
init: {
selector: `#${this.id}`,
content_style: "p {margin: 0; border:0; padding: 0;}",
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: this.height,
body_class: 'panel-body ',
object_resizing: true,//是否允许调整图像大小.
branding: false, //隐藏右下角由TINY驱动
contextmenu_never_use_native: true, //防止浏览器上下文菜单出现在编辑器中
elementpath: false, //隐藏底栏的元素路径(隐藏右下角元素显示)
toolbar: 'code | undo redo | lineheight| fontselect fontsizeselect formatselect | bold italic forecolor backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | imageUpload | help',
menubar: false,
plugins: ['advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste help wordcount'],
paste_retain_style_properties: 'all', //从Word中复制,保留所有样式
//paste_word_valid_elements: '*[*]', // 该设置在自word粘贴时,允许指定元素和属性保存在过滤结果中,要使用此功能,paste_enable_default_filters 要设置为true
paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
paste_convert_word_fake_lists: false, // 设为false可禁用复制word中的列表内容时,转换为html的UL或OL格式。
paste_webkit_styles: 'all',
paste_merge_formats: true, //启用PowerPaste插件的合并格式功能,例如:<b>abc <b>bold</b> 123</b>成为<b>abc bold 123</b>
end_container_on_empty_block: true, //如果在空的内部块元素中按下Enter键,则可以使用此选项拆分当前容器块元素。
powerpaste_word_import: 'clean', //保留标题,表格和列表等内容的结构,但删除内联样式和类。这样就产生了使用站点CSS样式表的简单内容,同时保留了原始文档的语义结构。
advlist_bullet_styles: 'default,circle,disc,square',
advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
default_link_target: '_blank', //默认链接是当前窗口打开,你也可以通过此参数将其变为_blank新窗口打开。
link_title: false,
nonbreaking_force_tab: true, // 按tab键插入三个 。一直按一直插。
images_upload_handler: this.images_upload_handler, //该images_upload_handler选项允许你指定被用来替代TinyMCE的默认的JavaScript和定制逻辑上传处理函数的函数。
setup: this.setup, //,您可以指定在渲染TinyMCE编辑器实例之前将执行的回调。
convert_urls: false
}
}
},
watch: {
value(newValue) {
this.myValue = newValue
},
myValue(newValue) {
this.$emit('input', newValue)
}
},
mounted() {
tinymce.init({})
},
methods: {
images_upload_handler(blobInfo, success, failure, progress) {
//this.oldValue = this.value;
let fd = new FormData();
fd.append('imageFile', blobInfo.blob(), blobInfo.filename());
this.$http.post('/editor/uploadImg', fd).then(res => {
if (res.data.code == 200) {
success(res.data.data);
} else {
//这样写是因为复制进来的图片进入这方法时就以base64存在(即在this.oldValue以base64存在),
//如果上传失败this.oldValue中已存在图片,所以setContent是不能回滚
this.$message.error("图片上传失败");
success("");
// failure('上传失败!');
// this.setContent(this.oldValue);
}
}).catch(res => {
this.$message.error("图片上传失败");
success("");
// failure('上传失败!');
// this.setContent(this.oldValue);
})
},
setup(editor) {
//注册一个图片按钮
editor.ui.registry.addButton('imageUpload', {
tooltip: '图片',
icon: 'image',
onAction: () => {
//点击按钮后执行
this.oldValue = this.value;
let self = this;
let input = document.getElementById("file-" + this.tinymceId);
input.click();
input.onchange = function () {
let file = input.files[0];
let fd = new FormData();
fd.append('imageFile', file);
self.$http.post('/editor/uploadImg', fd).then(res => {
if (res.data.code == 200) {
tinymce.get(self.tinymceId).insertContent("<img src='" + res.data.data + "'>");
input.value = '';
} else {
self.setContent(self.oldValue);
}
}).catch(res => {
self.setContent(self.oldValue);
})
}
}
});
},
}
}
</script>
<style lang="stylus" scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container {
::v-deep {
.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>
<style>
.oe-editor-wrap{
position: relative;
}
.oe-editor-del-btn{
position: absolute;
top:0;
right:0;
z-index:3010;
padding:0 2px 1px;
border-left:1px solid #DCDFE6;
}
.oe-editor-del-btn img{
height:36px;
}
.tox .tox-dialog-wrap__backdrop {
z-index: 3000;
background-color: rgba(0, 0, 0, .6);
}
.tox .tox-dialog {
z-index: 3001;
}
/*调整堆叠顺序,让下拉列表等显示*/
.tox-tinymce-aux {
z-index: 3002!important;
}
.tox-tinymce{
border: 1px solid #DCDFE6!important;
}
.tox .tox-statusbar{
border-top: 1px solid #DCDFE6!important;
}
.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){
border-right: 1px solid #DCDFE6!important;
}
.tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary{
background: none!important;
border-bottom:1px solid #DCDFE6;
}
.tox .tox-tbtn svg{
fill:#7b8397!important;
}
</style>
使用
import editor from "@/components/TinymceEditor";
<editor v-model="record.content"></editor>
![](https://img.haomeiwen.com/i15535470/aac32fc4cc569b7d.png)
不引用@tinymce/tinymce-vue的tinymce组件
<template>
<div>
<textarea :id="tinymceId"></textarea>
<input type="file" hidden :id="'file-' + tinymceId"/>
<div class="oe-editor-del-btn" v-if="showDel">
<img src="@/assets/editor_del.png" alt="">
</div>
</div>
</template>
<script>
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons' ////解决了icons.js 报错Unexpected token '<'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/print'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/code'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'
//import 'tinymce/plugins/imagetools'
import {editorUploadImg} from "@/api/public";
const INIT = 0;
const INPUT = 1;
const CHANGED = 2;
export default {
name: "Tinymce",
props: {
id: {
type: String,
default: function () {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
height: {
type: [Number, String],
required: false,
default: 200
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
readonly: {
type: Boolean,
default: false
},
showDel: {
type: Boolean,
default: () => {
return false
}
}
},
data() {
return {
tinymceId: this.id,
oldValue: '',
status: CHANGED,
init: {
selector: `#${this.id}`,
content_style: "p {margin: 0; border:0; padding: 0;}",
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
height: this.height,
body_class: 'panel-body ',
object_resizing: true, //是否允许调整图像大小.
branding: false, //隐藏右下角由TINY驱动
contextmenu_never_use_native: true, //防止浏览器上下文菜单出现在编辑器中
elementpath: false, //隐藏底栏的元素路径(隐藏右下角元素显示)
toolbar: 'code | undo redo | lineheight| fontselect fontsizeselect formatselect | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | imageUpload | help',
menubar: false,
plugins: ['advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste help wordcount'],
paste_retain_style_properties: 'all', //从Word中复制,保留所有样式
paste_word_valid_elements: '*[*]', // 该设置在自word粘贴时,允许指定元素和属性保存在过滤结果中,要使用此功能,paste_enable_default_filters 要设置为true
paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
paste_convert_word_fake_lists: false, // 设为false可禁用复制word中的列表内容时,转换为html的UL或OL格式。
paste_webkit_styles: 'all',
paste_merge_formats: true, //启用PowerPaste插件的合并格式功能,例如:<b>abc <b>bold</b> 123</b>成为<b>abc bold 123</b>
end_container_on_empty_block: true, //如果在空的内部块元素中按下Enter键,则可以使用此选项拆分当前容器块元素。
advlist_bullet_styles: 'default,circle,disc,square',
advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
default_link_target: '_blank', //默认链接是当前窗口打开,你也可以通过此参数将其变为_blank新窗口打开。
link_title: false,
nonbreaking_force_tab: true, // 按tab键插入三个 。一直按一直插。
images_upload_handler: this.images_upload_handler, //该images_upload_handler选项允许你指定被用来替代TinyMCE的默认的JavaScript和定制逻辑上传处理函数的函数。
setup: this.setup, //,您可以指定在渲染TinyMCE编辑器实例之前将执行的回调。
convert_urls: false,
readonly: this.readonly,
init_instance_callback : this.init_instance_callback,
// lineheight_val:
// '1 1.1 1.2 1.3 1.35 1.4 1.5 1.55 1.6 1.75 1.8 1.9 1.95 2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 3 3.1 3.2 3.3 3.4 4 5',
// fontsize_formats: '8pt 10pt 11pt 12pt 13pt 14pt 15pt 16pt 17pt 18pt 24pt 36pt',
// font_formats:
// "微软雅黑='微软雅黑';宋体='宋体';黑体='黑体';仿宋='仿宋';楷体='楷体';隶书='隶书';幼圆='幼圆';Andale Mono=andale mono,times;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;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings",
}
}
},
watch: {
value(newValue) {
if(this.status === CHANGED || this.status === INIT) return this.status = INPUT;
if (!newValue) {
tinymce.get(this.tinymceId).setContent('');
} else {
tinymce.get(this.tinymceId).setContent(newValue);
}
}
},
mounted() {
tinymce.init(this.init);
},
methods: {
images_upload_handler(blobInfo, success, failure, progress) {
// this.oldValue = this.value;
const fd = new FormData();
fd.append('imageFile', blobInfo.blob(), blobInfo.filename());
editorUploadImg(fd).then(res => {
if (res && res.code === 1) {
success(process.env.VUE_APP_BASE_API + res.data);
} else {
this.$message.error("图片上传失败");
success("");
// failure('上传失败!');
// this.setContent(this.oldValue);
}
}).catch(() => {
this.$message.error("图片上传失败");
success("");
// failure('上传失败!');
// this.setContent(this.oldValue);
});
},
setup(editor) {
editor.on('init', (e) => {
editor.setContent(this.value);
editor.on('input change undo redo execCommand', () => {
// 只在用户输入导致事件相应时才更新value数据,不然的话会导致光标跳到首位
if(this.status === INPUT || this.status === INIT) return this.status = CHANGED;
this.$emit('input', editor.getContent());
});
})
//注册一个图片按钮
editor.ui.registry.addButton('imageUpload', {
tooltip: '图片',
icon: 'image',
onAction: () => {
//点击按钮后执行
this.oldValue = this.value;
const self = this;
const input = document.getElementById("file-" + this.tinymceId);
input.click();
input.onchange = function () {
const file = input.files[0];
const fd = new FormData();
fd.append('imageFile', file);
editorUploadImg(fd).then(res => {
if (res && res.code === 1) {
tinymce.get(self.tinymceId).insertContent(`<img src='${process.env.VUE_APP_BASE_API + res.data}' alt=""/>`);
input.value = '';
} else {
self.setContent(self.oldValue);
}
}).catch(() => {self.setContent(self.oldValue);});
}
}
});
},
init_instance_callback(editor) {
// console.log("ID为: " + editor.id + " 的编辑器已初始化完成.");
// editor.on('change', (e) => {
// this.$emit('change', e.level.content)
// })
// editor.on('input', (e) => {
// this.$emit('input', e.target.innerHTML)
// });
}
}
}
</script>
<style lang="scss" scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container {
::v-deep {
.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>
<style lang="scss">
.oe-editor-wrap{
position: relative;
}
.oe-editor-del-btn{
position: absolute;
top:0;
right:0;
z-index:3010;
padding:0 2px 1px;
border-left:1px solid #DCDFE6;
}
.oe-editor-del-btn img{
height:36px;
}
.tox .tox-dialog-wrap__backdrop {
z-index: 3000;
background-color: rgba(0, 0, 0, .6);
}
.tox .tox-dialog {
z-index: 3001;
}
/*调整堆叠顺序,让下拉列表等显示*/
.tox-tinymce-aux {
z-index: 3002!important;
}
.tox-tinymce{
border: 1px solid #DCDFE6!important;
}
.tox .tox-statusbar{
border-top: 1px solid #DCDFE6!important;
}
.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){
border-right: 1px solid #DCDFE6!important;
}
.tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary{
background: none!important;
border-bottom:1px solid #DCDFE6;
}
.tox .tox-tbtn svg{
fill:#7b8397!important;
}
</style>
网友评论