【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展

【【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展

    • 引言:
    • 一、 开始
    • 二、快速开始
      • 1、安装Tinymce
    • 三、封装成Vue组件
      • 1、文件结构
      • 2、index.vue
      • 3、dynamicLoadScript.js
      • 4、plugin.js
      • 5、toolbar.js
    • 四、使用Tinymce组件
    • 五、业务逻辑实现
      • 1、添加页面只读模式,解决方案(`readonly: true`):
        • - a、在组件中添加`props`
        • - b、在组件初始化的时候添加该配置
        • - c、使用组件时传参
      • 2、数据处理:传数据给后端需要进行base64加密,但是会把标签尖括号变成中文,导致回显时错误,解决方案:
        • - a、保存时转码之后再加密:
        • - b、回显时解密再转码
      • 3、打开页面时会出现Tinymce还未实例化的情况,页面展示空白,解决方案:
      • 4、在Tinymce编辑器上方自定义按钮,打开一个弹窗,选定一个参数添加至编辑器中鼠标点击的位置
      • 5、在Tinymce编辑器实现右击可以选择只粘贴文本

引言:

在Vue项目的开发过程中,经常需要使用富文本编辑器来处理用户的输入内容。Tinymce 是一个功能强大且易于使用的富文本编辑器,它支持大多数常见的文本编辑功能,并且可以通过插件进行扩展。本文将详细介绍如何在Vue项目中使用Tinymce富文本编辑器。

一、 开始

官网文档:https://www.tiny.cloud/docs/
中文文档:http://tinymce.ax-z.cn/
社区版及开发版官方最新打包地址:https://www.tiny.cloud/get-tiny/self-hosted/
汉化包:http://tinymce.ax-z.cn/static/tiny/langs/zh_CN.js

二、快速开始

1、安装Tinymce

首先,在Vue项目的根目录下打开终端,运行以下命令来安装Tinymce

npm install tinymce

上述命令会下载并安装Tinymce的依赖到你的项目中。

三、封装成Vue组件

1、文件结构

【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展_第1张图片

2、index.vue

<template>
  <div :class="{ fullscreen: fullscreen }" class="tinymce-container" :style="{ width: containerWidth }">
    <textarea :id="tinymceId" class="tinymce-textarea" />
  div>
template>

<script>
/**
 * docs:
 * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
 */
import plugins from './plugins'
import toolbar from './toolbar'
import load from './dynamicLoadScript'

// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
// http://cdn.jsdelivr.net无法访问了,将cdn.jsdelivr.net域名替换为fastly.jsdelivr.net或者gcore.jsdelivr.net
// const tinymceCDN = 'https://cdn.jsdelivr.net/npm/[email protected]/tinymce.min.js'
const tinymceCDN = 'https://fastly.jsdelivr.net/npm/[email protected]/tinymce.min.js'

export default {
  name: 'Tinymce',
  props: {
    id: {
      type: String,
      default: function () {
        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
      }
    },
    value: {
      type: String,
      default: ''
    },
    toolbar: {
      type: Array,
      required: false,
      default() {
        return []
      }
    },
    menubar: {
      type: String,
      default: 'file edit insert view format table'
    },
    height: {
      type: [Number, String],
      required: false,
      default: 360
    },
    width: {
      type: [Number, String],
      required: false,
      default: 'auto'
    }
  },
  data() {
    return {
      hasChange: false,
      hasInit: false,
      tinymceId: this.id,
      fullscreen: false,
      languageTypeList: {
        en: 'en',
        zh: 'zh_CN',
        es: 'es_MX',
        ja: 'ja'
      }
    }
  },
  computed: {
    language() {
      return this.languageTypeList['zh']
    },
    containerWidth() {
      const width = this.width
      if (/^[\d]+(\.[\d]+)?$/.test(width)) {
        // matches `100`, `'100'`
        return `${width}px`
      }
      return width
    }
  },
  watch: {
    value(val) {
      if (!this.hasChange && this.hasInit) {
        this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(val || ''))
      }
    },
    language() {
      // this.destroyTinymce()
      this.$nextTick(() => this.initTinymce())
    }
  },
  mounted() {
    this.init()
  },
  activated() {
    if (window.tinymce) {
      this.initTinymce()
    }
  },
  deactivated() {
    this.destroyTinymce()
  },
  destroyed() {
    this.destroyTinymce()
  },
  methods: {
    init() {
      // dynamic load tinymce from cdn
      load(tinymceCDN, (err) => {
        if (err) {
          this.$message.error(err.message)
          return
        }
        this.initTinymce()
      })
    },
    initTinymce() {
      const _this = this
      window.tinymce.init({
        language: this.language,
        selector: `#${this.tinymceId}`,
        height: this.height,
        body_class: 'panel-body',
        branding: false,
        object_resizing: false,
        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
        menubar: this.menubar,
        plugins: plugins,
        toolbar_drawer: true,
        end_container_on_empty_block: true,
        powerpaste_word_import: 'clean',
        paste_data_images: true, //允许粘贴base64图片
        paste_enable_default_filters: false, //word文本设置
        code_dialog_height: 450,
        code_dialog_width: 1000,
        advlist_bullet_styles: 'default,circle,disc,square',
        //advlist_number_styles: 'default',
        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
        default_link_target: '_blank',
        link_title: true,
        fontsize_formats: '12px 14px 16px 18px 24px 36px 48px 56px 72px',
        font_formats:
          '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
        nonbreaking_force_tab: true, // inserting nonbreaking space   need Nonbreaking Space Plugin
        statusbar: false,
        init_instance_callback: (editor) => {
          console.log('init_instance_callback', editor)
          if (_this.value) {
            editor.setContent(_this.value)
          }
          _this.hasInit = true
          editor.on('NodeChange Change KeyUp SetContent', () => {
            this.hasChange = true
            this.$emit('input', editor.getContent())
          })
        },
        setup(editor) {
          editor.on('FullscreenStateChanged', (e) => {
            _this.fullscreen = e.state
          })
        },
        // it will try to keep these URLs intact
        // https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
        // https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
        convert_urls: false,
        // 整合七牛上传
        // images_dataimg_filter(img) {
        //   setTimeout(() => {
        //     const $image = $(img);
        //     $image.removeAttr('width');
        //     $image.removeAttr('height');
        //     if ($image[0].height && $image[0].width) {
        //       $image.attr('data-wscntype', 'image');
        //       $image.attr('data-wscnh', $image[0].height);
        //       $image.attr('data-wscnw', $image[0].width);
        //       $image.addClass('wscnph');
        //     }
        //   }, 0);
        //   return img
        // },
        images_upload_handler(blobInfo, success, failure, progress) {
          // progress(0);
          // const token = _this.$store.getters.token;
          // getToken(token).then(response => {
          //   const url = response.data.qiniu_url;
          //   const formData = new FormData();
          //   formData.append('token', response.data.qiniu_token);
          //   formData.append('key', response.data.qiniu_key);
          //   formData.append('file', blobInfo.blob(), url);
          //   upload(formData).then(() => {
          //     success(url);
          //     progress(100);
          //   })
          // }).catch(err => {
          //   failure('出现未知问题,刷新页面,或者联系程序员')
          //   console.log(err);
          // });
          const img = `data:${blobInfo.blob().type};base64,${blobInfo.base64()}`
          success(img)
        }
      })
    },
    destroyTinymce() {
      const tinymce = window.tinymce.get(this.tinymceId)
      if (this.fullscreen) {
        tinymce.execCommand('mceFullScreen')
      }

      if (tinymce) {
        tinymce.destroy()
      }
    },
    setContent(value) {
      window.tinymce.get(this.tinymceId).setContent(value)
    },
    getContent() {
      window.tinymce.get(this.tinymceId).getContent()
    },
    imageSuccessCBK(arr) {
      arr.forEach((v) => window.tinymce.get(this.tinymceId).insertContent(`${v.url}" >`))
    }
  }
}
script>

<style lang="less" scoped>
.tinymce-container {
  position: relative;
  line-height: normal;
  /deep/ * {
    border-color: #efefef;
    white-space: pre-wrap;
  }
}

.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>

3、dynamicLoadScript.js

//dynamicLoadScript.js 动态导入tinymce.js脚本
let callbacks = []

function loadedTinymce() {
  // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
  // check is successfully downloaded script
  return window.tinymce
}

const dynamicLoadScript = (src, callback) => {
  const existingScript = document.getElementById(src)
  const cb = callback || function () {}

  if (!existingScript) {
    const script = document.createElement('script')
    script.src = src // src url for the third-party library being loaded.
    script.id = src
    document.body.appendChild(script)
    callbacks.push(cb)
    const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
    onEnd(script)
  }

  if (existingScript && cb) {
    if (loadedTinymce()) {
      cb(null, existingScript)
    } else {
      callbacks.push(cb)
    }
  }

  function stdOnEnd(script) {
    script.onload = function () {
      // this.onload = null here is necessary
      // because even IE9 works not like others
      this.onerror = this.onload = null
      for (const cb of callbacks) {
        cb(null, script)
      }
      callbacks = null
    }
    script.onerror = function () {
      this.onerror = this.onload = null
      cb(new Error('Failed to load ' + src), script)
    }
  }

  function ieOnEnd(script) {
    script.onreadystatechange = function () {
      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
      this.onreadystatechange = null
      for (const cb of callbacks) {
        cb(null, script) // there is no way to catch loading errors in IE8
      }
      callbacks = null
    }
  }
}

export default dynamicLoadScript

4、plugin.js

// Any plugins you want to use has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/

const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor visualblocks visualchars wordcount']

export default plugins

5、toolbar.js

// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols

const toolbar = [
  'searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen',
  'formatselect fontselect fontsizeselect'
]

export default toolbar

四、使用Tinymce组件

<template>
	<TinyMce
       ref="tiny"
       v-model="mdlValue.fullText"
       :toolbar="toolbar"
       height="400px"
       :menubar="''"
    >TinyMce>
template>
<script>
import TinyMce from '../Tinymce/index'
export default {
	components:{TinyMce },
	data(){
		toolbar: [
        'searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample hr bullist numlist link image charmap preview insertdatetime emoticons forecolor backcolor',
        'formatselect fontselect fontsizeselect'
      	],
	}
}
script>

五、业务逻辑实现

1、添加页面只读模式,解决方案(readonly: true):

通过查文档可以知道 readonly: true 可以配置Tinymce是否只读,然后把他封装到我们的组件里

- a、在组件中添加props

【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展_第2张图片

- b、在组件初始化的时候添加该配置

【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展_第3张图片

- c、使用组件时传参

【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展_第4张图片

2、数据处理:传数据给后端需要进行base64加密,但是会把标签尖括号变成中文,导致回显时错误,解决方案:

- a、保存时转码之后再加密:
this.fullText = Base64.encode(this.fullText.replace(/</g, '<').replace(/>/g,'>'))
- b、回显时解密再转码
this.fullText = Base64.decode(data.fullText).replace(/</g, '<').replace(/>/g, '>'))

3、打开页面时会出现Tinymce还未实例化的情况,页面展示空白,解决方案:

  • a、给Tinymce组件绑定**key** 值
    【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展_第5张图片

  • b、在使用Tinymce组件的页面的 mouted 去实例化
    【超详细】vue项目:Tinymce富文本使用教程以及踩坑总结+功能扩展_第6张图片

4、在Tinymce编辑器上方自定义按钮,打开一个弹窗,选定一个参数添加至编辑器中鼠标点击的位置

5、在Tinymce编辑器实现右击可以选择只粘贴文本

你可能感兴趣的:(vue.js,前端,javascript)