美文网首页
Tinymce在线编辑器

Tinymce在线编辑器

作者: 三没产品 | 来源:发表于2021-04-01 11:43 被阅读0次

环境:VUE2.x + Tinymce + Tinymce-Vue + element

安装tinymce-vue

npm i @tinymce/tinymce-vue@3.2.8

注意:因为tinymce-vue4.x版本旨在支持Vue3

image.png

安装tinymce

npm i tinymce@5.7.1

复制icons、skins

安装完成后,在 public文件夹 下创建 tinymce文件夹,然后在node_modules下找到 tinymce,注意不是 @tinymce,复制 icons、skins 文件夹到 public/tinymce

下载中文语言包

下载地址:https://www.tiny.cloud/get-tiny/language-packages/,下载 zh_CN,下载后,把解压后的 langs文件夹 放到 public/tinymce

image.png

tinymceEditor组件

components文件夹 下TinymceEditor.vue文件,复制以下代码

<template>
  <div>
    <Editor v-model="myValue" :id="tinymceId" :init="init" :disabled="disabled"></Editor>
    <input type="file" hidden :id="'file-' + tinymceId"/>
  </div>
</template>

<script>

import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons' ////解决了icons.js 报错Unexpected token '<'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/print'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/code'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/code'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'

export default {
  name: 'TinymceEditor',
  components: {Editor},
  props: {
    id: {
      type: String,
      default: function () {
        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
      }
    },
    value: {
      type: String,
      default: ''
    },
    height: {
      type: [Number, String],
      required: false,
      default: 200
    },
    width: {
      type: [Number, String],
      required: false,
      default: 'auto'
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      tinymceId: this.id,
      myValue: this.value,
      oldValue: '',
      init: {
         selector: `#${this.id}`,
        content_style: "p {margin: 0; border:0; padding: 0;}",
        language_url: '/tinymce/langs/zh_CN.js',
        language: 'zh_CN',
        skin_url: '/tinymce/skins/ui/oxide',
        height: this.height,
        body_class: 'panel-body ',
        object_resizing: true,//是否允许调整图像大小.
        branding: false, //隐藏右下角由TINY驱动
        contextmenu_never_use_native: true, //防止浏览器上下文菜单出现在编辑器中
        elementpath: false, //隐藏底栏的元素路径(隐藏右下角元素显示)
        toolbar: 'code | undo redo | lineheight| fontselect fontsizeselect formatselect | bold italic forecolor backcolor | \
               alignleft aligncenter alignright alignjustify | \
               bullist numlist outdent indent | removeformat | imageUpload | help',
        menubar: false,
        plugins: ['advlist autolink lists link image charmap print preview anchor',
          'searchreplace visualblocks code fullscreen',
          'insertdatetime media table paste help wordcount'],
        paste_retain_style_properties: 'all', //从Word中复制,保留所有样式
        //paste_word_valid_elements: '*[*]', // 该设置在自word粘贴时,允许指定元素和属性保存在过滤结果中,要使用此功能,paste_enable_default_filters 要设置为true
        paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
        paste_convert_word_fake_lists: false, // 设为false可禁用复制word中的列表内容时,转换为html的UL或OL格式。
        paste_webkit_styles: 'all',
        paste_merge_formats: true, //启用PowerPaste插件的合并格式功能,例如:<b>abc <b>bold</b> 123</b>成为<b>abc bold 123</b>
        end_container_on_empty_block: true, //如果在空的内部块元素中按下Enter键,则可以使用此选项拆分当前容器块元素。
        powerpaste_word_import: 'clean', //保留标题,表格和列表等内容的结构,但删除内联样式和类。这样就产生了使用站点CSS样式表的简单内容,同时保留了原始文档的语义结构。
        advlist_bullet_styles: 'default,circle,disc,square',
        advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
        default_link_target: '_blank', //默认链接是当前窗口打开,你也可以通过此参数将其变为_blank新窗口打开。
        link_title: false,
        nonbreaking_force_tab: true, // 按tab键插入三个&nbsp;。一直按一直插。
        images_upload_handler: this.images_upload_handler, //该images_upload_handler选项允许你指定被用来替代TinyMCE的默认的JavaScript和定制逻辑上传处理函数的函数。
        setup: this.setup, //,您可以指定在渲染TinyMCE编辑器实例之前将执行的回调。
        convert_urls: false
      }
    }
  },
  watch: {
    value(newValue) {
      this.myValue = newValue
    },
    myValue(newValue) {
      this.$emit('input', newValue)
    }
  },
  mounted() {
    tinymce.init({})
  },
  methods: {
    images_upload_handler(blobInfo, success, failure, progress) {
      //this.oldValue = this.value;
      let fd = new FormData();
      fd.append('imageFile', blobInfo.blob(), blobInfo.filename());
      this.$http.post('/editor/uploadImg', fd).then(res => {
        if (res.data.code == 200) {
          success(res.data.data);
        } else {
          //这样写是因为复制进来的图片进入这方法时就以base64存在(即在this.oldValue以base64存在),
          //如果上传失败this.oldValue中已存在图片,所以setContent是不能回滚
          this.$message.error("图片上传失败");
          success("");
          // failure('上传失败!');
          // this.setContent(this.oldValue);
        }
      }).catch(res => {
          this.$message.error("图片上传失败");
          success("");
          // failure('上传失败!');
          // this.setContent(this.oldValue);
      })
    },
    setup(editor) {
      //注册一个图片按钮
      editor.ui.registry.addButton('imageUpload', {
        tooltip: '图片',
        icon: 'image',
        onAction: () => {
          //点击按钮后执行
          this.oldValue = this.value;
          let self = this;
          let input = document.getElementById("file-" + this.tinymceId);
          input.click();
          input.onchange = function () {
            let file = input.files[0];
            let fd = new FormData();
            fd.append('imageFile', file);
            self.$http.post('/editor/uploadImg', fd).then(res => {
              if (res.data.code == 200) {
                tinymce.get(self.tinymceId).insertContent("<img src='" + res.data.data + "'>");
                input.value = '';
              } else {
                self.setContent(self.oldValue);
              }
            }).catch(res => {
              self.setContent(self.oldValue);
            })
          }
        }
      });
    },
  }
}
</script>

<style lang="stylus" scoped>
.tinymce-container {
  position: relative;
  line-height: normal;
}

.tinymce-container {
  ::v-deep {
    .mce-fullscreen {
      z-index: 10000;
    }
  }
}

.tinymce-textarea {
  visibility: hidden;
  z-index: -1;
}

.editor-custom-btn-container {
  position: absolute;
  right: 4px;
  top: 4px;
  //z-index: 2005;
}

.fullscreen .editor-custom-btn-container {
  z-index: 10000;
  position: fixed;
}

.editor-upload-btn {
  display: inline-block;
}
</style>
<style>
.oe-editor-wrap{
    position: relative;
}
.oe-editor-del-btn{
    position: absolute;
    top:0;
    right:0;
    z-index:3010;
    padding:0 2px 1px;
    border-left:1px solid #DCDFE6;
}
.oe-editor-del-btn img{
    height:36px;
}
.tox .tox-dialog-wrap__backdrop {
    z-index: 3000;
    background-color: rgba(0, 0, 0, .6);
}

.tox .tox-dialog {
    z-index: 3001;
}
/*调整堆叠顺序,让下拉列表等显示*/
.tox-tinymce-aux {
    z-index: 3002!important;
}
.tox-tinymce{
    border: 1px solid #DCDFE6!important;
}
.tox .tox-statusbar{
    border-top: 1px solid #DCDFE6!important;
}
.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){
    border-right: 1px solid #DCDFE6!important;
}

.tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary{
    background: none!important;
    border-bottom:1px solid #DCDFE6;
}
.tox .tox-tbtn svg{
    fill:#7b8397!important;
}
</style>

使用

import editor from "@/components/TinymceEditor";

<editor v-model="record.content"></editor>
image.png

不引用@tinymce/tinymce-vue的tinymce组件

<template>
    <div>
        <textarea :id="tinymceId"></textarea>
        <input type="file" hidden :id="'file-' + tinymceId"/>
        <div class="oe-editor-del-btn" v-if="showDel">
            <img src="@/assets/editor_del.png" alt="">
        </div>
    </div>
</template>

<script>
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons' ////解决了icons.js 报错Unexpected token '<'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/link'
import 'tinymce/plugins/image'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/print'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/code'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/help'
import 'tinymce/plugins/wordcount'
//import 'tinymce/plugins/imagetools'


import {editorUploadImg} from "@/api/public";

const INIT = 0;
const INPUT = 1;
const CHANGED = 2;

export default {
    name: "Tinymce",
    props: {
        id: {
            type: String,
            default: function () {
                return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
            }
        },
        value: {
            type: String,
            default: ''
        },
        height: {
            type: [Number, String],
            required: false,
            default: 200
        },
        width: {
            type: [Number, String],
            required: false,
            default: 'auto'
        },
        readonly: {
            type: Boolean,
            default: false
        },
        showDel: {
            type: Boolean,
            default: () => {
                return false
            }
        }
    },
    data() {
        return {
            tinymceId: this.id,
            oldValue: '',
            status: CHANGED,
            init: {
                selector: `#${this.id}`,
                content_style: "p {margin: 0; border:0; padding: 0;}",
                language_url: '/tinymce/langs/zh_CN.js',
                language: 'zh_CN',
                skin_url: '/tinymce/skins/ui/oxide',
                height: this.height,
                body_class: 'panel-body ',
                object_resizing: true, //是否允许调整图像大小.
                branding: false, //隐藏右下角由TINY驱动
                contextmenu_never_use_native: true, //防止浏览器上下文菜单出现在编辑器中
                elementpath: false, //隐藏底栏的元素路径(隐藏右下角元素显示)
                toolbar: 'code | undo redo | lineheight| fontselect fontsizeselect formatselect | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | imageUpload | help',
                menubar: false,
                plugins: ['advlist autolink lists link image charmap print preview anchor',
                    'searchreplace visualblocks code fullscreen',
                    'insertdatetime media table paste help wordcount'],
                paste_retain_style_properties: 'all', //从Word中复制,保留所有样式
                paste_word_valid_elements: '*[*]', // 该设置在自word粘贴时,允许指定元素和属性保存在过滤结果中,要使用此功能,paste_enable_default_filters 要设置为true
                paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
                paste_convert_word_fake_lists: false, // 设为false可禁用复制word中的列表内容时,转换为html的UL或OL格式。
                paste_webkit_styles: 'all',
                paste_merge_formats: true, //启用PowerPaste插件的合并格式功能,例如:<b>abc <b>bold</b> 123</b>成为<b>abc bold 123</b>
                end_container_on_empty_block: true, //如果在空的内部块元素中按下Enter键,则可以使用此选项拆分当前容器块元素。
                advlist_bullet_styles: 'default,circle,disc,square',
                advlist_number_styles: 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman',
                default_link_target: '_blank', //默认链接是当前窗口打开,你也可以通过此参数将其变为_blank新窗口打开。
                link_title: false,
                nonbreaking_force_tab: true, // 按tab键插入三个&nbsp;。一直按一直插。
                images_upload_handler: this.images_upload_handler, //该images_upload_handler选项允许你指定被用来替代TinyMCE的默认的JavaScript和定制逻辑上传处理函数的函数。
                setup: this.setup, //,您可以指定在渲染TinyMCE编辑器实例之前将执行的回调。
                convert_urls: false,
                readonly: this.readonly,
                init_instance_callback : this.init_instance_callback,
                // lineheight_val:
                //     '1 1.1 1.2 1.3 1.35 1.4 1.5 1.55 1.6 1.75 1.8 1.9 1.95 2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 3 3.1 3.2 3.3 3.4 4 5',
                // fontsize_formats: '8pt 10pt 11pt 12pt 13pt 14pt 15pt 16pt 17pt 18pt 24pt 36pt',
                // font_formats:
                //     "微软雅黑='微软雅黑';宋体='宋体';黑体='黑体';仿宋='仿宋';楷体='楷体';隶书='隶书';幼圆='幼圆';Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings",
            }
        }
    },
    watch: {
        value(newValue) {
            if(this.status === CHANGED || this.status === INIT) return this.status = INPUT;
            if (!newValue) {
                tinymce.get(this.tinymceId).setContent('');
            } else {
                tinymce.get(this.tinymceId).setContent(newValue);
            }
        }
    },
    mounted() {
        tinymce.init(this.init);
    },
    methods: {
        images_upload_handler(blobInfo, success, failure, progress) {
            // this.oldValue = this.value;
            const fd = new FormData();
            fd.append('imageFile', blobInfo.blob(), blobInfo.filename());
            editorUploadImg(fd).then(res => {
                if (res && res.code === 1) {
                    success(process.env.VUE_APP_BASE_API + res.data);
                } else {
                    this.$message.error("图片上传失败");
                    success("");
                    // failure('上传失败!');
                    // this.setContent(this.oldValue);
                }
            }).catch(() => {
                this.$message.error("图片上传失败");
                success("");
                // failure('上传失败!');
                // this.setContent(this.oldValue);
            });
        },
        setup(editor) {
            editor.on('init', (e) => {
                editor.setContent(this.value);
                editor.on('input change undo redo execCommand', () => {
                    // 只在用户输入导致事件相应时才更新value数据,不然的话会导致光标跳到首位
                    if(this.status === INPUT || this.status === INIT) return this.status = CHANGED;
                    this.$emit('input', editor.getContent());
                });
            })
            //注册一个图片按钮
            editor.ui.registry.addButton('imageUpload', {
                tooltip: '图片',
                icon: 'image',
                onAction: () => {
                    //点击按钮后执行
                    this.oldValue = this.value;
                    const self = this;
                    const input = document.getElementById("file-" + this.tinymceId);
                    input.click();
                    input.onchange = function () {
                        const file = input.files[0];
                        const fd = new FormData();
                        fd.append('imageFile', file);
                        editorUploadImg(fd).then(res => {
                            if (res && res.code === 1) {
                                tinymce.get(self.tinymceId).insertContent(`<img src='${process.env.VUE_APP_BASE_API + res.data}' alt=""/>`);
                                input.value = '';
                            } else {
                                self.setContent(self.oldValue);
                            }
                        }).catch(() => {self.setContent(self.oldValue);});
                    }
                }
            });
        },
        init_instance_callback(editor) {
            // console.log("ID为: " + editor.id + " 的编辑器已初始化完成.");
            // editor.on('change', (e) => {
            //     this.$emit('change', e.level.content)
            // })
            // editor.on('input', (e) => {
            //     this.$emit('input', e.target.innerHTML)
            // });
        }
    }
}
</script>

<style lang="scss" scoped>
.tinymce-container {
    position: relative;
    line-height: normal;
}

.tinymce-container {
    ::v-deep {
        .mce-fullscreen {
            z-index: 10000;
        }
    }
}

.tinymce-textarea {
    visibility: hidden;
    z-index: -1;
}

.editor-custom-btn-container {
    position: absolute;
    right: 4px;
    top: 4px;
    //z-index: 2005;
}

.fullscreen .editor-custom-btn-container {
    z-index: 10000;
    position: fixed;
}

.editor-upload-btn {
    display: inline-block;
}
</style>

<style lang="scss">
.oe-editor-wrap{
    position: relative;
}
.oe-editor-del-btn{
    position: absolute;
    top:0;
    right:0;
    z-index:3010;
    padding:0 2px 1px;
    border-left:1px solid #DCDFE6;
}
.oe-editor-del-btn img{
    height:36px;
}
.tox .tox-dialog-wrap__backdrop {
    z-index: 3000;
    background-color: rgba(0, 0, 0, .6);
}

.tox .tox-dialog {
    z-index: 3001;
}
/*调整堆叠顺序,让下拉列表等显示*/
.tox-tinymce-aux {
    z-index: 3002!important;
}
.tox-tinymce{
    border: 1px solid #DCDFE6!important;
}
.tox .tox-statusbar{
    border-top: 1px solid #DCDFE6!important;
}
.tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type){
    border-right: 1px solid #DCDFE6!important;
}

.tox .tox-toolbar, .tox .tox-toolbar__overflow, .tox .tox-toolbar__primary{
    background: none!important;
    border-bottom:1px solid #DCDFE6;
}
.tox .tox-tbtn svg{
    fill:#7b8397!important;
}
</style>

相关文章

网友评论

      本文标题:Tinymce在线编辑器

      本文链接:https://www.haomeiwen.com/subject/icctkltx.html