Secondary packaging development of vue3 rich text editor-Tinymce

Welcome to click to receive - "Advanced Cheats for Front-end Development Interview Questions": Reaching the Top of the Front-end - the most comprehensive summary of front-end knowledge points

Exclusive link

Introduction

1. Installation: pnpm add tinymce @tinymce/tinymce-vue ===> Vue3 + tinymce + @tinymce/tinymce-vue
2. Functional realization of image upload and fund Card insertion, revenue card insertion, source code reuse, maximum length limit, custom emoticon package insertion, text content input, preview and other functions

Insert image description here

Code display

Create the TinymceEditor.vue file as a public component under the components file

<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',
      '<svg t="1696250970925" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24834" width="21" height="21"><path d="M512 133.12c208.91648 0 378.88 169.96352 378.88 378.88s-169.96352 378.88-378.88 378.88-378.88-169.96352-378.88-378.88 169.96352-378.88 378.88-378.88m0-71.68c-248.83712 0-450.56 201.72288-450.56 450.56s201.72288 450.56 450.56 450.56 450.56-201.72288 450.56-450.56-201.72288-450.56-450.56-450.56z" fill="#2c2c2c" p-id="24835"></path><path d="M624.74752 263.6288a35.72224 35.72224 0 0 0-25.344 10.496L512 361.52832 424.59648 274.1248a35.73248 35.73248 0 0 0-25.344-10.496 35.84 35.84 0 0 0-25.344 61.17888L451.07712 401.9712H348.16a35.84 35.84 0 1 0 0 71.68h128v66.56H348.16a35.84 35.84 0 1 0 0 71.68h128v133.12a35.84 35.84 0 1 0 71.68 0v-133.12h128a35.84 35.84 0 1 0 0-71.68h-128v-66.56h128a35.84 35.84 0 1 0 0-71.68h-102.91712l77.16352-77.16352a35.84 35.84 0 0 0-25.33888-61.17888z" fill="#2c2c2c" p-id="24836"></path></svg>'
    )
    editor.ui.registry.addIcon(
      'income-icon',
      '<svg t="1696250530786" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15004" width="21" height="21"><path d="M920 152v720H104V152h816z m-67.2 67.2H171.2v585.6h681.6V219.2z" fill="#2c2c2c" p-id="15005"></path><path d="M32 152m7 0l946 0q7 0 7 7l0 53.2q0 7-7 7l-946 0q-7 0-7-7l0-53.2q0-7 7-7Z" fill="#2c2c2c" p-id="15006"></path><path d="M450.906 417.788l122.187 122.19 115.4-115.401 47.518 47.517-115.4 115.4-47.517 47.518-122.189-122.19-115.399 115.401-47.517-47.517 162.917-162.918z" fill="#2c2c2c" p-id="15007"></path><path d="M300.8 718.4H368v86.4h-67.2v-86.4z m120-86.4H488v172.8h-67.2V632z m120 48H608v124.8h-67.2V680z m120-67.2H728v192h-67.2v-192z" fill="#2c2c2c" p-id="15008"></path></svg>'
    )

    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>

Create wordlimit.ts file as a trigger condition for limiting characters

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
      }
    }
  }
})

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

Guess you like

Origin blog.csdn.net/weixin_43624724/article/details/133642446