欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Boot集成Tinymce富文本编辑器

程序员文章站 2024-03-25 13:30:46
...

基础环境

  • 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();
                }
            }
        });

实现效果
Spring Boot集成Tinymce富文本编辑器

保存功能

通过 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();
                }
            }
});