安装:
npm i tinymce -S
npm i @tinymce/tinymce-vue -S
下载中文包:
https://www.tiny.cloud/get-tiny/language-packages/
引入语言包和皮肤:
语言包就是你下载下来的,皮肤资源在node_modules/tinymce里面直接复制到public,详情看下图:
在项目根目录创建声明文件tinymce.d.ts
declare module 'tinymce/models/dom';
declare module 'tinymce/themes/silver';
declare module 'tinymce/icons/default';
declare module 'tinymce/plugins/advlist';
declare module 'tinymce/plugins/anchor';
declare module 'tinymce/plugins/autolink';
declare module 'tinymce/plugins/autoresize';
declare module 'tinymce/plugins/autosave';
declare module 'tinymce/plugins/charmap';
declare module 'tinymce/plugins/code';
declare module 'tinymce/plugins/codesample';
declare module 'tinymce/plugins/directionality';
declare module 'tinymce/plugins/emoticons';
declare module 'tinymce/plugins/fullscreen';
declare module 'tinymce/plugins/help';
declare module 'tinymce/plugins/image';
declare module 'tinymce/plugins/importcss';
declare module 'tinymce/plugins/link';
declare module 'tinymce/plugins/lists';
declare module 'tinymce/plugins/nonbreaking';
declare module 'tinymce/plugins/pagebreak';
declare module 'tinymce/plugins/preview';
declare module 'tinymce/plugins/quickbars';
declare module 'tinymce/plugins/save';
declare module 'tinymce/plugins/searchreplace';
declare module 'tinymce/plugins/table';
declare module 'tinymce/plugins/visualblocks';
declare module 'tinymce/plugins/visualchars';
declare module 'tinymce/plugins/wordcount';
封装组件:
<template>
<div class="tinymceBox">
<Editor
v-model="editContent"
:init="editorConfig">
</Editor >
</div>
</template>
<script lang="ts" setup>
import { onMounted, computed, onUnmounted } from 'vue'
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/models/dom'; // 引入dom模块。从Tinymce6,开始必须有此模块导入
import 'tinymce/themes/silver'; //默认主题
import 'tinymce/icons/default'; //引入编辑器图标icon,不引入则不显示对应图标
import 'tinymce/plugins/advlist'; //高级列表
import 'tinymce/plugins/autolink'; //自动链接
import 'tinymce/plugins/autoresize'; //编辑器高度自适应,注:plugins里引入此插件时,Init里设置的height将失效
import 'tinymce/plugins/autosave'; //自动存稿
import 'tinymce/plugins/charmap'; //特殊字符
import 'tinymce/plugins/code'; //编辑源码
import 'tinymce/plugins/codesample'; //代码示例
import 'tinymce/plugins/directionality'; //文字方向
import 'tinymce/plugins/fullscreen'; //全屏
import 'tinymce/plugins/help'; //帮助
import 'tinymce/plugins/image'; //插入编辑图片
import 'tinymce/plugins/importcss'; //引入css
import 'tinymce/plugins/emoticons';
import 'tinymce/plugins/link'; //超链接
import 'tinymce/plugins/lists'; //列表插件
import 'tinymce/plugins/nonbreaking'; //插入不间断空格
import 'tinymce/plugins/pagebreak'; //插入分页符
import 'tinymce/plugins/preview'; //预览
import 'tinymce/plugins/save'; //保存
import 'tinymce/plugins/searchreplace'; //查找替换
import 'tinymce/plugins/table'; //表格
import 'tinymce/plugins/visualblocks'; //显示元素范围
import 'tinymce/plugins/visualchars'; //显示不可见字符
import 'tinymce/plugins/wordcount'; //字数统计
//组件属性
const props = defineProps({
modelValue: {
type: String,
required: true,
default: ''
},
placeholder: {
type: String,
required: false,
default: '请在这里输入内容'
},
selectorId: {
type: String,
required: false,
default: ''
},
height: {
type: Number,
required: false,
default: 360
},
})
//双向绑定htmlContent
const emit = defineEmits(['update:modelValue','editor']);
const editContent = computed({
get(): string {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
//上传图片处理 没有接口 进行模拟处理
const handerImageUpload = function (blobInfo, progress) {
console.log("blobInfo",blobInfo)
console.log("progress",progress)
return new Promise((resolve,reject)=>{
setTimeout(()=>{
progress(100)
resolve('https://xxxxx.jpeg')
},500)
console.log("reject",reject)
})
}
//配置编辑器的菜单工具
const editorConfig = {
selector: '#'+props.selectorId,
emoticons_database_url: 'tinymce/emoticons/emojis.js',
language_url: 'tinymce/langs/zh-Hans.js',
language: 'zh-Hans', //汉化
skin_url: 'tinymce/skins/ui/oxide', //皮肤
content_css: 'tinymce/skins/content/default/content.css',
content_style: 'body{font-size:14px;font-family:Microsoft YaHei,微软雅黑,宋体,Arial,Helvetica,sans-serif;line-height:1}img {max-width:100%;}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before { color: #BFBFBF;}',
height: props.height, //高度
menubar: false,
toolbar:[
'blocks fontfamily fontsize forecolor backcolor bold italic underline strikethrough link emoticons undo redo ',
'alignleft aligncenter alignright alignjustify outdent indent lineheight | styleselect formatselect fontselect fontsizeselect | bullist numlist | blockquote subscript superscript removeformat charmap | table image | indent2em formatpainter axupimgs | fullscreen code codesample'],
plugins:
'code codesample preview searchreplace autolink directionality visualblocks visualchars fullscreen image link table charmap pagebreak nonbreaking advlist lists wordcount autosave emoticons',
line_height_formats: '0.6 0.8 1 1.2 1.4 1.6 2', //行高
font_size_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px', //字体大小
font_family_formats:'微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;楷体=楷体,Helvetica,Arial,sans-serif;华文新魏=华文新魏,Helvetica,Arial,sans-serif;隶书=隶书,Helvetica,Arial,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
images_file_types: 'jpeg,jpg,png,gif',
images_upload_handler: handerImageUpload,
convert_urls: false,
relative_urls: false,
paste_data_images: true,
placeholder: props.placeholder,
branding: false, //tiny技术支持信息是否显示
statusbar: true, //最下方的元素路径和字数统计那一栏是否显示
elementpath: false, //元素路径是否显示
custom_undo_redo_levels: 20, //撤销和重做的次数
draggable_modal: true, //对话框允许拖拽
element_format: 'xhtml', //输出 xhtml
br_in_pre: false, //pre内不添加 br 标签
promotion: false, //去除upgrade提示
init_instance_callback:function (editor){
//将editor交给父组件,做一些其他事情
emit('editor', editor);
//初始化完成后,手动点击右下角将字数统计转换为字符模式
const elements = editor.getContainer().getElementsByClassName('tox-statusbar__wordcount')
const wordcount = elements[0]
wordcount.click()
}
}
//生命周期函数
onMounted(async () => {
tinymce.init({}); //初始化tinymce
});
onUnmounted(() => {
tinymce.remove(); //销毁tinymce
});
</script>
<style scoped lang="less">
.tinymceBox {
width: 100%;
}
</style>
使用组件:
<script lang="ts" setup>
import EditorTinymce from '@/components/EditorTinymce.vue'
//接收tinymce编辑器的editor,用来获取右下角字符统计
let _editor = null
const receiveEditor = (editor)=>{
_editor = editor
}
/* 贴上的是代码片段,自己放在需要的位置去
//获取右下角字符数
const elements = _editor.getContainer().getElementsByClassName('tox-statusbar__wordcount')
const wordcountButton = elements[0]
const wordcount = parseInt(wordcountButton.innerHTML)
*/
</script>
//模板中使用组件:
<EditorTinymce v-model="htmlContent" @editor="receiveEditor" placeholder="placeholder"/>
遇到的问题:
右下角字数统计有两种模式,默认英文统计的是单词,不是单个字母。点击后显示的字符个数,这个符合一般的需求。没有好的办法设置,所以在初始化回调里进行了手动切换
init_instance_callback:function (editor){
//将editor交给父组件,做一些其他事情
emit('editor', editor);
//初始化完成后,手动点击右下角将字数统计转换为字符模式
const elements = editor.getContainer().getElementsByClassName('tox-statusbar__wordcount')
const wordcount = elements[0]
wordcount.click()
}
获取右下角的字数统计
//获取右下角字符数
const elements = _editor.getContainer().getElementsByClassName('tox-statusbar__wordcount')
const wordcountButton = elements[0]
const wordcount = parseInt(wordcountButton.innerHTML)
更改placeholder的颜色
//content_style里面添加.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before { color: #BFBFBF;} 将color: #BFBFBF更改为你喜欢的颜色
content_style: 'body{font-size:14px;font-family:Microsoft YaHei,微软雅黑,宋体,Arial,Helvetica,sans-serif;line-height:1}img {max-width:100%;}.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before { color: #BFBFBF;}'
遇到编译错误Rollup failed to resolve import "tinymce/tinymce" from "xxx".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
build.rollupOptions.external
此时需要将tinymce放入build.rollupOptions.external中,打开vite.config.ts文件,在external里面添加,就像下面的代码一样
build: {
target: 'esnext',
rollupOptions: {
external: ['tinymce', /^@tinymce\/tinymce-vue\/.*/],
input: 'src/main.ts',
output: {
entryFileNames: `[name].js`,
chunkFileNames: `[name].js`,
assetFileNames: `[name].[ext]`,
format: 'system'
}
}
}
注意上传图片的方法类型与旧版本不同了
//images_upload_handler类型
type UploadHandler = (blobInfo: BlobInfo, progress: ProgressFn) => Promise<string>;