1. 安装
  2. 语言包
  3. 载入插件
  4. 图片上传
  5. 封装为 Vue Component
  6. 插件开发
  7. 参考

在 Vue 中使用 TinyMCE

TinyMCE 是一个开源的(LGPL 授权协议)富文本编辑器.

本文的软件环境如下:

1 "@tinymce/tinymce-vue": "2.1.0"
2 "tinymce": "^5.0.7"
3 "vue": "^2.6.10"

安装

TinyMCE 官方提供了 tinymce-vue 供 Vue 用户使用.
GitHub - tinymce/tinymce-vue: Official TinyMCE Vue component

但是 tinymce-vue 默认使用的是官方的 CDN,需要到官网注册帐号,否则会有个提示.
feat: add prefer cdn url config by yugasun · Pull Request #6 · tinymce/tinymce-vue · GitHub
捕获.PNG

如果不想用官方的 CDN,可以手动载入TinyMCE.

安装依赖:

1
2
yarn add @tinymce/tinymce-vue@2.1.0
yarn add tinymce@5.0.7

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<editor :init="init" v-model="data"></editor>
</template>
<script>
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver/theme.min.js';
import 'tinymce/skins/ui/oxide/skin.min.css';
import Editor from '@tinymce/tinymce-vue';

export default {
data() {
return {
data: '',
init: {}
};
},
components: {
Editor
}
};
</script>

也可以不用 tinymce-vue, 自己封装 tinymce.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div id="tinymce-vue"></div>
</template>

<script>
import Tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/skins/ui/oxide/skin.min.css';

export default {
mounted() {
Tinymce.init({
selector: '#tinymce-vue'
});
}
};
</script>

或者用 <script> 标签全局引入. 当然,官方的 CDN 也要注册.
TinyMCE | Quick start

不想用官方的 CDN 的话可以自己编译代码.过程如下:
GitHub - tinymce/tinymce: The world's #1 JavaScript library for rich text editing. Available for React, Vue and Angular

1
2
3
4
5
6
7
8
// 先下载代码
git clone https://github.com/tinymce/tinymce.git
// 进入所在文件夹
cd tinymce
// 下载依赖
yarn
// 编译
yarn build

编译好的文件在 dist 文件夹里,找到 tinymce_5.x.x.zip 解压即可.
然后把解压得到的 tinymce 文件夹放在 public/js 中, 打开 public/index.html,添加如下代码:

1
<script src="./js/tinymce/tinymce.min.js"></script>

在 Vue component 中,可以通过 window.tinymce 来使用 tinymce.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="tinymce-vue"></div>
</template>
<script>
export default {
mounted() {
window.tinymce.init({
selector: '#tinymce-vue',
plugins: 'image',
toolbar: 'image'
});
}
};
</script>

语言包

中文之类的语言包需要到下面这个地址下载,这里以 zh_CN.js 为例:
Language Packages | TinyMCE

如果是用 <script> 全局引入的 tinymce,直接把 zh_CN.js 丢到 langs 文件夹里即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="tinymce-vue"></div>
</template>
<script>
export default {
mounted() {
window.tinymce.init({
selector: '#tinymce-vue',
language: 'zh_CN',
});
}
};
</script>

如果是用 import 方法引入的 tinymce, 则需要手动引入 zh_CN.js.

1
import './tinymce/langs/zh_CN.js';  

引入之后可能会报错:

error: Unnecessary escape character: / (no-useless-escape) at

不过这个报错没啥影响, 打开 zh_CN.js,直接用 eslint-disable 注释掉即可.

1
2
3
/* eslint-disable no-useless-escape,no-undef */

tinymce.addI18n('zh_CN',{ ...

import 方法载入的 tinymce 需要单独引入插件/主题.
而用 <script> 标签全局载入的则不需要,以下不再赘述.

当前阶段最好还是用 <script> 标签全局载入比较好, 省事还没 bug.

载入插件

这里以 image 插件为例, 还是分两种方法讨论
1 <scritp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div id="tinymce-vue"></div>
</template>
<script>
export default {
mounted() {
tinymce.init({
selector: '#tinymce-vue',
plugins: 'image imagetools',
toolbar: 'image '
});
}
};
</script>

2 import

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div id="tinymce-vue"></div>
</template>

<script>
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/skins/ui/oxide/skin.min.css';
// 载入插件
import 'tinymce/plugins/image';
import 'tinymce/plugins/imagetools';
export default {
mounted() {
tinymce.init({
selector: '#tinymce-vue',
plugins: 'image imagetools', // 插件
toolbar: 'image' // 工具栏
});
}
};
</script>

注意, 跨域图片无法使用 imagetools javascript - TinyMCE Image Tools Not Working - Stack Overflow

图片上传

image 插件有两个常用的参数
1 images_upload_url 上传地址, 无此参数时不显示上传选项.
2 images_upload_handler 覆盖默认的上传逻辑. 接受 function 作为参数, 有3个参数blobInfo, success, failure.

TinyMCE | Image & file upload options

这里以 sm 图床为例 上传 API 文档 - SM.MS - Simple Free Image Hosting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div id="tinymce-vue"></div>
</template>

<script>
const __IMG_UPLOAD_URL__ = 'https://sm.ms/api/upload';

export default {
mounted() {
window.tinymce.init({
selector: '#tinymce-vue',
plugins: 'image',
toolbar: 'image ',
// without images_upload_url set, Upload tab won't show up
images_upload_url: __IMG_UPLOAD_URL__,
// override default upload handler to simulate successful upload
images_upload_handler: function(blobInfo, success, failure) {
var xhr, formData;
xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.open('POST', __IMG_UPLOAD_URL__);

xhr.onload = function() {
var json;
if (xhr.status != 200) {
failure('HTTP Error: ' + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
if (!json || typeof json.data.url != 'string') {
failure('Invalid JSON: ' + xhr.responseText);
return;
}
success(json.data.url);
};
formData = new FormData();
formData.set('smfile', blobInfo.blob());
xhr.send(formData);
}
});
}
};
</script>

封装为 Vue Component

用 v-model 传入初始文本, 配合 tinymce 的 change 和 keyup 事件, 自动更新数据.

用到的Api:
1 editor.setContent 和 editor.getContent 用于设置和获取 tinymce 的文本内容.
TinyMCE | tinymce.Editor

2 setup 参数接收一个 function作为参数, 该函数将在 tinymce 渲染(rended)前被调用. 函数的参数 editor 为当前编辑器的实例.
TinyMCE | Integration and setup options

components/tinymce-vue/tinymce-vue.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div id="tinymce-vue"></div>
</template>
<script>
export default {
model: {
prop: 'content',
event: 'changeContent'
},
props: {
content: {
type: String,
default: ''
}
},
mounted() {
const vm = this;
window.tinymce
.init({
selector: '#tinymce-vue',
setup: function(editor) {
editor.on('change keyup', () => {
const content = editor.getContent();
// 更新文本
vm.$emit('changeContent', content);
});
}
})
.then(editors => {
this.editor = editors[0];
// 载入初始文本
this.editor.setContent(this.content);
});
},
beforeDestroy() {
// 销毁
this.editor.remove();
this.editor.destroy();
},
};
</script>

使用方式

1
<tinymce-vue v-model="content"></tinymce>

另外,也可以用 v-bind 传设置参数进来, 然后用 Object.assign 和默认设置合并.

插件开发

以 line-height 插件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
!(function() {
'use strict';

/* eslint-disable no-undef */
const global = tinymce.util.Tools.resolve('tinymce.PluginManager');
// 注册插件
global.add('lineheight', function(editor) {
// 等待初始化,否则获取不到 editor.formatter
editor.on('init', function() {
// 注册样式
editor.formatter.register({
lineheight: {
selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table',
styles: { 'line-height': '%value' }
}
});
// 注册图标
editor.ui.registry.addIcon(
'line-height',
`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M9.984 12.984v-1.969h12v1.969h-12zM9.984 18.984v-1.969h12v1.969h-12zM9.984 5.016h12v1.969h-12v-1.969zM6 6.984v10.031h2.484l-3.469 3.469-3.516-3.469h2.484v-10.031h-2.484l3.516-3.469 3.469 3.469h-2.484z"></path>
</svg>
`
);
});
// 按钮
editor.ui.registry.addMenuButton('lineheightselect', {
tooltip: 'Line height', // 按钮提示,相当于 title
icon: 'line-height', // 按钮图标,就是上面注册的那个
// 点击下拉菜单按钮时调用,接收一个回调函数作为参数, 将一个菜单描述列表传递给回调函数
fetch: function(callback) {
const defaultLineHeightFormats = '1 1.5 1.75 2 3 4 5';
const userSettings = editor.settings.lineheight_formats;
const lineheightFormats = typeof userSettings === 'string' ? userSettings : defaultLineHeightFormats;

const items = lineheightFormats.split(' ').map(item => {
let text = item,
value = item;
const values = item.split('=');
if (values.length > 1) {
[text, value] = values;
}

return {
type: 'menuitem', // 按钮类型
text: text,
onAction: function() { // 事件,点击按钮项时调用
// 应用样式
editor.formatter.apply('lineheight', { value: value });
// 触发 change 事件
editor.fire('change', {});
}
};
});

callback(items);
}
});
});
})();

API文档:
fetch
formatter
formatter参数

参考

在 Vue 项目中引入 tinymce 富文本编辑器 - Wise.Wrong - 博客园
Vue 中使用 TinyMCE | 公孙二狗
富文本 | vue-element-admin
How to Upload Image in TinyMCE Editor using PHP - CodexWorld