当前位置: 首页 > news >正文

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

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

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

简介

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?: stringplugins?: stringtoolbar?: stringwordlimit?: 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, // 不显示右下角logoauto_update: false, // 不进行自动更新resize: true, // 可以调整大小menubar: false, // 关闭顶部菜单skin_url: '/tinymce/skins/ui/oxide', // 手动引入CSScontent_css: '/tinymce/skins/content/default/content.css', // 手动引入CSStoolbar_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 = 32imgNode.height = 32imgNode.style = 'vertical-align: bottom;'imgNode.src = `/src/assets/emoji/${item}.webp` // 注意写你的项目相对路径range.insertNode(imgNode)dialogVisible.value = falseeditor.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.activeEditoreditor.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 = nullconst 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 = 0let _wordCount = 0let oldContent = editor.getContent()const WordCount = editor.plugins.wordcounteditor.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 = _wordCountif (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 falseif (!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 nullif (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>

相关文章:

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

欢迎点击领取 -《前端面试题进阶指南》&#xff1a;前端登顶之巅-最全面的前端知识点梳理总结 *分享一个使用比较久的&#x1fa9c; 简介 1、安装&#xff1a;pnpm add tinymce / pnpm add tinymce/tinymce-vue > Vue3 tinymce tinymce/tinymce-vue 2、功能实现图片上传…...

typescript 类型声明文件

typescript 类型声明文件概述 在今天几乎所有的JavaScript应用都会引入许多第三方库来完成任务需求。这些第三方库不管是否是用TS编写的&#xff0c;最终都要编译成JS代码&#xff0c;才能发布给开发者使用。6我们知道是TS提供了类型&#xff0c;才有了代码提示和类型保护等机…...

Hadoop伪分布式环境搭建

什么是Hadoop伪分布式集群&#xff1f; Hadoop 伪分布式集群是一种在单个节点上模拟分布式环境的配置&#xff0c;用于学习、开发和测试 Hadoop 的功能和特性。它提供了一个简化的方式来体验和熟悉 Hadoop 的各个组件&#xff0c;而无需配置和管理一个真正的多节点集群。 在 Ha…...

javaee ssm框架项目添加分页控件

搭建ssm框架项目 参考上一篇博文 添加分页控件 引入依赖 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schema…...

2023年中国非晶纳米晶竞争格局、产业链及行业产量分析[图]

非晶合金又称“液态金属、金属玻璃”&#xff0c;是一种新型软磁合金材料&#xff0c;主要包含铁、硅、硼等元素。其主要制品非晶合金薄带的制造工艺是采用急速冷却技术将合金熔液以每秒106℃的速度急速冷却&#xff0c;形成厚度约0.03mm的非晶合金薄带&#xff0c;物理状态表现…...

在业务开发中遇到的树形结构(部门、区域、职位),递归处理。

文章目录 概要对象结构示例完整示例小结 概要 本文主要记录在树形结构中会遇到的问题&#xff0c; 使用部门结构讲解&#xff0c;main方法进行演示。 1、获取部门树结构 2、根据部门id获取所有下级 3、根据部门id获取上级部门 4、根据部门id获取类似面包屑&#xff08;总公司…...

张量-算术操作函数

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 …...

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

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

华为OD机试真题-补种未成活胡杨(Java/C++/Go/Python)

华为OD机试真题-补种未成活胡杨(Java/C++/Go/Python) 题目描述 近些年来,我国防沙治沙取得显著成果。某沙漠新种植N棵胡杨(编号1-N),排成一排。 一个月后,有M棵胡杨未能成活。现可补种胡杨K棵,请问如何补种(只能补种,不能新种),可以得到最多的连续胡杨树? 输入…...

Java卷上天,可以转行干什么?

小刚是某名企里的一位有5年经验的高级Java开发工程师&#xff0c;每天沉重的的工作让他疲惫不堪&#xff0c;让他萌生出想换工作的心理&#xff0c;但是转行其他工作他又不清楚该找什么样的工作 因为JAVA 这几年的更新实在是太太太……快了&#xff0c;JAVA 8 都还没用多久&am…...

Pyside6 安装和简单界面开发

Pyside6 安装和简单界面开发 Pyside6介绍Pysied6开发环境搭建Python安装Pysied6安装 Pyside6界面开发简单界面设计界面设计界面编译 编写界面初始化代码软件打包 Pyside6介绍 对于Python的GUI开发来说&#xff0c;Python自带的可视化编程模块的功能较弱&#xff0c;PySide是跨…...

python读取vivo手机截图,将满屏图片文件移动别的路径

问题之初 python读取vivo手机截图&#xff0c; 将满屏图片文件移动别的路径好多这样的图片&#xff0c;占用手机大量的内存&#xff0c;食之无味弃之可惜&#xff01;那么会复制粘贴&#x1f440;代码的我们我们今天就把这些图片筛选清理掉。 这段代码 原有逻辑的基础上&…...

【一周安全资讯1007】多项信息安全国家标准10月1日起实施;GitLab发布紧急安全补丁修复高危漏洞

要闻速览 1.以下信息安全国家标准10月1日起实施 2.GitLab发布紧急安全补丁修复高危漏洞 3.主流显卡全中招&#xff01;GPU.zip侧信道攻击可泄漏敏感数据 4.MOVEit漏洞导致美国900所院校学生信息发生大规模泄露 5.法国太空和国防供应商Exail遭黑客攻击&#xff0c;泄露大量敏感…...

2023年09月个人工作生活总结

本文为 2023 年 9 月工作生活总结。 研发编码 Alpine 容器 某工程部署于alpine镜像&#xff0c;当初看上是因为其体积小&#xff0c;其它微服务&#xff0c;在250MB左右&#xff0c;但那个工程只用50MB。最近发现时间戳转换不正确。对于同一时间字符串转时间戳函数&#xff0…...

现货白银图表分析的依据

现货白银的行情图表分析其实与股票的差不多&#xff0c;投资者可以结合均线、k线的变化&#xff0c;来分析实时的行情走势。当走势图的均线呈多头排列&#xff0c;即短期、中期、长期均线依次从上到下排列并向右上方运行&#xff0c;且白银价格沿各均线向右上方拉升&#xff0c…...

python多线程与多进程

多线程与多进程 一, 什么是进程, 什么是线程? ​ 进程: 运行中的程序. 每次我们执行一个程序, 咱们的操作系统对自动的为这个程序准备一些必要的资源(例如, 分配内存, 创建一个能够执行的线程. ) ​ 线程: 程序内, 可以直接被CPU调度的执行过程. 是操作系统能够进行运算调度…...

62从零开始学Java之时间相关的类都有哪些?

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 我们在开发时&#xff0c;除了数字、数学这样的常用API之外&#xff0c;还有日期时间类&#xff0c;更…...

2023年山东安全员c证考试题库及答案解析来了!

...

【Leetcode】买卖股票系列

121. 买卖股票的最佳时机 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔…...

SLAM面试笔记(8) — 计算机视觉面试题

目录 问题1&#xff1a;目标检测的算法分类 问题2&#xff1a;卷积神经网络的组成 问题3&#xff1a;输入层的作用 问题4&#xff1a;卷积层作用 问题5&#xff1a;卷积核类型 问题6&#xff1a;11卷积核作用 问题7&#xff1a;卷积核是否越大越好 问题8&#xff1a;棋…...

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器

——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的​​一体化测试平台​​&#xff0c;覆盖应用全生命周期测试需求&#xff0c;主要提供五大核心能力&#xff1a; ​​测试类型​​​​检测目标​​​​关键指标​​功能体验基…...

visual studio 2022更改主题为深色

visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中&#xff0c;选择 环境 -> 常规 &#xff0c;将其中的颜色主题改成深色 点击确定&#xff0c;更改完成...

oracle与MySQL数据库之间数据同步的技术要点

Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异&#xff0c;它们的数据同步要求既要保持数据的准确性和一致性&#xff0c;又要处理好性能问题。以下是一些主要的技术要点&#xff1a; 数据结构差异 数据类型差异&#xff…...

NFT模式:数字资产确权与链游经济系统构建

NFT模式&#xff1a;数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新&#xff1a;构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议&#xff1a;基于LayerZero协议实现以太坊、Solana等公链资产互通&#xff0c;通过零知…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...