Vue 使用 TinyMCE 富文本编辑器

478 阅读2分钟

前言

说到富文本编辑器,Quill 是 Vue 开发者绕不开的,但遗憾的是,QuillVue 扩展 vue-quill-editor 在 5 年前停止更新了。

斯人已逝,我们迎来了新的富文本编辑器,TinyMCE

TinyMCE 官方文档

TinyMCE 中文文档

环境

vue: 2.6.10

tinymce: 5.4.2

@tinymce/tinymce-vue: 3.2.3

copy-webpack-plugin: 6.1.1

实现

安装 tinymce@tinymce/tinymce-vue

yarn add tinymce

yarn add @tinymce/tinymce-vue

创建通用富文本组件

这种复用度高的组件,自然是要抽象为公告组件的。

和很多组件包一样,TinyMCE 也是通过一个基础组件和配置参数对象来实现功能的。

// 引入tinyMCE对象
import tinymce from "tinymce/tinymce";
// 引入编辑器组件
import Editor from "@tinymce/tinymce-vue";
// 引入主题文件
import "tinymce/themes/silver/theme";
// 引入皮肤样式
import "tinymce/skins/ui/oxide/skin.css";

在默认配置下的 TinyMCE 显得过于简陋了,所以我们要自己引入需要的功能插件包。

需要注意的是,引入和使用在 TinyMCE 中是不同的操作。引入后,不更改工具栏配置,也是使用不了对应功能插件的。

// 编辑器插件plugins
// 更多插件参考:https://www.tiny.cloud/docs/plugins/
import "tinymce/plugins/image"; // 插入上传图片插件
import "tinymce/plugins/table"; // 插入表格插件
import "tinymce/plugins/lists"; // 列表插件
import "tinymce/plugins/wordcount"; // 字数统计插件
import 'tinymce/plugins/media'; // 插入上传视频插件
import 'tinymce/plugins/autosave'; // 自动保存插件
import 'tinymce/plugins/preview'; // 预览插件
import 'tinymce/plugins/hr'; // 预览插件
import 'tinymce/plugins/code'; // 源码插件

const config = {
  .....
  // 需要配置 toolbar,使功能按钮显示出来,除了一些特殊插件,如字数统计插件等。
  // 当 toolbar 为 String 类型时,所有组件都会放置在同一行,直到溢出换行。
  // 当 toolbar 为 Array 类型时,每个下标为一行。
  toolbar: [
  "undo redo | cut copy paste pastetext | forecolor backcolor bold italic underline hr link anchor | alignleft aligncenter alignright alignjustify outdent indent",
  "styleselect formatselect fontsizeselect",
  "bullist numlist | blockquote subscript superscript removeformat | table image charmap emoticons pagebreak insertdatetime print preview | bdmap indent2em lineheight formatpainter axupimgs",
  ]
}

然后在 template 使用 Editor 组件,组件内的富文本值可以直接通过 v-model 获取,init 属性传初始配置对象,onClick 接收点击事件。

mounted 生命周期方法内调用 tinymce 对象的 init 方法,完成初始化。

<template>
  <div class="tinymce-editor">
    <editor
      v-model="myValue"
      :init="init"
      :disabled="disabled"
      @onClick="onClick"
    ></editor>
  </div>
</template>

export default {
  mounted() {
    tinymce.init({});
  },
}

这样就可以了,在想要使用的地方直接引入。看起来简单,真的是踩了好多坑。

如果想了解最新的功能,建议直接看 TinyMCE 官方文档

如果想了解 API ,建议看 TinyMCE 中文文档,但是只能用来参考,因为 TinyMCE 中文文档 已经很久没更新了,而且他的用法主要围绕 <script /> 标签直接引入写的,并不怎么适合 Vue

问题

编辑器样式消失

因为我的编辑器获取样式是相对路径,所以需要使用 copy-webpack-plugin 插件包,将静态文件不打包直接放到指定目录,建议是配置一个绝对路径,我是因为木已成舟不想修改了。

module.exports = {
  ......
  plugins: [
    ......
    new CopyPlugin({//复制不需要参加打包的文件到指定目录
      patterns: [
        { from: resolve(__dirname, './src/static'), to: 'static' }
      ]
    }),
  ]
}

代码

<template>
  <div class="tinymce-editor">
    <editor
      v-model="myValue"
      :init="init"
      :disabled="disabled"
      @onClick="onClick"
    ></editor>
  </div>
</template>
<script>
import tinymce from "tinymce/tinymce";
import Editor from "@tinymce/tinymce-vue";
import "tinymce/themes/silver/theme";
import "tinymce/skins/ui/oxide/skin.css";
// 编辑器插件plugins
// 更多插件参考:https://www.tiny.cloud/docs/plugins/
import "tinymce/plugins/image"; // 插入上传图片插件
import "tinymce/plugins/table"; // 插入表格插件
import "tinymce/plugins/lists"; // 列表插件
import "tinymce/plugins/wordcount"; // 字数统计插件
import "tinymce/plugins/media"; // 插入上传视频插件
import "tinymce/plugins/autosave"; // 自动保存插件
import "tinymce/plugins/preview"; // 预览插件
import "tinymce/plugins/hr"; // 预览插件
import "tinymce/plugins/code"; // 源码插件

export default {
  components: {
    Editor,
  },
  props: {
    disabled: {
      type: Boolean,
      default: false,
    },
    plugins: {
      type: [String, Array],
      default: "lists image table wordcount autosave preview hr",
    },
    toolbar: {
      type: [String, Array],
      default: () => {
        return [
          "undo redo | cut copy paste pastetext | forecolor backcolor bold italic underline hr link anchor | alignleft aligncenter alignright alignjustify outdent indent",
          "styleselect formatselect fontsizeselect",
          "bullist numlist | blockquote subscript superscript removeformat | table image charmap emoticons pagebreak insertdatetime print preview | bdmap indent2em lineheight formatpainter axupimgs",
        ];
      },
    },
    value: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      init: {
        selector: "textarea",
        language_url: "./static/tinymce/langs/zh_CN.js",
        language: "zh_CN",
        convert_urls: false,
        skin_url: "./static/tinymce/skins/ui/oxide",
        content_css: "./static/tinymce/skins/content/default/content.css",
        icons: "default",
        icons_url: "./static/tinymce/icons/icons.js",
        content_style: "img {max-width: 100%} ",
        height: "80vh",
        plugins: this.plugins,
        toolbar: this.toolbar,
        branding: false,
        // menubar: 'file edit',
        // 如需ajax上传可参考https://www.tiny.cloud/docs/configure/file-image-upload/#images_upload_handler
        images_upload_handler: (blobInfo, success, failure) => {
          var xhr, formData;
          xhr = new XMLHttpRequest();
          xhr.withCredentials = false;
          xhr.open("POST", "./file/upload");
          xhr.onload = function () {
            var json;
            if (xhr.status != 200) {
              failure("HTTP Error: " + xhr.status);
              return;
            }
            json = JSON.parse(xhr.responseText);
            // this.imgsUrl[this.imgsUrl.length - 1] = json["data"];
            success("./files/" + json["path"]);
          };
          formData = new FormData();
          formData.append("file", blobInfo.blob(), blobInfo.filename());
          formData.append("dir", "jfzsy");
          xhr.send(formData);
        },
      },
      myValue: this.value,
    };
  },
  mounted() {
    tinymce.init({});
  },
  methods: {
    // 添加相关的事件,可用的事件参照文档=> https://github.com/tinymce/tinymce-vue => All available events
    // 需要什么事件可以自己增加
    onClick(e) {
      this.$emit("onClick", e, tinymce);
    },
    // 可以添加一些自己的自定义事件,如清空内容
    clear() {
      this.myValue = "";
    },
  },
  watch: {
    value(newValue) {
      this.myValue = newValue;
    },
    myValue(newValue) {
      this.$emit("input", newValue);
    },
  },
};
</script>