Tinymce富文本编辑器实践经验总结

Vue項目集成Tinymce

组件功能支持情况

  • 获取内容
  • 设置内容
  • 插入内容
  • 光标放最后
  • 销毁编辑器
  • 内容双向绑定
  • 自定义图片上传
  • 文章末尾有Tinymce源码链接,推荐直接下载本地部署使用(npm方法使用需要秘钥,而且限制每天的打开次数),下载后配合示例代码嘎嘎好用,已支持简体中文)

更完整的案例见:https://blog.csdn.net/m0_62332650/article/details/139579289?spm=1001.2014.3001.5501

vue组件方式使用示例

属性介绍

  • content:编辑器内容;可利用sync指令实现双向绑定
  • show:控制编辑器显示隐藏
        <Tinymce
          ref="Tinymce"
          :content.sync="content"
          :show="show"
        />


组件完整代码

<template>
  <textarea
    :id="tinymceID"
    :class="hasTinymceInstance ? 'show' : 'hide'"
  ></textarea>
</template>

<script>
  import { getFpToekn } from '@/api/fp'
  import { fpUrl } from '@/config'
  import axios from 'axios'
  export default {
    props: {
      content: {
        type: null,
        default: '',
      },
      show: {
        type: Boolean,
        default: () => false,
      },
    },
    data() {
      return {
        tinymceID: 'tinymceEditor',
        hasTinymceInstance: undefined, // 标记是否存在编辑器实例
      }
    },
    watch: {
      show: {
        handler(newData, oldData) {
          // console.log('show', 'newData:', newData, ',', 'oldData:', oldData)
          // console.log('show content', this.content)
          // console.log('this.hasTinymceInstance', this.hasTinymceInstance)
          switch (newData) {
            case true:
              this.showTinymce().then(() => {
                this.setContent(this.content || '')
              })
              break
            case false:
              this.hideTinymce()
              break
            default:
              break
          }
        },
        immediate: true,
      },
      content(newData, oldData) {
        // console.log('content', 'newData:', newData, ',', 'oldData:', oldData)
        // console.log('this.hasTinymceInstance', this.hasTinymceInstance)
        if (this.hasTinymceInstance) {
          this.showTinymce().then(() => {
            this.setContent(newData || '')
          })
        }
      },
    },
    mounted() {},
    beforeDestroy() {
      console.log('beforeDestroy')
      if (this.hasTinymceInstance) {
        this.destroyTinymce()
      }
    },
    methods: {
      // 显示
      async showTinymce() {
        return new Promise((resolve) => {
          if (this.hasTinymceInstance) {
            window.tinyMCE.editors[this.tinymceID].show()
            resolve()
          } else {
            this.initTinymceEditor(resolve)
          }
        })
      },
      // 隐藏
      hideTinymce() {
        this.hasTinymceInstance && window.tinyMCE.editors[this.tinymceID].hide()
      },
      // 创建
      initTinymceEditor(resolve) {
        const init = () => {
          window.tinymce.init({
            selector: `#${this.tinymceID}`,
            language: 'zh_CN',
            // 注册插件
            plugins:
              ' preview searchreplace autolink directionality visualblocks visualchars fullscreen image link template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount imagetools textpattern help emoticons autosave bdmap indent2em autoresize formatpainter axupimgs',
            // 工具栏配置。| 为分组,单行超出会折叠;不加|不会折叠,超出自动换行
            toolbar:
              'code undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | \
      styleselect formatselect fontselect fontsizeselect | bullist numlist | blockquote subscript superscript removeformat | \
      table image charmap emoticons hr pagebreak insertdatetime  preview | fullscreen | bdmap indent2em lineheight formatpainter axupimgs',
            /*content_css: [ //可设置编辑区内容展示的css,谨慎使用
              '/static/reset.css',
              '/static/ax.css',
              '/static/css.css',
          ],*/
            // link_list: [
            //   { title: '预置链接1', value: 'http://www.tinymce.com' },
            //   { title: '预置链接2', value: 'http://tinymce.ax-z.cn' }
            // ],
            // image_list: [
            //   { title: '预置图片1', value: 'https://www.tiny.cloud/images/glyph-tinymce@2x.png' },
            //   { title: '预置图片2', value: 'https://www.baidu.com/img/bd_logo1.png' }
            // ],
            // image_class_list: [
            //   { title: 'None', value: '' },
            //   { title: 'Some class', value: 'class-name' }
            // ],
            toolbar_sticky: true,
            toolbar_drawer: 'floating',
            min_height: window.screen.height - 380,
            max_height: window.screen.height - 380,
            fontsize_formats:
              '12px 14px 15px 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;Times New Roman=times new roman,times,serif',
            importcss_append: true,
            // images_upload_url: '/demo/upimg.php', // 指定一个接受上传文件的后端处理程序地址
            // 自定义图片上传逻辑
            images_upload_handler: async (blobInfo, succFun, failFun) => {
              try {
                let file = blobInfo.blob()
                let formData = new FormData()
                const res = await getFpToekn({
                  mimeLimit: ['image/*'],
                  fsizeLimit: 10 * 1024 * 1024,
                })
                formData.append('Authorization', res.data['token'])
                formData.append('file', file)
                formData.append('fpfile', file)
                const response = await axios.post(fpUrl, formData, {
                  withCredentials: false,
                })
                succFun(response.data.url)
              } catch (e) {
                console.error(e)
                failFun('图片上传失败,请重试')
              }
            },
            // file_picker_types: 'file image media', // 指定允许上传的类型
            //自定义文件选择器的回调内容(不配置此配置时不会显示上传按钮)
            // file_picker_callback: function (callback, value, meta) {
            //   console.log('file_picker_callback', callback, '---',  value, '---', meta);
            //   if (meta.filetype === 'file') {
            //     callback('https://www.baidu.com/img/bd_logo1.png', { text: 'My text' });
            //   }
            //   if (meta.filetype === 'image') {
            //     callback('https://www.baidu.com/img/bd_logo1.png', { alt: 'My alt text' });
            //   }
            //   if (meta.filetype === 'media') {
            //     callback('movie.mp4', { source2: 'alt.ogg', poster: 'https://www.baidu.com/img/bd_logo1.png' });
            //   }
            // },
            autosave_ask_before_unload: true, // 当关闭或跳转URL时,弹出提示框提醒用户仍未保存变更内容。默认开启提示。
            autosave_interval: '30s', // 自动存稿的时间间隔
            autosave_restore_when_empty: false, // 当编辑器初始化时内容区为空时,Tinymce是否应自动还原存储在本地存储中的草稿。
            statusbar: true, // 隐藏状态栏 默认true显示,false隐藏
            setup: (editor) => {
              console.log('ID为: ' + editor.id + ' 的编辑器即将初始化.')
              editor.on('blur', () => {
                // console.log('blur', editor.getContent())
                this.$emit('update:content', editor.getContent())
              })
            },
            init_instance_callback: (editor) => {
              console.log('ID为: ' + editor.id + ' 的编辑器已初始化完成.')
              this.hasTinymceInstance = true
              resolve()
            },
          })
        }
        if (!window.tinymce) {
          const script = document.createElement('script')
          script.src = './static/tinymce/js/tinymce/tinymce.min.js'
          script.onload = init
          document.body.appendChild(script)
        } else {
          init()
        }
      },
      // 获取内容
      getContent() {
        const cnt = window.tinyMCE.editors[this.tinymceID].getContent()
        console.log(cnt)
      },
      // 设置内容
      setContent(cnt) {
        if (this.hasTinymceInstance) {
          // console.log('setContent', cnt, window.tinyMCE.editors[this.tinymceID])
          window.tinyMCE.editors[this.tinymceID].show() // 需要先显示编辑器才能设置内容
          window.tinyMCE.editors[this.tinymceID].setContent(cnt)
          // this.goEnd() // 执行setContent后,光标默认在最左侧,手动设置光标放最后
        }
      },
      // 插入内容
      insertContent(cnt) {
        this.hasTinymceInstance &&
          window.tinyMCE.editors[this.tinymceID].insertContent(cnt)
        console.log(cnt)
      },
      // 光标放最后
      goEnd() {
        const editor = window.tinyMCE.editors[this.tinymceID]
        editor.execCommand('selectAll')
        editor.selection.getRng().collapse(false)
        editor.focus()
      },
      // 销毁编辑器
      destroyTinymce() {
        if (this.hasTinymceInstance) {
          console.log('销毁编辑器')
          window.tinyMCE.editors[this.tinymceID].off()
          window.tinyMCE.editors[this.tinymceID].destroy()
          this.hasTinymceInstance = false
        }
      },
    },
  }
</script>

<style scoped lang="scss">
  .show {
    visibility: visible;
    width: 1000px;
    height: 400px;
  }
  .hide {
    visibility: hidden;
    width: 1000px;
    height: 400px;
  }
</style>


实现[选择快捷工具栏]和[插入快捷工具栏]

在编辑器内容区,光标插入(回车)或选择时,在光标位置出现的快捷工具栏。

可使用任何在工具栏(toolbar)中可用的项目。

使用该选项必须先启用quickbars插件。

tinymce.init({
    selector: '#textarea1',
    plugins: 'quickbars',
    quickbars_insert_toolbar: 'quickimage quicktable',
    quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote',
});

实现格式刷功能

插件源码地址:https://gitee.com/wgmgitee/tinymce5/tree/master/external_plugins/formatpainter
配置:
在这里插入图片描述
把文件放到下图所示的目录里:
在这里插入图片描述
Tinymce初始化时会自动加载插件。

实现粘贴时清除格式

tinymce.init({
  // 其他配置项...
  plugins: "paste",
  paste_as_text: true
});

实现粘贴时清除格式和标签

tinymce.init({
  // 其他配置项...
  plugins: "paste",
  paste_as_text: true, // 清除格式
  paste_preprocess: function (plugin, args) {
              args.content = `<div>${args.content.replace(
                /<[^>]+>/g,
                ''
              )}</div>` // 移除所有 HTML 标签
            },
});

实现输入文本时默认使用div标签

TinyMCE 默认情况下会将输入的文本包装在p标签中,这是因为它的默认配置是使用p作为块级元素。要使 TinyMCE 输入文本时默认使用div标签,可以通过以下方法实现:

tinymce.init({
  // 其他配置项...
  forced_root_block: 'div'
});

在菜单栏自定义一个单级菜单,点击直接执行“清除格式”

tinymce.init({
  selector: 'textarea',
  menubar: 'ycustommenu',
  setup: function(editor) {
    editor.addMenu('mycustommenu', {
      title: '清除格式',
      onclick: function() {
        editor.execCommand('removeFormat');
      }
    });
  }
});

在工具栏自定义下拉选项功能

tinymce.init({
  selector: 'textarea',
  toolbar: 'customStyles', // 将菜单项添加到工具栏上显示
  setup: (editor) => {
    // 自定义菜单项
    editor.ui.registry.addMenuButton('customStyles', {
      text: '预设样式',
      fetch: function (callback) {
        const items = [
          {
            type: 'menuitem',
            text: '段落标题',
            onAction: function () {
              editor.selection.setContent(
                '<p style="margin:0; padding:0;color: #1B1A1A; line-height: 2; font-size: 16px; font-weight: 600">' +
                  editor.selection.getContent({ format: 'text' }) +
                  '</p>'
              )
            },
          },
          {
            type: 'menuitem',
            text: '正文内容',
            onAction: function () {
              editor.selection.setContent(
                '<p style="margin:0; padding:0;color: #696B70; line-height: 1.5; font-size: 15px; font-weight: 400">' +
                  editor.selection.getContent({ format: 'text' }) +
                  '</p>'
              )
            },
          },
          {
            type: 'menuitem',
            text: '强调',
            onAction: function () {
              editor.selection.setContent(
                '<p style="margin:0; padding:0;color: #FF9500; line-height: 1.5; font-size: 15px; font-weight: 400">' +
                  editor.selection.getContent({ format: 'text' }) +
                  '</p>'
              )
            },
          },
          {
            type: 'menuitem',
            text: '超强调',
            onAction: function () {
              editor.selection.setContent(
                '<p style="margin:0; padding:0;color: #EB4B4B; line-height: 1.5; font-size: 15px; font-weight: 400">' +
                  editor.selection.getContent({ format: 'text' }) +
                  '</p>'
              )
            },
          },
          {
            type: 'menuitem',
            text: '链接',
            onAction: function () {
              editor.selection.setContent(
                '<a style="color: #0075FF; line-height: 1.5; font-size: 15px; font-weight: 400; text-decoration: underline; cursor: pointer">' +
                  editor.selection.getContent() +
                  '</a>'
              )
            },
          },
        ]
        callback(items)
      },
    })
  },
})

在工具栏自定义普通按钮功能

tinymce.init({
  selector: 'textarea',
  toolbar: 'customRemoveFormat', // 将菜单项添加到工具栏上显示
  setup: (editor) => {
    editor.ui.registry.addButton('customRemoveFormat', {
      text: '清除格式',
      onAction: () => {
        editor.execCommand('removeFormat', false)
      },
    })
  },
})

execCommand 执行命令

可用命令可在此查看:https://www.tiny.cloud/docs/tinymce/latest/editor-command-identifiers/

Tinymce 自定义插件

/*
  Note: We have included the plugin in the same JavaScript file as the TinyMCE
  instance for display purposes only. Tiny recommends not maintaining the plugin
  with the TinyMCE instance and using the `external_plugins` option.
*/
tinymce.PluginManager.add('example', (editor, url) => {
  const openDialog = () => editor.windowManager.open({
    title: 'Example plugin',
    body: {
      type: 'panel',
      items: [
        {
          type: 'input',
          name: 'title',
          label: 'Title'
        }
      ]
    },
    buttons: [
      {
        type: 'cancel',
        text: 'Close'
      },
      {
        type: 'submit',
        text: 'Save',
        buttonType: 'primary'
      }
    ],
    onSubmit: (api) => {
      const data = api.getData();
      /* Insert content when the window form is submitted */
      editor.insertContent('Title: ' + data.title);
      api.close();
    }
  });
  /* Add a button that opens a window */
  editor.ui.registry.addButton('example', {
    text: 'My button',
    onAction: () => {
      /* Open window */
      openDialog();
    }
  });
  /* Adds a menu item, which can then be included in any menu via the menu/menubar configuration */
  editor.ui.registry.addMenuItem('example', {
    text: 'Example plugin',
    onAction: () => {
      /* Open window */
      openDialog();
    }
  });
  /* Return the metadata for the help plugin */
  return {
    getMetadata: () => ({
      name: 'Example plugin',
      url: 'http://exampleplugindocsurl.com'
    })
  };
});

/*
  The following is an example of how to use the new plugin and the new
  toolbar button.
*/
tinymce.init({
  selector: 'textarea#custom-plugin',
  plugins: 'example',
  toolbar: 'example'
});

Tinymce 注册并应用自定义格式

注意:不能在setup里注册,要在init_instance_callback里注册

tinymce.init({
  init_instance_callback: function (editor) {
    editor.formatter.register('mycustomformat', {
          block: 'h1', // 指定块级元素的类型,例如 "div"。block和inline二选一,不能同时配置
          inline: 'span', // 指定内联元素的类型,例如 "span"。
          styles: { color: '#ff0000', margin: 0, padding: 0 }, // 指定要应用到格式化文本的样式属性和值的对象。
          classes: 'my-custom-format', //  指定要应用到格式化文本的 CSS 类。
          attributes: { // 指定要应用到格式化文本的 HTML 属性的对象
            'data-custom': 'example',
            'title': 'Custom Format'
          }
        });
  }
});

使用

editor.formatter.apply('mycustomformat');

Tinymce 自定义样式的5种方法

  1. 注册自定义格式,参考上面
  2. formats
    该选项可用于覆盖编辑器默认格式,添加自定义格式
    配置:
block_formats: '标题1=h1;标题2=h2;标题3=h3;标题4=h4;标题5=h5;标题6=h6;段落=p;Div=div;自定义=test', // formatselect 的配置
      formats:{
        h1: { block: 'h1', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        h2: { block: 'h2', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        h3: { block: 'h3', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        h4: { block: 'h4', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        h5: { block: 'h5', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        h6: { block: 'h6', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        p: { block: 'p', styles: { margin: '0px', padding: '0px', textAlign: 'justify' }},
        test: { inline: 'span', classes: 'class1' }
      }, // 该选项可用于覆盖编辑器默认格式,添加自定义格式

效果:
在这里插入图片描述
源码:
在这里插入图片描述

  1. setContent
editor.selection.setContent(
                          '<p style="margin:0; padding:0;">' +
                            '<span style="color: #1B1A1A; line-height: 2; font-size: 16px; font-weight: 600">' +
                            editor.selection.getContent({ format: 'text' }) +
                            '</span>' +
                            '</p>'
                        )
  1. execCommand命令
  2. setAttribute
const selectedNode = editor.selection.getNode()
selectedNode.setAttribute(
                            'style',
                            'color: #0075FF; line-height: 1.5; font-size: 15px; font-weight: 400; text-decoration: underline; cursor: pointer'
                          )

editor.formatter 对象

文档:https://www.tiny.cloud/docs/tinymce/latest/apis/tinymce.formatter/

TinyMCE 5 中的 editor.formatter 对象具有以下方法:

  • match(name: string): boolean - 检查指定的格式是否已应用于当前选区的内容。
例:执行过了editor.execCommand('bold')
执行editor.formatter.match('bold')会返回true
  • toggle(name: string, state?: boolean): void - 切换指定格式的状态。如果 state 参数为 true,则应用格式;如果为 false,则移除格式。
  • apply(name: string, vars?: Record<string, any>): void - 应用指定的格式,并可以传递额外的参数。
  • remove(name: string, vars?: Record<string, any>): void - 移除指定的格式,并可以传递额外的参数。

开启插件支持粘贴上传图片

配置 paste_data_images: true,images_upload_handler自定义上传逻辑

Tinymce 自定义粘贴图片上传

tinymce.init({
          setup: (editor) => {
            editor.on('paste', async (event) => {
              try {
                const clipboardItems = event.clipboardData.items
                for (const clipboardItem of clipboardItems) {
                  const kind = clipboardItem.kind
                  const type = clipboardItem.type
                  if (kind === 'file' && type.startsWith('image/')) {
                    const file = clipboardItem.getAsFile()
                    if (file) {
                      const formData = new FormData()
                      formData.append('image', file)
                      const res = await getFpToekn({
                        mimeLimit: ['image/*'],
                        fsizeLimit: 10 * 1024 * 1024,
                      })
                      formData.append('Authorization', res.data['token'])
                      formData.append('file', file)
                      formData.append('fpfile', file)
                      this.$emit('update:loading', true)
                      const response = await axios.post(fpUrl, formData, {
                        withCredentials: false,
                      })
                      const imgSrc = response.data.url
                      this.insertContent(`<img src="${imgSrc}" alt="" />`)
                    }
                  }
                }
              } catch (error) {
                console.error('Clipboard error:', error)
              }
            })
          },
        });

自定义粘贴逻辑【支持粘贴纯文本、图片(自动上传)、富文本】

https://blog.csdn.net/m0_62332650/article/details/141197149

源码

https://gitee.com/wgmgitee/tinymce5

常见问题

1,与ElementUI的Dialog组件搭配使用时,会出现工具栏层级样式错乱问题,解决办法:把z-index设置得更大
在这里插入图片描述
2. 自定义插件报错
原因:使用了箭头函数
解决:用function关键字声明函数代替箭头函数

  1. 使用了自定义样式再使用格式刷会报错
    原码:
editor.formatter.register('custom-h2', {
                block: 'h2',
                styles: {
                  margin: 0,
                  padding: 0,
                  color: '#1B1A1A',
                  lineHeight: 2,
                  fontSize: 16,
                  fontWeight: 600,
                },
              })

原因:没有margin、padding会报错;margin、padding单位为数字也报错
解决:加上margin、padding,单位为字符串

editor.formatter.register('custom-h2', {
          block: 'h2',
          styles: {
            margin: '0',
            padding: '0',
            color: '#1B1A1A',
            lineHeight: 2,
            fontSize: 16,
            fontWeight: 600,
          },
        })
  1. 格式刷不能刷自定义样式
    原因:自定义样式定义在块级标签里,格式刷默认只能刷行内样式

相关资料

中文文档:http://tinymce.ax-z.cn/
官方文档:https://www.tiny.cloud/

你可以通过以下步骤来使用tinymce富文本编辑器: 1. 引入tinymce资源文件: 首先,你需要在你的项目中引入tinymce的资源文件。你可以从官方网站下载或者使用CDN链接。在你的HTML文件中添加以下代码来引入tinymce的资源文件: ```html <script src="https://cdn.tiny.cloud/1/your-api-key/tinymce/5/tinymce.min.js"></script> ``` 请将"your-api-key"替换为你自己的API密钥。 2. 创建一个textarea元素: 在你的HTML文件中,创建一个textarea元素来作为tinymce编辑器的容器。例如: ```html <textarea id="myTextarea"></textarea> ``` 3. 初始化tinymce: 使用以下代码来初始化tinymce编辑器: ```javascript tinymce.init({ selector: '#myTextarea' }); ``` 这将把id为"myTextarea"的textarea元素转换成一个tinymce编辑器。 4. 配置编辑器选项: 你可以根据需要配置编辑器的选项,比如设置编辑器的语言、工具栏按钮、插件等。你可以在初始化tinymce时传入一个选项对象来配置编辑器。例如: ```javascript tinymce.init({ selector: '#myTextarea', language: 'zh_CN', plugins: 'link image code', toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | code' }); ``` 这个例子中,我们设置了编辑器的语言为中文,加载了链接、图片和代码插件,并且设置了工具栏按钮。 5. 更多功能和配置: tinymce有很多其他的功能和配置,你可以参考官方文档来了解更多详细信息。官方文档提供了丰富的示例和教程,帮助你快速上手和使用tinymce富文本编辑器。你可以在官方网站的文档页面找到更多信息。 希望这些步骤可以帮助到你使用tinymce富文本编辑器。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [在 Vue 项目中引入 tinymce 富文本编辑器的完整代码](https://download.csdn.net/download/weixin_38688855/12760354)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [tinymce富文本编辑器的使用](https://blog.csdn.net/weixin_44867717/article/details/128167874)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值