Skip to content

Vue 使用 TinyMCE 富文本编辑器

前言

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

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

TinyMCE 官方文档

TinyMCE 中文文档

环境

Vue: v2.6.10

tinymce: v5.4.2

@tinymce/tinymce-vue: v3.2.3

copy-webpack-plugin: v6.1.1

实现

安装 tinymce@tinymce/tinymce-vue

shell
yarn add tinymce

yarn add @tinymce/tinymce-vue

创建通用富文本组件

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

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

js
// 引入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 中是不同的操作。引入后,不更改工具栏配置,也是使用不了对应功能插件的。

js
// 编辑器插件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 方法,完成初始化。

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

<script>
  export default {
    mounted() {
      tinymce.init({});
    },
  };
</script>

这样就可以了,在想要使用的地方直接引入。

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

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

问题

编辑器样式消失

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

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

代码

html
<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>

Released under the MIT License.