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

news2024/11/19 7:40:31

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

*分享一个使用比较久的🪜

简介

1、安装:pnpm add tinymce / pnpm add @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',
      '<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>

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

使用
<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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1066120.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式锁:5种方案解决商品超卖的方案

一 分布式锁 1.1 分布式锁的作用 在多线程高并发场景下&#xff0c;为了保证资源的线程安全问题&#xff0c;jdk为我们提供了synchronized关键字和ReentrantLock可重入锁&#xff0c;但是它们只能保证一个工程内的线程安全。在分布式集群、微服务、云原生横行的当下&#xff…

w10系统 如何使用 C++、cmake、opencv、

w10系统的C环境配置 1.安装 vscode编辑器 首先安装&#xff1a;VScode 安装后开始安装插件&#xff1a; C 插件 2.配置w10系统的C环境 使用编译器MinGW 官方地址&#xff1a;https://www.mingw-w64.org/ 下载地址&#xff1a;https://sourceforge.net/projects/mingw-w64/f…

AI产品经理-能力模型

一、概况 AI产品经理/助理&#xff08;需求工程师&#xff09;&#xff1a;大多数入门的AI产品经理应该都在这里&#xff0c;顾名思义&#xff0c;就是在整体产品规划中帮助大PD实现部分产品功能的助理或者需求工程师&#xff0c;需要具备比较强的AI知识框架与理解能力以保障各…

Openlayers 教程 - 地图以及图层数据导出(打印)图片

Openlayers 教程 - 地图以及图层数据导出&#xff08;打印&#xff09;图片 地图导出核心代码完整代码&#xff1a;在线示例 本文包括地图导出核心代码、完整代码以及在线示例。 地图导出核心代码 这里放上 ES 封装的核心代码&#xff0c;创建多边形或者其他几何对象&#xff…

做小说推文和短剧推广,找数据好的授权平台

小说推文和短剧推广很多平台吃单怎么办&#xff1f;可以试试”巨量推文“&#xff0c;一个不吃单的平台 众所周知 小说推文和短剧推广很多平台会吃单&#xff0c;比如你实际官方数据是10个订单&#xff0c;很多平台只给你5个&#xff0c;这样你损失可能就是一半的利润&#xf…

【MySQL】基本查询(二)

文章目录 一. 结果排序二. 筛选分页结果三. Update四. Delete五. 截断表六. 插入查询结果结束语 操作如下表 //创建表结构 mysql> create table exam_result(-> id int unsigned primary key auto_increment,-> name varchar(20) not null comment 同学姓名,-> chi…

虚拟展厅有什么重要意义,了解虚拟展厅在宣传中的应用

引言&#xff1a; 随着科技的不断进步&#xff0c;虚拟展厅已经逐渐成为展览行业的重要一环。虚拟展厅是一种数字化平台&#xff0c;为观众提供了与传统展览完全不同的体验。 一&#xff0e;虚拟展厅的定义 虚拟展厅是一个通过互联网和虚拟现实技术创建的数字展示空间&#x…

windows系统下利用python对指定文件夹下面的所有文件的创建时间进行修改

windows系统下利用python对指定文件夹下面的所有文件的创建时间进行修改 不知道其他的朋友们有没有这个需求哈&#xff0c;反正咱家是有这个需求 需求1、当前有大量的文件需要更改文件生成的时间&#xff0c;因为不可告知的原因&#xff0c;当前的文件创建时间是不能满足使用的…

现在玩51单片机,这也太LOW了?

作为一名科普的博主&#xff0c;今天我们来聊一聊51单片机。 一、什么是51单片机 单片机是一种微型计算机&#xff0c;广泛应用于各种电子产品和工业控制领域。51单片机是指基于Intel的8051微处理器为核心的单片机&#xff0c;是最为常见和广泛应用的单片机之一。 二、51单片机…

近视眼选择什么台灯好?分享医生都说好的台灯

如今全国近视人数已经超过6亿&#xff0c;差不多占据了我国人口的一半&#xff0c;而青少年的近视率更是位居世界第一&#xff01;据数据显示&#xff0c;全国儿童青少年总体近视率高达53.6%&#xff0c;其中小学生为36.0%&#xff0c;初中生为71.6%&#xff0c;高中生为81.0%&…

微服务学习(十一):安装Git

微服务学习&#xff08;十一&#xff09;&#xff1a;安装Git 1、下载Git 官网下载Git 2、将下载后的资源包上传到服务器 3、解压并安装 tar -zxvf git-2.42.0.tar.gz4、安装依赖 yum install zlib yum install zlib-devel5、执行操作命令 cd /home/git/git-2.42.0 ./co…

华为云CodeArts Check代码检查服务用户声音反馈集锦(8)

作者&#xff1a;gentle_zhou 原文链接&#xff1a;CodeArts Check代码检查服务用户声音反馈集锦&#xff08;8&#xff09;-云社区-华为云 CodeArts Check&#xff08;原CodeCheck&#xff09;&#xff0c;是自主研发的代码检查服务。建立在华为30年自动化源代码静态检查技术…

张量-算术操作函数

tf.add(x,y,name None)求和函数 示例代码如下: import tensorflow.compat.v1 as tf tf.disable_v2_behavior()x 1 y 2a tf.add(x,y)with tf.Session() as sess:print(sess.run(a)) tf.subtract(x,y,name None)减法函数 示例代码如下: import tensorflow.compat.v1 as …

吐血整理,最全Pytest自动化测试框架快速上手(超详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 pytest框架 pyte…

前端【响应式图片处理】之 【picture标签】

目录 &#x1f31f;前言&#x1f31f;目前最常见的解决方案&#x1f31f;新的解决方案<picture>&#x1f31f;<picture>的工作原理&#x1f31f;<picture> 兼容性解决方案&#x1f31f;写在最后 &#x1f31f;前言 哈喽小伙伴们&#xff0c;前端开发过程中经…

<el-input> textarea文本域显示滚动条(超过高度就自动显示)

需求&#xff1a;首先是给定高度&#xff0c;输入文本框要自适应这个高度。文本超出高度就会显示滚动条否则不显示。 <el-row class"textarea-row"><el-col :span"3" class"first-row-title">天气</el-col><el-col :span&…

多目标优化两种算法:加权、智能优化算法

传统数学优化算法&#xff08;加权&#xff09; 使用数学优化算法解决多目标优化问题通常是将各个子目标聚合成一个带权重的单目标函数&#xff0c;系数由决策者决定&#xff0c;或者由优化方法自适应调整。即通过加权等方式将多目标问题转化为单目标问题进行求解。 这样每次只…

编程每日一练(多语言实现)基础篇:控制台打印九九乘法口诀表

文章目录 一、实例描述二、技术要点三、代码实现3.1 C 语言实现3.2 Python 语言实现3.3 Java 语言实现3.4 JavaScript 语言实现3.5 Go 语言实现 一、实例描述 本实例要求打印出乘法口诀表&#xff0c;在乘法口诀有行和列项的相乘得出的乘法结果。根据这个特点&#xff0c;使用…

Configuration of phpstudy and sqli-labs

Go download the app&#xff1a; 小皮面板(phpstudy) - 让天下没有难配的服务器环境&#xff01; (xp.cn) Have done. Then enter the program. Enable both functions&#xff1a; Apache and MySQL. Open the website&#xff1a; Next, Lets make the sqli-liab. GitHub…

Web server failed to start. Port 8080 was already in use

一、问题 package com.djc.boot;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annota…