Angular结合TinyMCE实现富文本编辑器

1. 背景介绍

自己编写了一个博客系统,想要支持用户写作博客,考查了几种开源编辑器方案,例如 Quill, ckEditor,最后还是选择了 TinyMCE 编辑器。

原因主要在与它开箱即用,插件丰富,而且很多插件都是比其他编辑器做得优秀。

2. 创建 Angular 工程

2.1. 创建工程

首先创建一个 angular 工程. 工程的名字就叫 angular-richtext-editor.

1
2
3

ng new angular-richtext-editor

2.2. 添加依赖

这里需要添加 tinymce/tinymce-angular 依赖包, 以下是 tinymce-angular 与 Angular 之间的兼容关系.

tinymce-angular, angular 的版本兼容性描述可以在tinymce-angular官网找到。

由于我目前使用的 angular 版本 17.3.2, 我选择的 tinymce-angular 为 8.x 版本。

1
2
3
4

cd angular-richtext-editor
npm install --save @tinymce/tinymce-angular@^8

2.3. 安装 tinymce

@tinymce/tinymce-angular 这个依赖包只是用来整合 angular 与 tinymce,但是真正需要的 tinymce 仍然没有包含在工程之中。

如果要安装 tinymce 有三种方法。

  • 通过 CDN 安装
  • 通过 npm manager 安装
  • 通过.zip 文件安装

2.3.1. 通过 CDN 安装 tinyMCE

使用 CDN 安装 tinyMCE 比较简单方便,但是需要到 tiny.cloud 上注册账号,并获取 apikey.

获取到 apikey 以后,将 apikey 配置到编辑器即可。

1
<editor apiKey="your-api-key" [init]="init" />

2.3.2. 通过 npm manager 安装 tinyMCE

通过 npm manager 安装 tinyMCE 分为以下几个步骤:

  1. 安装 tinyMCE 依赖包
1
2
3

npm install --save tinymce

  1. 配置 angular.json, 将 tinymce 单独编译为文件。
1
2
3
"assets": [
{ "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" }
]
  1. 加载 TinyMCE

要在编辑器初始化时加载 TinyMCE(也称为延迟加载),请使用 TinyMCE_SCRIPT_SRC 令牌向组件添加依赖项提供程序。

1
2
3
4
5
6
7
8
9
10
import { EditorComponent, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
/* ... */
@Component({
/* ... */
standalone: true,
imports: [EditorComponent],
providers: [
{ provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }
]
})

或者:
要在加载页面或应用程序时加载 TinyMCE,请执行以下操作:

打开 angular.json 并将 TinyMCE 添加到全局脚本标记中。

1
2
3
"scripts": [
"node_modules/tinymce/tinymce.min.js"
]

更新编辑器配置以包括 base_url 和后缀选项。

1
2
3
4
5
6
7
8
9
10

export class AppComponent {
/* ... */
init: EditorComponent['init'] = {
/* ... */
base_url: '/tinymce', // Root for resources
suffix: '.min' // Suffix to use when loading resources
};
}

2.3.3. 通过.zip 文件安装 tinyMCE

通过 zip 包安装 tinyMCE,可以参考这篇文章

2.4. 配置 tinyMCE

首先在 component 定义配置

1
2
3
4
5
6
7
8
9
10
11

export class AppComponent {
/* ... */
init: EditorComponent['init'] = {
/* ... */
base_url: '/tinymce', // Root for resources
suffix: '.min' // Suffix to use when loading resources
/* ... */
};
}

一份完整的配置列表如下所示

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

tinyConfig: EditorComponent['init'] = {
plugins:
'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap quickbars emoticons accordion fullscreen',
editimage_cors_hosts: ['picsum.photos'],
menubar: false,
language: 'zh_CN',
toolbar:
'undo redo | code preview | blocks fontfamily fontsize | bold italic underline strikethrough removeformat | align numlist bullist | link image media table | lineheight outdent indent| forecolor backcolor | charmap emoticons | save print | pagebreak anchor codesample | ltr rtl | fullscreen',
autosave_ask_before_unload: true,
autosave_interval: '30s',
autosave_prefix: '{path}{query}-{id}-',
autosave_restore_when_empty: false,
autosave_retention: '2m',
image_advtab: true,
quickbars_insert_toolbar: false,
link_list: [
{ title: 'My page 1', value: 'https://www.tiny.cloud' },
{ title: 'My page 2', value: 'http://www.moxiecode.com' },
],
image_list: [
{ title: 'My page 1', value: 'https://www.tiny.cloud' },
{ title: 'My page 2', value: 'http://www.moxiecode.com' },
],
image_class_list: [
{ title: 'None', value: '' },
{ title: 'Some class', value: 'class-name' },
],
importcss_append: true,
file_picker_callback: this.filePickHandler.bind(this),
height: 600,
image_caption: true,
quickbars_selection_toolbar:
'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
noneditable_class: 'mceNonEditable',
contextmenu: 'link image table',
skin: 'oxide',
content_css: 'default',
content_style:
'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
};

3. 使用tinyMCE

app.component.html

1
2
3
4
5
<h1>TinyMCE 7 Angular Demo</h1>
<editor
[init]="tinyConfig"
></editor>

4. 设置语言

修改语言设置,默认为英文

1
2
3
4
5
6
7
8

tinyConfig: EditorComponent['init'] = {
......
language_url: '/assets/js/langs/zh_CN.js',
language: 'zh_CN',
.......
}

tiny cloud languagepacks 下载中文语言包,将其放置在例如:src/assets/js/langs/

将 language_url 指向 zh_CN.js. Language 设置为 zh_CN。

修改 angular.json, 将 assets 包含在 assets 内。

1
2
3
4
"assets": [
......
"src/assets",
......

5. 如何上传图片到self hosted服务器

首先将tinyMCE配置的file_picker_callback指向自定义函数filePickHandler

file_picker_callback: this.filePickHandler.bind(this),

以下是一段客户端代码。

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

filePickHandler(callback: any, value: any, meta: any) {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
let that = this;
input.addEventListener('change', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
const file: File = target.files[0];

const formData = new FormData();
formData.append('file', file, file.name);

that.http
.post(
'url_to_file_upload_server',
formData
)
.subscribe({
next: (response: any) => {
const href = response.url;
/* call the callback and populate the Title field with the file name */
callback(href, { title: file.name });
},
error: (err: any)=> {
console.log(err)
}
});
}
});

input.click();
}

url_to_file_upload_server 修改为服务端url.

服务端代码golang 版本如下:

由于涉及到安全性问题,这里只公开部分代码。

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

// @Summary 上传文件
// @Description 上传文件
// @Tags UploadCover
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "文件"
// @Success 0 {object} Response[string]
// @Router /upload/file [post]
func (ctrl *blogController) UploadCover(c *gin.Context) {
_, fileHeader, err := c.Request.FormFile("file")
if err != nil {
control.ReturnError(c, control.ErrFileReceive, err)
return
}

oss := upload.NewOSS()
filePath, _, err := oss.UploadFile(fileHeader) //将文件保存至服务器。
if err != nil {
control.ReturnError(c, control.ErrFileUpload, err)
return
}

control.ReturnSuccessWithImgUrl(c, filePath)
}
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

// 文件上传到本地
func (ls *Local) UploadFile(file *multipart.FileHeader) (filePath, fileName string, err error) {
ext := path.Ext(file.Filename) // 读取文件后缀
name := strings.TrimSuffix(file.Filename, ext) // 读取文件名
name = utils.MD5(name) // 加密文件名
filename := name + "_" + time.Now().Format("20060102150405") + ext // 拼接新文件名

//conf := g.Conf.Upload
storeDir := 获取服务器端存储位置文件夹子
displayPath := 文件上传后,获取文件的的url
mkdirErr := os.MkdirAll(storeDir, os.ModePerm) // 尝试创建存储路径
if mkdirErr != nil {
zap.S().Error("function os.MkdirAll() Filed", mkdirErr)
return "", "", errors.New("function os.MkdirAll() Filed, err:" + mkdirErr.Error())
}

storePath := storeDir + "/" + filename // 文件存储路径
filepath := displayPath + "/" + filename // 文件展示路径

f, openError := file.Open() // 读取文件
if openError != nil {
zap.S().Error("function file.Open() Failed", openError)
return "", "", errors.New("function file.Open() Failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭

out, createErr := os.Create(storePath)
if createErr != nil {
zap.S().Error("function os.Create() Failed", createErr)
return "", "", errors.New("function os.Create() Failed, err:" + createErr.Error())
}
defer out.Close() // 创建文件 defer 关闭

_, copyErr := io.Copy(out, f) // 拷贝文件
if copyErr != nil {
zap.S().Error("function io.Copy() Failed", copyErr)
return "", "", errors.New("function io.Copy() Failed, err:" + copyErr.Error())
}
return filepath, filename, nil
}

6. 参考文档

Using the TinyMCE package with the Angular framework

Angular结合TinyMCE实现富文本编辑器

https://pengtech.net/angular/angular_tinyMCE_editor.html

作者

鹏叔

发布于

2024-06-24

更新于

2024-08-23

许可协议

评论