Spring Boot集成Tinymce富文本编辑器


基础环境

  • IDEA
  • Spring Boot 2.2.1
  • Freemarker

tinymce简介

TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。同类程序有:UEditor、Kindeditor、Simditor、CKEditor、wangEditor、Suneditor、froala等。

经过多番对比(界面好看),认定Tinymce功能更加完善,并且具备可扩展特性,功能完善,遂采用。

小例子

下面是tinymce实例化的例子,通过id绑定,渲染页面。tinymce可以嵌入到任意web项目中。编辑内容的保存可以通过post表单的形式提交,也可以自己获取内容提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>

</head>

<body>
<h1>TinyMCE快速开始示例</h1>
<form method="post">
<textarea id="demo">Hello, World!</textarea>
</form>
</body
<script src='tinymce.min.js'></script>
<script>
tinymce.init({
selector: '#demo'//ID绑定
//此处可添加更多特性
});
</script>
</html>

正式开始

引入Tinymce脚本

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

渲染区域

1
2
3
<div class="layui-form-item">
<textarea id="content" name="content"></textarea>
</div>

实例化

支持图片、视频

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
tinymce.init({
selector: '#content',//绑定渲染区
height: 600,
plugins: 'paste importcss code table advlist fullscreen imagetools textcolor colorpicker hr autolink link image lists preview media wordcount',
toolbar: 'styleselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat hr | paste code link | undo redo | fullscreen',
skin: 'oxide',
language: 'zh_CN',//汉化
convert_urls: false,
// relative_urls : true,
images_upload_url: '../tmmedia/upload',//图片上传地址
images_upload_credentials: true,
image_dimensions: false,
image_class_list: [
{title: '无', value: ''},
{title: '预览', value: 'preview'},
],
// images_upload_base_path: '/',
forced_root_block: 'p',
force_p_newlines: true,
importcss_append: true,
content_style: `
* { padding:0; margin:0; }
html, body { height:100%; }
img { max-width:100%; display:block;height:auto; }
a { text-decoration: none; }
iframe { width: 100%; }
p { line-height:1.6; margin: 0px; }
table { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
.mce-object-iframe { width:100%; box-sizing:border-box; margin:0; padding:0; }
ul,ol { list-style-position:inside; }
`,
insert_button_items: 'image link | inserttable',
// CONFIG: Paste
paste_retain_style_properties: 'all',
paste_word_valid_elements: '*[*]', // word需要它
paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传
paste_convert_word_fake_lists: false, // 插入word文档需要该属性
paste_webkit_styles: 'all',
paste_merge_formats: true,
nonbreaking_force_tab: false,
paste_auto_cleanup_on_paste: false,

// CONFIG: Font
fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

// CONFIG: StyleSelect
style_formats: [
{
title: '首行缩进',
block: 'p',
styles: {'text-indent': '2em'}
},
{
title: '行高',
items: [
{title: '1', styles: {'line-height': '1'}, inline: 'span'},
{title: '1.5', styles: {'line-height': '1.5'}, inline: 'span'},
{title: '2', styles: {'line-height': '2'}, inline: 'span'},
{title: '2.5', styles: {'line-height': '2.5'}, inline: 'span'},
{title: '3', styles: {'line-height': '3'}, inline: 'span'}
]
}
],
// Tab
tabfocus_elements: ':prev,:next',
object_resizing: true,

// Image
imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions',
file_picker_types: 'media',
media_live_embeds: true,
//be used to add custom file picker to those dialogs that have it.
file_picker_callback: function (cb, value, meta) {
if (meta.filetype == 'media') {
//创建一个隐藏的type=file的文件选择input
let input = document.createElement('input');
input.setAttribute('type', 'file');
input.onchange = function(){
let file = this.files[0];//只选取第一个文件。如果要选取全部,后面注意做修改
let xhr, formData;
xhr = new XMLHttpRequest();
xhr.open('POST', '../tmmedia/upload');//自定义文件上传
xhr.withCredentials = true;
xhr.upload.onprogress = function (e) {
// 进度(e.loaded / e.total * 100)
};
xhr.onerror = function () {
console.log(xhr.status);
return;
};
xhr.onload = function () {
let json;
if (xhr.status < 200 || xhr.status >= 300) {
console.log('HTTP 错误: ' + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
console.log(json)
//接口返回的文件保存地址
let mediaLocation=json.location;
//cb()回调函数,将mediaLocation显示在弹框输入框中
cb(mediaLocation, { title: file.name });

};
formData = new FormData();
//假设接口接收参数为file,值为选中的文件
formData.append('file', file);
//正式使用将下面被注释的内容恢复
xhr.send(formData);
}
//触发点击
input.click();
}
}
});

实现效果

保存功能

通过 tinymce.activeEditor.getContent()获取编辑区内容,内容为html代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
form.on('submit(save)', function (data) {
var field = data.field;
//获取内容核心
field.content = tinymce.activeEditor.getContent()

var loadIndex;
loadIndex = layer.load(2);
$.post("../tmknowledgebase/saveOrUpdate", field, function (data) {
if (data.code === 200) {
tools.success("保存成功!");
layer.msg("成功!", {time: 1000}, function () {
layer.close(loadIndex);
//传给上个页面,刷新table用
tools.putTempData('submitOK', true);
//关掉对话框
tools.closeThisDialog();
});
} else {
layer.close(loadIndex)
tools.error(data.msg);
}
})
})

内容回显

在保存编辑内容后,如果我们想要再次编辑,需要对以保存内容进行回显,之前提到,保存的内容实际是html片段,因此采用html渲染即可。在此项目中采用Freemarker框架,渲染代码如下:

${data.content!""}为后台返回的编辑区内容

1
2
3
 <div class="layui-card-body layui-form-item">
<textarea id="content" name="content">${data.content!""}</textarea>
</div>

实例化区域代码与上文相同

1
2
3
4
tinymce.init({
selector: '#content',
height: 600,
...

汉化

tinymce默认是英文的,需要引入汉化包zh_CN.js到langs目录下,在language属性下添加zh_CN.

1
2
3
4
tinymce.init({
selector: '#content',
language:'zh_CN',//注意大小写
});

图片上传

首先后台自定义文件上传接口

注意:需要指定具体的下载地址,否则上传后文件无法回显 map.put("location", "当前文件实际下载地址");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping(value = "/tmmedia/upload")
public Object downloadFile(@RequestParam MultipartFile file) {
Map<String, String> map = new HashMap<>();
try {
String fileName = file.getOriginalFilename();
String extension = StringUtils.getFilenameExtension(fileName);
String name = IdUtil.fastUUID() + "." + extension;
ftpUtils.upload(dir, name, file);
// 文件下载,供前台回显
map.put("location", downloadUrl + dir + "/" + name);
} catch (Exception e) {
e.printStackTrace();
map.put("location", "");
}
return map;
}

渲染区配置

1
2
3
4
5
6
7
8
9
10
11
12
tinymce.init({
selector: '#content',
language:'zh_CN',//注意大小写
plugins: image ',
images_upload_url: '../tmmedia/upload',//图片上传地址
images_upload_credentials: true,
image_dimensions: false,
image_class_list: [
{title: '无', value: ''},
{title: '预览', value: 'preview'},
],
});

视频播放

首先后台自定义文件上传接口

==注意==:需要指定具体的下载地址,否则上传后文件无法回显 map.put("location", "当前文件实际下载地址");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping(value = "/tmmedia/upload")
public Object downloadFile(@RequestParam MultipartFile file) {
Map<String, String> map = new HashMap<>();
try {
String fileName = file.getOriginalFilename();
String extension = StringUtils.getFilenameExtension(fileName);
String name = IdUtil.fastUUID() + "." + extension;
ftpUtils.upload(dir, name, file);
// 文件下载,供前台回显
map.put("location", downloadUrl + dir + "/" + name);
} catch (Exception e) {
e.printStackTrace();
map.put("location", "");
}
return map;
}

渲染区配置

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
tinymce.init({
selector: '#content',
language:'zh_CN',//注意大小写
plugins: media ',
file_picker_types: 'media',
media_live_embeds: true,
//be used to add custom file picker to those dialogs that have it.
file_picker_callback: function (cb, value, meta) {
if (meta.filetype == 'media') {
//创建一个隐藏的type=file的文件选择input
let input = document.createElement('input');
input.setAttribute('type', 'file');
input.onchange = function(){
let file = this.files[0];//只选取第一个文件。如果要选取全部,后面注意做修改
let xhr, formData;
xhr = new XMLHttpRequest();
xhr.open('POST', '../tmmedia/upload');//自定义文件上传
xhr.withCredentials = true;
xhr.upload.onprogress = function (e) {
// 进度(e.loaded / e.total * 100)
};
xhr.onerror = function () {
console.log(xhr.status);
return;
};
xhr.onload = function () {
let json;
if (xhr.status < 200 || xhr.status >= 300) {
console.log('HTTP 错误: ' + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
console.log(json)
//接口返回的文件保存地址
let mediaLocation=json.location;
//cb()回调函数,将mediaLocation显示在弹框输入框中
cb(mediaLocation, { title: file.name });


};
formData = new FormData();
//假设接口接收参数为file,值为选中的文件
formData.append('file', file);
//正式使用将下面被注释的内容恢复
xhr.send(formData);
}
//触发点击
input.click();
}
}
});

文章作者: 苏叶新城
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 苏叶新城 !
  目录