TinyMce的使用

tinymce中文官网

tinymce英文官网

安装

注:可以不用下载安装tinymce文件包,采用官网注册申请的api-key值通过cdn的方式获取在线样式内容

下载完成后,在public文件夹里面创建static文件夹,再在里面创建tinymce文件夹,把node_module中的tinymce文件下的skins文件夹,以及解压后的汉化包拷贝到创建的tinymce文件夹中(无法直接从node_module中访问)

使用

typescript 复制代码
<script lang="ts" setup>
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue';
import { ElMessage, ElLoading } from 'element-plus';
import Editor from '@tinymce/tinymce-vue';
//引入tinymce开启本地模式
import tinymce from 'tinymce/tinymce'; //tinymce核心文件

//引入图标和主题等
import 'tinymce/themes/silver/theme';
import 'tinymce/icons/default/icons';
import 'tinymce/models/dom';
//引入插件
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/link';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/code';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/table';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/wordcount';
import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/image';

import { cos as COS, uploadData as UploadData } from '@/utils/upload'; //图片上传接口
onMounted(() => {
  cos.value = COS;
  uploadData.value = UploadData;
  tinymce.init({});
});
onUnmounted(() => {
  tinymce.remove();
});
const cos = ref();
const uploadData = ref();
const prop = defineProps({
  modelValue: {
    type: String,
    default: ''
  },
  height: {
    type: Number,
    default: 400
  }
});
const emit = defineEmits<{
  (e: 'update:modelValue', value: any): void;
}>();
const contentValue = computed({
  get() {
    return prop.modelValue;
  },
  set(value) {
    emit('update:modelValue', value);
  }
});
const editorHeight = computed(() => prop.height);
const init = reactive({
  selector: '#editor',
  language_url: 'static/tinymce/langs/zh-Hans.js', // 语言包
  language: 'zh-Hans', // 语言类型
  skin_url: 'static/tinymce/skins/ui/oxide', // 皮肤类型
  toolbar: [
    'bold italic hr | fontsize blocks | forecolor backcolor align |  blockquote removeformat | subscript superscript | bullist table insertdatetime | link charmap  wordcount searchreplace code | codesample visualblocks image  fullscreen preview | undo redo'
  ],
  height: editorHeight.value,
  menubar: false, // 清空上方不需要的菜单栏
  statusbar: false,
  plugins:
    'codesample lists advlist link autolink charmap fullscreen preview code searchreplace table visualblocks wordcount insertdatetime image',
  placeholder: '请输入内容吧...',
  branding: false, //tiny技术支持信息是否显示
  resize: true, //编辑器宽高是否可变,false-否,true-高可变,'both'-宽高均可,注意引号
  elementpath: false, //元素路径是否显示
  content_css: 'static/tinymce/skins/content/default/content.css', // 自动导入样式会报404错误需要手动操作,
  // 编辑区内容样式
  content_style:
    'body{font-size:12pt;font-family:Microsoft YaHei,微软雅黑,宋体,Arial,Helvetica,sans-serif;line-height:1.5}img {max-width:100%;}',
  paste_data_images: true, //图片是否可粘贴
 // 图片自定义上传方式
  images_upload_handler: (blobInfo: any) =>
    new Promise((resolve, reject) => {
      cos.value
        .uploadImg(blobInfo.blob())
        .then((data: any) => {
          resolve('https://' + data.Location);
        })
        .catch((err: any) => {
          reject(err);
        });
    }),
  //可上传文件类型
  file_picker_types: 'file image media',
  // 文件上传
  file_picker_callback: (callback: any) => {
    const fileBtn = document.createElement('input');
    fileBtn.type = 'file';
    fileBtn.style.position = 'fixed';
    fileBtn.style.left = '0';
    fileBtn.style.top = '0';
    fileBtn.style.opacity = '0';
    document.body.appendChild(fileBtn);
    fileBtn.click();
    fileBtn.addEventListener('change', () => {
      const loading = ElLoading.service({
        lock: true,
        text: 'Loading',
        background: 'rgba(0, 0, 0, 0.7)'
      });
      const fileList: any = fileBtn.files;
      let file: any = undefined;
      if (fileList.length) {
        file = fileList[0];
      } else {
        ElMessage.error('请选择需要上传的文件');
        return;
      }

      new Promise(() => {
        cos.value
          .uploadImg(file)
          .then((data: any) => {
            loading.close();
            callback('https://' + data.Location, { text: file.name });
          })
          .catch(() => {
            loading.close();
            ElMessage.error('该文件无法上传');
          });
      });
    });
  }
});
</script>

<template>
  <div id="sample">
    <Editor
      id="editor"
      api-key="rmwp2ztcxsgkocdb77x4gaub5tmz7k1jpsx39xvvg2lnjzu1"
      v-model="contentValue"
      :init="init"
    />
  </div>
</template>

<style lang="scss" scoped>
#sample {
  width: 100%;
}
</style>

代码编写中遇到的问题

问题:中文官网和tinymce6之前版本提供的自定义上传图片的方式适用于vue2版本,并不能在vue3中使用,所以无法覆盖编辑器本身的上传图片的功能,还是优先选择编辑器自带的上传图片方式把图片转换为base64编码

typescript 复制代码
// 官网vue2
images_upload_handler: function (blobInfo, succFun, failFun) {
        var xhr, formData;
        var file = blobInfo.blob();//转化为易于理解的file对象
        xhr = new XMLHttpRequest();
        xhr.withCredentials = false;
        xhr.open('POST', '/demo/upimg.php');
        xhr.onload = function() {
            var json;
            if (xhr.status != 200) {
                failFun('HTTP Error: ' + xhr.status);
                return;
            }
            json = JSON.parse(xhr.responseText);
            if (!json || typeof json.location != 'string') {
                failFun('Invalid JSON: ' + xhr.responseText);
                return;
            }
            succFun(json.location);
        };
        formData = new FormData();
        formData.append('file', file, file.name );//此处与源文档不一样
        xhr.send(formData);
    }
// vue3
  images_upload_handler: (blobInfo: any) =>
    new Promise((resolve, reject) => {
      cos.value
        .uploadImg(blobInfo.blob())
        .then((data: any) => {
          resolve('https://' + data.Location);
        })
        .catch((err: any) => {
          reject(err);
        });
    })

注意:由于官网并没有提供上传文件的插件,但提供了上传文件的回调函数和自定义插件的实现方式

通过上传文件回调的方式实现文件的上传

tinymce编辑器初始化时,添加选择文件的回调函数file_picker_callback添加回调函数,link插件下的模态框就会多出一个上传svg图标。

在不写函数逻辑的情况下,此时点击上传图标是没有任何反应的,因为未触发file_picker_callback回调函数中的逻辑

为了能在点击上传图标时选择文件,我们可以先创建一个typefileinput框,设置它的accept属性为我们需要上传的文件类型

其次,触发它的点击事件,以供我们选择文件

然后,我们需要为input框添加change事件。在change事件中拿到我们需要上传的file文件,并将它上传到阿里云服务器上,就能获取到该file文件在阿里云服务器上的地址url

紧接着,我们可以根据file.type, 判断用户上传的文件类型,封装不同的方法。传入fileurl作为不同函数方法的参数, 对不同情况进行分类讨论

最后,我们使用file_picker_callback提供的callback方法,调用回调函数即可。callback方法有两个作用:a. 文件上传完成后,在插入/编辑链接模态框回显上传的文件信息 。其中,url插入/编辑链接模态框回显的地址信息,text插入/

编辑链接模态框回显的显示文字信息。b. 点击保存按钮后,会在TinyMCE编辑器中展示一个指向上传文件的链接。

自定义插件的方式可参考tinymce爱好者魔改插件(适用于vue2,tinymce版本4以下)

问题: 由于webpack和vite的打包方式不同导致tinymce无法正常访问样式资源代码

  • webpack会将所有的文件都进行打包,包括public里面的文件

  • vite在不进行任何配置的情况下,会将除开public的所有引用到资源打包编译添加哈希值至assets文件夹中(非引用文件以及行内样式图片未被打包编译资源会被treeSharp直接忽略不打包),vite只会对public文件夹进行不打包处理,public文件夹内所有文件会移至dist中,与vite.config.ts打包后的静态目录"assetsDir: 'static'"同级,如下图

  • 当由于rewrite.conf配置文件规定了资源的访问方式,这就导致了正常情况下,tinymce编辑器的资源样式地址访问错误

解决方法:

  1. 下载rollup-plugin-copy,使用该插件将public里面的文件指定你想要的打包目录中去,但这样打包会产生一个问题就是该插件的操作只是一个拷贝作用,源文件依旧存在,这样就导致了打包体积增加资源浪费
  2. 将public文件创建vite打包资源输出目录("assetsDir: 'static'")同名的文件夹static,将tinymce文件夹放入staic文件夹中,在vite打包的过程中由于同名文件static会自动合并
  3. 通过官网注册获取密钥实现cdn的方式获取

还有注意的一点是网上所有的教程资源地址都是以斜杠开头'/'的路径地址(如:'/static/tinymce/langs/zh-Hans.js'),这样就导致资源的访问指向了网站的根目录(tiku.zuoyebang.cc/static/tiny...),而不会带上我们需要的网站制定的域名'/fe',所以在实际开发中tinymce的资源访问路径不能以斜杠开头'/'(如'static/tinymce/skins/ui/oxide'),至于为什么本地运行的环境加上'/'也能正常访问是因为在本地运行的时候,根目录就是我所编辑的项目目录,资源能够正常访问

问题: 弹窗中使用tinymce编辑器,tinymce的所有自带弹窗无法展示

全局样式文件里设置

css 复制代码
.tox-tinymce-aux {
    z-index: 9999 !important;
}
相关推荐
cwj&xyp9 分钟前
Python(二)str、list、tuple、dict、set
前端·python·算法
dlnu201525062211 分钟前
ssr实现方案
前端·javascript·ssr
古木201916 分钟前
前端面试宝典
前端·面试·职场和发展
轻口味2 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀3 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef5 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端