vue3 + tinymce + ts 集成

513 阅读5分钟

安装:

npm i tinymce -S
npm i @tinymce/tinymce-vue -S

下载中文包:

https://www.tiny.cloud/get-tiny/language-packages/

引入语言包和皮肤:

语言包就是你下载下来的,皮肤资源在node_modules/tinymce里面直接复制到public,详情看下图:

1704942013203.png

在项目根目录创建声明文件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>;

最后展示的效果:

image.png