• vue3富文本编辑器的二次封装开发-Tinymce


    欢迎点击领取 -《前端开发面试题进阶秘籍》:前端登顶之巅-最全面的前端知识点梳理总结

    专享链接

    简介

    1、安装:pnpm add tinymce @tinymce/tinymce-vue ===> Vue3 + tinymce + @tinymce/tinymce-vue
    2、功能实现图片上传、基金卡片插入、收益卡片插入、源代码复用、最大长度限制、自定义表情包插入、文本内容输入、预览等功能

    在这里插入图片描述

    代码展示

    在components文件下创建TinymceEditor.vue文件作为公共组件

    <template>
      <div>
        <Editor ref="EditorRefs" v-model="content" :init="myTinyInit" />
        <div class="editor_footer">
          <span v-if="wordlimit">
            <span>{{ wordLenght }}</span>
            <span> / </span>
            <span>{{ wordlimit.max }}</span> 字符
          </span>
        </div>
        <el-dialog title="自定义表情包" v-model="dialogVisible" width="45%">
          <div class="emoji">
            <div class="emoji-item" v-for="item in 40" :key="item">
              <img :src="`/src/assets/emoji/${item}.webp`" alt="" @click="chooseEmoji(item)" />
            </div>
          </div>
        </el-dialog>
        <button @click="handlePreview">预览</button>
      </div>
    </template>
    
    <script lang="ts" setup>
    import './wordlimit' // 限制字符文件
    import tinymce from 'tinymce/tinymce'
    import Editor from '@tinymce/tinymce-vue'
    import 'tinymce/icons/default/icons'
    import 'tinymce/themes/silver'
    import 'tinymce/models/dom/model'
    import 'tinymce/plugins/table'
    import 'tinymce/plugins/lists'
    import 'tinymce/plugins/link'
    import 'tinymce/plugins/help'
    import 'tinymce/plugins/wordcount'
    import 'tinymce/plugins/code'
    import 'tinymce/plugins/preview'
    import 'tinymce/plugins/visualblocks'
    import 'tinymce/plugins/visualchars'
    import 'tinymce/plugins/fullscreen'
    import '/public/tinymce/plugins/image/index.js'
    
    import { sumLetter } from '@/utils/utilTool'
    import { computed, onMounted, reactive, ref, watch } from 'vue'
    
    const props = withDefaults(
      defineProps<{
        modelValue?: string
        plugins?: string
        toolbar?: string
        wordlimit?: any
      }>(),
      {
        plugins: 'image code wordcount wordlimit preview', // 默认开启工具库
        toolbar: 'image emoji fund—icon income-icon code' // 富文本编辑器工具
      }
    )
    
    const emit = defineEmits(['input'])
    
    const wordLenght = ref<number | string>(0)
    
    const content = ref<string>('')
    
    const EditorRefs = ref<any>()
    
    const dialogVisible = ref<boolean>(false)
    
    const myTinyInit = reactive({
      width: '100%',
      height: 600, // 默认高度
      statusbar: false,
      language_url: '/tinymce/langs/zh_CN.js', // 配置汉化-> 需下载对应汉化包引入
      language: 'zh_CN', // 语言标识
      branding: false, // 不显示右下角logo
      auto_update: false, // 不进行自动更新
      resize: true, // 可以调整大小
      menubar: false, // 关闭顶部菜单
      skin_url: '/tinymce/skins/ui/oxide', // 手动引入CSS
      content_css: '/tinymce/skins/content/default/content.css', // 手动引入CSS
      toolbar_mode: 'wrap',
      plugins: props?.plugins, // 插件
      toolbar: props?.toolbar, // 功能按钮
      wordlimit: props?.wordlimit, // 字数限制
      image_caption: false,
      paste_data_images: true,
    
      //粘贴图片后,自动上传
      urlconverter_callback: function (url, node, on_save, name) {
        return url
      },
    
      images_upload_handler: (blobInfo) =>
        new Promise((resolve, reject) => {
          console.log(blobInfo.blob())
          const formData = new FormData()
          formData.append('file', blobInfo.blob(), blobInfo.filename())
          resolve('https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/image-20230512090059968.png')
          // axios
          //   .post(`/api/backend/upload`, formData, {
          //     headers: {
          //       'Content-Type': 'multipart/form-data',
          //       Authorization: 'Bearer ' + store.state.user.accessToken,
          //     },
          //   })
          //   .then((res) => {
          //     if (res.data.code === 1) {
          //       resolve(`/image_manipulation${res.data.data.filePath}`)
          //     } else {
          //       ElNotification.warning(res.data.msg)
          //     }
          //   })
          //   .catch((error) => {
          //     reject(error)
          //   })
        }),
    
      setup: (editor) => { // 自定义图标内容及触发点击事件等功能
        editor.ui.registry.addIcon(
          'fund—icon',
          ''
        )
        editor.ui.registry.addIcon(
          'income-icon',
          ''
        )
    
        editor.ui.registry.addButton('emoji', {
          icon: 'emoji',
          tooltip: '自定义表情包',
          onAction: () => {
            dialogVisible.value = true
          }
        })
    
        editor.ui.registry.addButton('fund—icon', {
          icon: 'fund—icon',
          tooltip: '基金',
          onAction: () => {
            editor.insertContent('Hello')
          }
        })
    
        editor.ui.registry.addButton('income-icon', {
          icon: 'income-icon',
          tooltip: '晒收益',
          onAction: () => {
            editor.insertContent('Hello')
          }
        })
      },
    
      init_instance_callback: (editor: any) => {
        editor.on('input', () => getEditorWordLen())
      }
    })
    
    const initContent = computed(() => {
      return props.modelValue
    })
    
    // 选择自定义表情包
    const chooseEmoji = (item) => {
      const editor = EditorRefs.value.getEditor()
      const range = editor.selection.getRng()
      const imgNode = editor.getDoc().createElement('img')
      imgNode.width = 32
      imgNode.height = 32
      imgNode.style = 'vertical-align: bottom;'
      imgNode.src = `/src/assets/emoji/${item}.webp` // 注意写你的项目相对路径
      range.insertNode(imgNode)
      dialogVisible.value = false
      editor.execCommand('seleceAll')
      editor.selection.getRng().collapse()
      editor.focus()
    }
    
    const getEditorWordLen = () => {
      const content = tinymce.activeEditor.getContent({ format: 'text' })
      const wordObj = sumLetter(content)
      wordLenght.value = wordObj?.txt?.length || 0
    }
    
    const handlePreview = () => {
      const editor = tinymce.activeEditor
      editor.on('preview', (editor) => {
        console.log(editor)
      })
    }
    
    onMounted(() => {
      tinymce.init({})
      setTimeout(() => getEditorWordLen(), 800)
    })
    
    watch(
      initContent,
      (newVal) => {
        content.value = newVal
      },
      { deep: true, immediate: true }
    )
    
    watch(
      content,
      (newVal) => {
        emit('input', newVal)
      },
      { deep: true }
    )
    </script>
    
    <script lang="ts">
    export default { name: 'TinymceEditor' }
    </script>
    
    <style scoped lang="scss">
    .emoji {
      display: flex;
      flex-wrap: wrap;
    }
    
    .emoji-item {
      display: flex;
      justify-content: center;
      align-items: center;
      margin-left: 10px;
      margin-bottom: 8px;
      cursor: pointer;
    
      img {
        width: 48px;
        height: 48px;
      }
    }
    
    .editor_footer {
      margin-top: 20px;
      font-size: 13px;
    }
    </style>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240

    创建wordlimit.ts文件,作为限制字符的触发条件

    import tinymce from 'tinymce/tinymce'
    import { ElMessage } from 'element-plus'
    import { sumLetter } from '@/utils/utilTool'
    
    tinymce.PluginManager.add('wordlimit', function (editor): any {
      const pluginName = '字数限制'
      const app = tinymce.util.Tools.resolve('tinymce.util.Delay')
      const Tools = tinymce.util.Tools.resolve('tinymce.util.Tools')
      const wordlimit_event = editor.getParam('ax_wordlimit_event', 'SetContent Undo Redo Keyup input paste')
      const options = editor.getParam('wordlimit', {}, 'object')
      let close = null
    
      const toast = function (message) {
        close && close.close()
        close = ElMessage.error(message)
        return
      }
    
      // 默认配置
      const defaults = {
        spaces: false, // 是否含空格
        isInput: false, // 是否在超出后还可以输入
        maxMessage: '超出最大输入字符数量!',
        changeCallback: () => {}, // 自定义的回调方法
        changeMaxCallback: () => {},
        toast // 提示弹窗
      }
    
      class WordLimit {
        constructor(editor, options) {
          options = Tools.extend(defaults, options)
          let preCount = 0
          let _wordCount = 0
          let oldContent = editor.getContent()
          const WordCount = editor.plugins.wordcount
    
          editor.on(wordlimit_event, function (e) {
            const content = editor.getContent() || e.content || ''
            if (!options.spaces) {
              _wordCount = WordCount.body.getCharacterCount()
            } else {
              _wordCount = WordCount.body.getCharacterCountWithoutSpaces()
            }
            options.changeCallback({
              ...options,
              editor,
              num: _wordCount,
              content,
              ...sumLetter(content)
            })
            if (_wordCount > options.max) {
              preCount = _wordCount
              if (options.isInput == !1) {
                editor.setContent(oldContent)
                if (!options.spaces) {
                  _wordCount = WordCount.body.getCharacterCount()
                } else {
                  _wordCount = WordCount.body.getCharacterCountWithoutSpaces()
                }
              }
              editor.getBody().blur()
              editor.fire('wordlimit', {
                maxCount: options.max,
                wordCount: _wordCount,
                preCount: preCount,
                isPaste: e.type === 'paste' || e.paste || false
              })
              toast('最多只能输入' + options.max + '个字')
            }
            oldContent = editor.getContent()
          })
        }
      }
    
      const setup = function () {
        if (!options && !options.max) return false
        if (!editor.plugins.wordcount) return toast('请先在tinymce的plugins配置wordlimit之前加入wordcount插件')
        app.setEditorTimeout(
          editor,
          function () {
            const editDom = editor.getContainer()
            const wordNum: any = editDom.querySelector('button.tox-statusbar__wordcount')
            const statusbarpath: any = editDom.querySelector('.tox-statusbar__path')
            statusbarpath ? statusbarpath.remove() : void null
            if (wordNum?.innerText?.indexOf('字符') == -1) wordNum.click()
            new WordLimit(editor, options)
          },
          300
        )
      }
    
      setup()
    
      return {
        getMetadata: function () {
          return {
            name: pluginName
          }
        }
      }
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    使用
    <template>
      <div class="post_contaniner">
        <div style="width: 100%">
          <TinymceEditor v-model="content" @input="inputContent" :wordlimit="{ max: 300 }" />
        </div>
      </div>
    </template>
    
    <script lang="ts" setup>
    import { ref } from 'vue'
    
    const content = ref('Hello World')
    
    const inputContent = (newVal) => {
      console.log(newVal)
      content.value = newVal
    }
    </script>
    
    <style scoped lang="scss">
    .post_contaniner {
      .right {
        flex: 1;
        box-shadow: 0 1px 10px 3px #dbdbdb;
        margin-right: 10px;
        padding: 10px;
        box-sizing: border-box;
      }
    }
    </style>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
  • 相关阅读:
    前端领域的插件式设计
    商业智能BI行业分析思维框架:铅酸蓄电池行业(二)
    MySQL开发技巧——行列转换
    Qt-QImage-convertTo-copy-convertToFormat-格式转换
    spring-cloud-alibaba - nacos配置中心&注册中心 实战
    git还原到之前某个版本
    Linux系统及应用复习题
    使用SSH通过FinalShell远程连接Ubuntu服务器
    Python自动化操作sqlite数据库
    深入理解算法的时间复杂度
  • 原文地址:https://blog.csdn.net/weixin_43624724/article/details/133642446