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表单的形式提交,也可以自己获取内容提交。

<!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脚本

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

渲染区域

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

实例化

支持图片、视频

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代码。

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!""}为后台返回的编辑区内容

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

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

tinymce.init({
           selector: '#content',
           height: 600,
           ...

汉化

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

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

图片上传

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

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

@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;
    }

渲染区配置

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", "当前文件实际下载地址");

@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;
    }

渲染区配置

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 许可协议。转载请注明来源 苏叶新城 !
  目录