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

【超详细】vue项目: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的依赖到你的项目中。

三、配置说明

{
width: '100%', //  设置富文本编辑器宽度
height: '100%', //  设置富文本编辑器高度
menubar: false, // 设置富文本编辑器菜单, 默认true
branding: false, // 关闭底部官网提示 默认true
statusbar: true, // 显示底部状态栏 默认true
readonly: false, // 设置只读属性 默认 false
resize: false, // 调节编辑器大小 默认 true
autosave_ask_before_unload: true, // 阻止有内容时浏览器阻塞行为, 默认 true  需引入插件autosave
autosave_interval: '3s', // 设置自动保存为草稿时间 单位只能为s 需引入插件autosave
autosave_prefix: `editor_${_this.$route.path}`, // 设置自动保存为草稿时前缀 本地localStorage中存储  需引入插件autosave
autosave_retention: '300m', // 自动草稿的有效期 单位只能为m  需引入插件autosave
contextmenu: 'copy paste cut link', // 上下文菜单 默认 false
draggable_modal: true, // 模态框拖动 默认false
placeholder: '开始编写吧', // 占位符
theme: 'silver', // 主题 必须引入
skin_url: '/tinymce/skins/ui/oxide', // 主题路径
icons: 'custom',  // 自定义图标名称
icons_url: '/tinymce/icons/icons.js', // 自定义图标路径
language_url: '/tinymce/langs/zh_CN.js', // 中文化 默认为英文
language: 'zh_CN', // 设置富文本编辑器语言
content_css: `/tinymce/skins/content/default`, // 富文本编辑器内容区域样式
content_style: 'body, p{font-size: 12px}', // 为内容区编辑自定义css样式
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 26px 36px 48px 56px', // 工具栏自定义字体大小选项
font_formats: "微软雅黑='微软雅黑'; 宋体='宋体'; 黑体='黑体'; 仿宋='仿宋'; 楷体='楷体'; 隶书='隶书'; 幼圆='幼圆'; 方正舒体='方正舒体'; 方正姚体='方正姚体'; 等线='等线'; 华文彩云='华文彩云'; 华文仿宋='华文仿宋'; 华文行楷='华文行楷'; 华文楷体='华文楷体'; 华文隶书='华文隶书'; Andale Mono=andale mono,times; Arial=arial; Arial Black=arial black;avant garde; Book Antiqua=book antiqua;palatino; Comic Sans MS=comic sans ms; Courier New=courier new;courier; Georgia=georgia; Helvetica=helvetica; Impact=impact;chicago; Symbol=symbol; Tahoma=tahoma;arial; sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms; Verdana=verdana;geneva; Webdings=webdings; Wingdings=wingdings", // 工具栏自定义字体选项
toolbar_sticky: true, // 粘性工具栏 默认false (在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部)
toolbar_mode: 'sliding', // sliding生效条件toolbar必须为字符串,且有'|'区分,不能为数组
plugins: ['autosave help textpattern lineheight'], // 插件配置
toolbar: 'fontselect styleselect fontsizeselect restoredraft undo redo | bold italic underline strikethrough subscript superscript removeformat forecolor backcolor lineheight align outdent indent help', // 工具栏配置
images_upload_handler: (blobInfo, success, failure) => {
  // 发送请求, 获取图片路径后, 将路径传给success
  success('http://pic.sc.chinaz.com/files/pic/pic9/202005/apic25209.jpg')
}, // 图片上传函数 需引入插件image
image_advtab: true, // 为上传图片窗口添加高级属性 需引入插件image
paste_data_images: true, // 粘贴data格式的图像 需引入插件paste 谷歌浏览器无法粘贴
paste_as_text: true, // 默认粘贴为文本 需引入插件paste 谷歌浏览器无法粘贴
templates: [{ title: '标题', description: '描述', content: '内容' }], // 内容模板 需引入插件templates
visual: false, // 颜色辅助
quickbars_selection_toolbar: 'bold italic underline strikethrough | link h2 h3 h4 blockquote', // 设置 快速选择 触发提供的工具栏 需引入插件  默认 'alignleft aligncenter alignright' 设置为false禁用
quickbars_insert_toolbar: 'quickimage quicktable', // 设置 快速插入 触发提供的工具栏 需引入插件quickbars 默认 quickimage quicktable 设置为false禁用
textpattern_patterns: [
  { start: '*', end: '*', format: 'italic' },
  { start: '**', end: '**', format: 'bold' },
  { start: '#', format: 'h1' },
  { start: '##', format: 'h2' },
  { start: '###', format: 'h3' },
  { start: '####', format: 'h4' },
  { start: '#####', format: 'h5' },
  { start: '######', format: 'h6' },
  { start: '1. ', cmd: 'InsertOrderedList' },
  { start: '* ', cmd: 'InsertUnorderedList' },
  { start: '- ', cmd: 'InsertUnorderedList' }
], // 快速排版  类似于markdown 需引入插件textpattern
init_instance_callback: editor => { // 初始化结束后执行, 里面实现双向数据绑定功能
  if (_this.value) {
    editor.setContent(_this.value)
  }
  _this.hasInit = true
  editor.on('Input undo redo Change execCommand SetContent', (e) => {
    _this.hasChange = true
    // editor.getContent({ format: ''text }) // 获取纯文本
    _this.$emit('change', editor.getContent())
  })
},
setup: (editor) => { // 初始化前执行
  // 监听鼠标按下事件
  editor.on('keydown', (e) => {
    if (e.keyCode === 9) {
      if (e.shiftKey) {
        editor.execCommand('Outdent')
      } else {
        editor.execCommand('Indent')
      }
      e.preventDefault()
      e.stopPropagation()
    }
  })
  // 注册自定义上传按钮
  editor.ui.registry.addButton('upload', {
    text: `<i class="el-icon-upload" style="font-size: 24px"></i>`,
    tooltip: '自定义上传',
    onAction: () => {
      _this.config.show = true
    }
  })
  // 注册获取内容按钮
  editor.ui.registry.addButton('submit', {
    text: `<i class="el-icon-position" style="font-size: 18px"></i>`,
    tooltip: '获取内容',
    onAction: () => {
      console.log(editor.getContent())
    }
  })
  // 注册清空内容按钮
  editor.ui.registry.addButton('empty', {
    text: `<i class="el-icon-close" style="font-size: 18px"></i>`,
    tooltip: '清空内容',
    onAction: () => {
      _this.content = ''
      editor.setContent('')
    }
  })}
}

四、封装成Vue组件

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/tinymce-all-in-one@4.9.3/tinymce.min.js'
const tinymceCDN = 'https://fastly.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/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 &nbsp; 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(`<img class="wscnph" src="${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>

六、menubar(菜单)配置

1、什么是菜单

在这里插入图片描述

如图一个编辑器的工具部分。
分为两个部分,上面的文件、编辑...菜单栏 部分。

2、启用和停用菜单栏

通过init配置项menubar来配置菜单栏是否启用的项目和显示的顺序。

tinymce.init({
  selector: '#textarea1',  // change this value according to your HTML
  //启用菜单栏并显示如下项 [文件 编辑 插入 格式 表格]
  menubar: 'file edit insert view format table',
})

tinymce.init({
  selector: '#textarea2',  // change this value according to your HTML
  //禁用菜单栏
  menubar: false,
})

3、配置菜单项

每个菜单在经过上面的配置后都会有一个默认的子菜单
如果想自己定义每个菜单的子菜单项需要通过menu配置。

  • title对应在menubar中对应的项。
  • items为在各个菜单总显示的功能的名称
  • |为分割符号会将菜单分割为几个部分
tinymce.init({
  selector: '#textarea1',  // change this value according to your HTML
  //启用菜单栏并显示如下项 [文件 编辑 插入 格式 表格]
  menubar: 'file edit insert view format table',
  // 配置每个菜单栏的子菜单项(如下是默认配置)
  menu: {
    file: {title: 'File', items: 'newdocument'},
    edit: {title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall'},
    insert: {title: 'Insert', items: 'link media | template hr'},
    view: {title: 'View', items: 'visualaid'},
    format: {title: 'Format', items: 'bold italic underline strikethrough superscript subscript | formats | removeformat'},
    table: {title: 'Table', items: 'inserttable tableprops deletetable | cell row column'},
  }
})

4、菜单栏相关补充

  • 菜单配置项指的是各个子菜单的项。
  • 所属插件为核心的项 为基本包里自带的功能,直接写在menuitems项里就可以,
  • 属于插件的项 需要 引入插件(plugins: '插件名') 然后在menuitems中配置。

七、toolbar(工具栏)配置

1、什么是工具栏

在这里插入图片描述

如图一个编辑器的工具部分。
分为两个部分,下面红框框住的为 工具栏 部分。

2、禁用或启用菜单栏

tinymce.init({
  selector: '#textarea1',  // change this value according to your HTML
  //启用工具栏并显示如下项 
  toolbar: 'undo redo | styleselect | bold italic | link image',
})

tinymce.init({
  selector: '#textarea2',  // change this value according to your HTML
  //禁用工具栏
  toolbar: false,
})

3、启用多个工具栏

/* 启用多个工具栏 */

tinymce.init({
  selector: '#textarea3',  // change this value according to your HTML
  // 启用多个工具栏
  toolbar: [
    'undo redo | styleselect | bold italic | link image',
    'alignleft aligncenter alignright',
  ]
})

tinymce.init({
  selector: '#textarea4',  // change this value according to your html
  // 工具栏1
  toolbar1: 'undo redo | styleselect | bold italic | link image',
  // 工具栏2
  toolbar2: 'alignleft aligncenter alignright',
})

4、自定义工具栏

上面在工具栏中插入的都是编辑器自带的工具。
这里讲一下 如何插入自定义的按钮

主要的流程是:

  • inittoolbar中注册自定义按钮
  • setup方法中使用editor.addButton()定义按钮。
tinymce.init({
  selector: '#textarea',
  // 注册button
  toolbar: 'mybutton',

  setup: function (editor) {
    // 定义按钮,
    editor.addButton('mybutton', {
      // 按钮,名
      text: 'My button',
      // 是否显示图标
      icon: false,
      //onclick事件
      onclick: function () {
        // 这里点击后会插入一句话
        editor.insertContent(' <b>It\'s my button!</b> ')
      }
    })
  }
})

上面是最基本的配置方法,

还有一些其他属性可以配置:

  • tooltip: 就是鼠标滑过时的提示文字
  • icon: 按钮的图标(这里指的是TinyMCE中自带的)
  • image: 如果希望直接配置图标(可以是URL或者path)
  • onclick: 点击事件
  • onpostrender: 触发按钮渲染的事件(用来在合适的时机禁用按钮)
  • cmd: 点击按钮时出发的编辑器事件(已经注册的)

更为深入的自定义按钮配置方法这里暂时不做说明。
可以参看TinyMCE Docs

5、toolbar相关补充

所属插件为核心的项为基本包里自带的功能,直接写在toolbar里就可以,
属于插件的项需要引入插件(plugins: '插件名')然后在toolbar中配置。

toolbar配置功能所需插件
restoredraft恢复上次的草稿无需引入插件
undo撤销无需引入插件
redo恢复无需引入插件
fontselect字体选择无需引入插件
styleselect格式选择无需引入插件
fontsizeselect字号选择无需引入插件
cut剪切无需引入插件
copy复制无需引入插件
selectall全选无需引入插件
bold粗体无需引入插件
italic斜体无需引入插件
underline下划线无需引入插件
strikethrough删除线无需引入插件
subscript下标无需引入插件
superscript上标无需引入插件
removeformat清除格式无需引入插件
forecolor文字颜色无需引入插件
backcolor背景颜色无需引入插件
align对齐方式无需引入插件
outdent增加缩进无需引入插件
indent减少缩进无需引入插件
paste粘贴paste
ltr文字方向从左向右directionality
rtl文字方向从右向左directionality
anchor锚点anchor
lineheight行高lineheight
bullist符号列表advlist,lists(advlist依赖于lists)
numlist数字列表advlist,lists(advlist依赖于lists)
insertdatetime插入时间/日期insertdatetime
table表格table
tabledelete删除表格table
tableprops表格属性table
tablerowprops行属性table
tablecellprops单元格属性table
tableinsertrowbefore在上方插入table
tableinsertrowafter在下方插入table
tabledeleterow删除行table
tableinsertcolbefore在左侧插入table
tableinsertcolafter在右侧插入table
tabledeletecol删除列table
hr水平分割线hr
nonbreaking插入不间断空格nonbreaking
pagebreak分页符pagebreak
template内容模板template
charmap特殊字符charmap
emoticons表情插件emoticons
image插入编辑图片image
quickimage快捷插入图片image
media插入/编辑媒体资源media
link超链接link
toc目录生成器toc
code编辑源码code
quickbars快速工具栏quickbars
codesample代码示例codesample
fullpage文档属性fullpage
print打印print
searchreplace查找替换searchreplace
preview预览preview
fullscreen全屏fullscreen
wordcount字数统计wordcount
visualblocks显示区块边框visualblocks
visualchars显示不可见字符visualchars
help帮助help

八、contextmenu(上下文菜单)配置

1、什么是上下文菜单

所谓上下文菜单,就是在编辑器内容出现的 右键关联菜单

1、菜单可根据不同的环境出现不同的内容:

  • 在当鼠标在链接上时是编辑链接,
  • 当鼠标在表格上时是编辑表格

2、其值为任何已注册的菜单项,允许使用“|”作为分隔。

  • 取值:String
  • 默认:link image imagetools table spellchecker

2、禁用或启用上下文菜单

a、启用
tinymce.init({
    selector: '#textarea1',
    contextmenu: "bold copy ",
});
b、禁用(如不行则特殊处理一下)

正常的话只需要把contextmenu给成false即可:

tinymce.init({
    selector: '#textarea1',
    contextmenu: false,
});

但是,有时候使用Tinymce时设置contextmenu为false没有效果,就需要我们特殊处理一下:

这里通过监听contextmenu事件来禁用Tinymce的默认右键菜单,并通过preventDefault()方法来阻止浏览器上下文菜单的弹出。完整的配置示例如下:

tinymce.init({
 selector: '#textarea1',
 plugins: 'contextmenu',
 contextmenu: false,
 setup: function (editor) {
     editor.on('contextmenu', function (e) {
         e.preventDefault();
     });
 }
});

九、tinymce插件

来源于Tinymce中文文档

plugin功能备注
advlist高级列表插件官方地址
anchor锚点插件官方地址
autolink自动链接插件官方地址
autoresize编辑器大小自适应官方地址
autosave自动存稿官方地址
bbcode官方地址
bbcode官方地址
charmap特殊字符插件官方地址
code编辑源码官方地址
codesample代码示例插件官方地址
directionality文字方向官方地址
emoticons表情插件官方地址
fullpage文档属性官方地址
fullscreen全屏官方地址
help帮助官方地址
hr水平分割线官方地址
image插入编辑图片官方地址
importcss引入css官方地址
media插入编辑媒体官方地址
insertdatetime插入当前日期时间官方地址
legacyoutput输出HTML4官方地址
link超链接官方地址
lists列表插件官方地址
nonbreaking插入不间断空格官方地址
noneditable不可编辑元素官方地址
pagebreak插入分页符官方地址
paste粘贴插件官方地址
preview预览官方地址
print打印官方地址
quickbars快速工具栏官方地址
save保存官方地址
searchreplace查找替换官方地址
spellchecker拼写检查官方地址
tabfocustab切入切出官方地址
table表格插件官方地址
template内容模板官方地址
textcolor文字颜色无需引入,已集成官方地址
textpattern快速排版官方地址
toc目录生成器官方地址
visualblocks显示块元素范围官方地址
visualchars显示不可见字符官方地址
wordcount字数统计官方地址

十、获取TinyMCE编辑器中的内容

有时候需要验证tinyMCE编辑器中的内容是否符合规范(不为空)
需要获取里面的内容。

1、如果当前页面只有一个编辑器:

  • 获取内容:tinyMCE.activeEditor.getContent()
  • 设置内容:tinyMCE.activeEditor.setContent(“需要设置的编辑器内容”)

2、如果当前页面有多个编辑器(下面的“[0]”表示第一个编辑器,以此类推):

  • 获取内容:tinyMCE.editors[0].getContent()
  • 设置内容:tinyMCE.editors[0].setContent(“需要设置的编辑器内容”)

3、获取不带HTML标记的纯文本内容:

var activeEditor = tinymce.activeEditor;
var editBody = activeEditor.getBody();
activeEditor.selection.select(editBody);
var text = activeEditor.selection.getContent( { ‘format’ : ‘text’ } );

十一、TinyMCE上传图片

1、返回的josn数据格式为

{"location":"http://localhost/images/00C01FA6364DFF9757D1CF446748A47852B2D475.jpg"}

2、样例:

tinymce.init({
            selector: '#file-picker',
            language: 'zh-Hans',//语言
            height: 600,//编辑器高度
            branding: false,//是否禁用“Powered by TinyMCE”
            plugins: [
                'powerpaste table advlist autolink lists link charmap print preview hr anchor pagebreak',
                'searchreplace wordcount visualblocks visualchars code fullscreen',
                'insertdatetime nonbreaking save table contextmenu directionality',
                'emoticons textcolor colorpicker textpattern image code codesample toc pagebreak'
            ],
            toolbar1: 'code undo redo formatselect fontselect fontsizeselect insert styleselect  bold italic underline alignleft aligncenter alignright alignjustify forecolor backcolor newdocument table insert bullist numlist outdent indent link image rotateleft rotateright flipv fliph print preview  emoticons  codesample  pagebreak  toc  fullscreen superscript subscript ltr rtl hr',
            //toolbar2: 'print preview  forecolor backcolor emoticons  codesample  pagebreak  toc  fullscreen',
            image_advtab: true,
            //images_upload_url
            paste_data_images: true,
            menubar: true,//禁用标题栏
            automatic_uploads: true,
            media_live_embeds: true,//查看上传的视频
            //图片选择上传
            images_upload_handler: function (blobInfo, success, failure) {

                var file = blobInfo.blob();//转化为易于理解的file对象
                var isLt10M = file.size / 1024 / 1024 < 4;
                if (!isLt10M) {
                    failure('上传图片大小不能超过5MB哦!');
                    return;
                }
                var xhr, formData;
                xhr = new XMLHttpRequest();
                xhr.withCredentials = false;
                xhr.open("POST", "http://localhost/Handler.ashx?API=uploadImg&UserName=" + document.getElementById("UserName").value);
                formData = new FormData();
                formData.append('file', file, file.name);
                console.log(formData);
                xhr.onload = function (e) {
                    var json;

                    if (xhr.status != 200) {
                        failure('HTTP Error: ' + xhr.status);
                        return;
                    }
                    json = JSON.parse(this.responseText);

                    if (!json || typeof json.location != 'string') {
                        failure('Invalid JSON: ' + xhr.responseText);
                        return;
                    }
                    success(json.location);
                };
                xhr.send(formData);
            }
        });

十二、业务逻辑实现

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

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

- a、在组件中添加props

在这里插入图片描述

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

在这里插入图片描述

- c、使用组件时传参

在这里插入图片描述

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

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

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

- a、给Tinymce组件绑定**key** 值

在这里插入图片描述

- b、在使用Tinymce组件的页面的 mouted 去实例化

在这里插入图片描述

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

- a、工具栏自定义按钮
tinymce.init({
  selector: '#textarea',
  // 注册button
  toolbar: 'customButton',

  setup: function (editor) {
  	// 绑定鼠标点击事件
    editor.on('click', function (e) {
       // 获取鼠标点击的位置坐标(如果不需要获取鼠标单击的当前行则不用绑定click时事件)
       _this.clickGetRng = editor.selection.getRng()
    })
    // 定义按钮,
    editor.addButton('customButton', {
      tooltip: '打开弹窗按钮',
      icon: 'indent',
      onclick: function () {
        // 在这里执行你想要的操作
         _this.$emit('showModal')
      }
    })
  }
})

这个代码里,自定义了一个按钮,告诉父组件去打开一个弹窗,然后在父组件去进行自己业务逻辑实现

- b、父组件调用showModal并打开弹窗

在这里插入图片描述
打开弹窗具体逻辑就不写了

- c、弹窗内容双击之后,调用Tinymce函数来进行内容插入

在这里插入图片描述
在这里插入图片描述

- d、Tinymce中执行内容插入逻辑

在这里插入图片描述

5、实现首行缩进2字符

在这里插入图片描述

工具栏的增加缩进点击默认是给当前行配置一个padding-left:40px的样式,需要把它改成text-indent: 2em

  • 首先,我们要先在工具栏 自定义一个缩进的按钮
tinymce.init({
  selector: '#textarea',
  // 注册button
  toolbar: 'mybutton',

  setup: function (editor) {
  	// 绑定鼠标点击事件
    editor.on('click', function (e) {
       // 获取鼠标点击的位置坐标(如果不需要获取鼠标单击的当前行则不用绑定click时事件)
       _this.clickGetRng = editor.selection.getRng()
    })
    // 定义按钮,
    editor.addButton('customIndentButton', {
      tooltip: '增加缩进',
      icon: 'indent',
      onclick: function () {
        // 自定义的点击事件处理函数
         _this.indentCurrentLine(editor)
      }
    })
  }
})
  • 然后 绑定在工具栏
tinymce.init({
	toolbar: ['customIndentButton']
})
  • 根据业务需求来自定义点击该按钮之后的回调函数
- a、获取鼠标单击的当前行,并为当前行添加样式text-indent: 2em;
indentCurrentLine(editor) {
  // 获取鼠标单击的当前行,并为当前行添加样式text-indent: 2em;
  var rng = this.clickGetRng
  // 获取包含单击位置的最近的DOM元素
  var parentElement = editor.dom.getParent(rng.startContainer, editor.dom.isBlock)
  console.log(rng, parentElement)
  // 为该元素添加text-indent: 2em;样式
  const rm = editor.dom.getStyle(parentElement, 'text-indent')
  const em1 = rm.substring(0, rm.length - 2) - 0 + 2 + 'em'
  editor.dom.setStyle(parentElement, 'text-indent', em1)
},
- b、选中一段文本,并将其变成一个段落,并首行缩进2个字符;
indentCurrentLine(editor) {
  // 获取当前选中的文本行
  var selection = editor.selection.getContent({ format: 'text' })
  var selectedLines = selection.split('\n')
  console.log(selection, selectedLines)
  // 添加样式text-indent: 2em到每一行
  var indentedLines = selectedLines.map(function (line) {
    return '<p style="text-indent: 2em;">' + line + '</p>'
  })
  // 替换选中的文本为添加了样式的文本行
  editor.selection.setContent(indentedLines.join(''))
},

8、关于tinymce内容改变后,编辑器光标位置重置的问题

流程:通过监听props的value,调用tinymcesetContent(),这一步会将内容的光标重置,回到开始位置。

- a、原因

这个问题出现的原因是在vue场景下才有的:传入的数据会进行一次格式整理,并触发Change更新组件外部的数据,然后会再触发一次数据传入。

比如,首次传入组件的数据是富文本正文,组件再传入tinymce后会被格式化为<p>富文本正文</p>,并触发tinymce的change事件,接下来组件emit通知更新外部数据来源,然后传入组件的原始数据会被改为<p>富文本正文</p>。数据再次传入组件后,两份数据没有变化才停止了更新至tinymce。

正是这个过程光标会重置。

- b、解决思路
watch: {
    value(newVal, oldVal) {
      // console.log(this.hasChange, this.hasInit);
      if (!this.hasChange && this.hasInit) {
        this.$nextTick(() => {
          window.tinymce && window.tinymce.get(this.tinymceId).setContent(newVal || '')
          // 让光标定位到未尾
          window.tinymce.activeEditor.selection.select(window.tinymce.activeEditor.getBody(), true)
          window.tinymce.activeEditor.selection.collapse(false)
        })
      }
},

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

tinymce.init({
  selector: '#myTextarea',
  plugins: 'paste',
  paste_as_text: true,  // 粘贴为纯文本
  paste_block_drop: true,  // 阻止粘贴带有格式的文本
  // 添加自定义选项:“粘贴”、“粘贴纯文本” 和 “全选” 选项
  contextmenu: 'paste | paste_text | selectall',  
  setup: function (editor) {
    editor.ui.registry.addMenuItem('paste_text', {
      text: '粘贴纯文本',
      onAction: function () {
        editor.execCommand('mceInsertContent', false, editor.clipboard.pasteText());
      }
    });
  }
});
  • 27
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值