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
欢迎点击领取 -《前端面试题进阶指南》:前端登顶之巅-最全面的前端知识点梳理总结 *分享一个使用比较久的🪜 简介 1、安装:pnpm add tinymce / pnpm add tinymce/tinymce-vue > Vue3 tinymce tinymce/tinymce-vue 2、功能实现图片上传…...
typescript 类型声明文件
typescript 类型声明文件概述 在今天几乎所有的JavaScript应用都会引入许多第三方库来完成任务需求。这些第三方库不管是否是用TS编写的,最终都要编译成JS代码,才能发布给开发者使用。6我们知道是TS提供了类型,才有了代码提示和类型保护等机…...
Hadoop伪分布式环境搭建
什么是Hadoop伪分布式集群? Hadoop 伪分布式集群是一种在单个节点上模拟分布式环境的配置,用于学习、开发和测试 Hadoop 的功能和特性。它提供了一个简化的方式来体验和熟悉 Hadoop 的各个组件,而无需配置和管理一个真正的多节点集群。 在 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年中国非晶纳米晶竞争格局、产业链及行业产量分析[图]
非晶合金又称“液态金属、金属玻璃”,是一种新型软磁合金材料,主要包含铁、硅、硼等元素。其主要制品非晶合金薄带的制造工艺是采用急速冷却技术将合金熔液以每秒106℃的速度急速冷却,形成厚度约0.03mm的非晶合金薄带,物理状态表现…...
在业务开发中遇到的树形结构(部门、区域、职位),递归处理。
文章目录 概要对象结构示例完整示例小结 概要 本文主要记录在树形结构中会遇到的问题, 使用部门结构讲解,main方法进行演示。 1、获取部门树结构 2、根据部门id获取所有下级 3、根据部门id获取上级部门 4、根据部门id获取类似面包屑(总公司…...
张量-算术操作函数
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 …...
虚拟展厅有什么重要意义,了解虚拟展厅在宣传中的应用
引言: 随着科技的不断进步,虚拟展厅已经逐渐成为展览行业的重要一环。虚拟展厅是一种数字化平台,为观众提供了与传统展览完全不同的体验。 一.虚拟展厅的定义 虚拟展厅是一个通过互联网和虚拟现实技术创建的数字展示空间&#x…...
华为OD机试真题-补种未成活胡杨(Java/C++/Go/Python)
华为OD机试真题-补种未成活胡杨(Java/C++/Go/Python) 题目描述 近些年来,我国防沙治沙取得显著成果。某沙漠新种植N棵胡杨(编号1-N),排成一排。 一个月后,有M棵胡杨未能成活。现可补种胡杨K棵,请问如何补种(只能补种,不能新种),可以得到最多的连续胡杨树? 输入…...
Java卷上天,可以转行干什么?
小刚是某名企里的一位有5年经验的高级Java开发工程师,每天沉重的的工作让他疲惫不堪,让他萌生出想换工作的心理,但是转行其他工作他又不清楚该找什么样的工作 因为JAVA 这几年的更新实在是太太太……快了,JAVA 8 都还没用多久&am…...
Pyside6 安装和简单界面开发
Pyside6 安装和简单界面开发 Pyside6介绍Pysied6开发环境搭建Python安装Pysied6安装 Pyside6界面开发简单界面设计界面设计界面编译 编写界面初始化代码软件打包 Pyside6介绍 对于Python的GUI开发来说,Python自带的可视化编程模块的功能较弱,PySide是跨…...
python读取vivo手机截图,将满屏图片文件移动别的路径
问题之初 python读取vivo手机截图, 将满屏图片文件移动别的路径好多这样的图片,占用手机大量的内存,食之无味弃之可惜!那么会复制粘贴👀代码的我们我们今天就把这些图片筛选清理掉。 这段代码 原有逻辑的基础上&…...
【一周安全资讯1007】多项信息安全国家标准10月1日起实施;GitLab发布紧急安全补丁修复高危漏洞
要闻速览 1.以下信息安全国家标准10月1日起实施 2.GitLab发布紧急安全补丁修复高危漏洞 3.主流显卡全中招!GPU.zip侧信道攻击可泄漏敏感数据 4.MOVEit漏洞导致美国900所院校学生信息发生大规模泄露 5.法国太空和国防供应商Exail遭黑客攻击,泄露大量敏感…...
2023年09月个人工作生活总结
本文为 2023 年 9 月工作生活总结。 研发编码 Alpine 容器 某工程部署于alpine镜像,当初看上是因为其体积小,其它微服务,在250MB左右,但那个工程只用50MB。最近发现时间戳转换不正确。对于同一时间字符串转时间戳函数࿰…...
现货白银图表分析的依据
现货白银的行情图表分析其实与股票的差不多,投资者可以结合均线、k线的变化,来分析实时的行情走势。当走势图的均线呈多头排列,即短期、中期、长期均线依次从上到下排列并向右上方运行,且白银价格沿各均线向右上方拉升,…...
python多线程与多进程
多线程与多进程 一, 什么是进程, 什么是线程? 进程: 运行中的程序. 每次我们执行一个程序, 咱们的操作系统对自动的为这个程序准备一些必要的资源(例如, 分配内存, 创建一个能够执行的线程. ) 线程: 程序内, 可以直接被CPU调度的执行过程. 是操作系统能够进行运算调度…...
62从零开始学Java之时间相关的类都有哪些?
作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 我们在开发时,除了数字、数学这样的常用API之外,还有日期时间类,更…...
【Leetcode】买卖股票系列
121. 买卖股票的最佳时机 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔…...
SLAM面试笔记(8) — 计算机视觉面试题
目录 问题1:目标检测的算法分类 问题2:卷积神经网络的组成 问题3:输入层的作用 问题4:卷积层作用 问题5:卷积核类型 问题6:11卷积核作用 问题7:卷积核是否越大越好 问题8:棋…...
聊聊MySQL面试常问名词回表、索引覆盖,最左匹配
文章目录 1. 前言2. 回表操作 Index Lookup2.1 什么是回表2.2 回表的成本2.3 如何避免回表 3. 索引覆盖 Covering Index3.1 什么是索引覆盖3.2 索引覆盖的优点3.3 如何使用索引覆盖 4. 最左匹配原则(Leftmost Prefix Match)4.1 什么是最左匹配原则4.2 最…...
【面试】C/C++面试八股
C/C面试八股 编译过程的四个阶段C和C语言的区别简单介绍一下三大特性多态的实现原理虚函数的构成原理虚函数的调用原理虚表指针在什么地方进行初始化的?构造函数为什么不能是虚函数为什么建议将析构函数设为虚函数虚函数和纯虚函数的区别抽象类类对象的对象模型内存…...
学习记忆——数学篇——算术——无理数
谐音记忆法 2 \sqrt{2} 2 ≈1.41421:意思意思而已;意思意思; 3 \sqrt{3} 3 ≈1.7320:—起生鹅蛋;一起生儿; 5 \sqrt{5} 5 ≈2.2360679:两鹅生六蛋(送)六妻舅;儿儿生…...
python协程和任务
协程概念引入 协程是我要重点去讲解的一个知识点. 它能够更加高效的利用CPU. 其实, 我们能够高效的利用多线程来完成爬虫其实已经很6了. 但是, 从某种角度讲, 线程的执行效率真的就无敌了么? 我们真的充分的利用CPU资源了么? 非也~ 比如, 我们来看下面这个例子. 我们…...
visual studio code配置anaconda3的python虚拟环境
参考: Visual Studio Code配置anconda3虚拟环境 - 知乎...
【Unity3D编辑器开发】Unity3D编辑器开发基础性框架结构【全面总结】
推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 嗨,大家好,我是恬静的小魔龙。 同学们…...
一座“城池”:泡泡玛特主题乐园背后,IP梦想照亮现实
“更适合中国宝宝体质”的主题乐园,被泡泡玛特造出来了。 9月26日,位于北京朝阳公园内的国内首个潮玩行业沉浸式 IP 主题乐园,也是泡泡玛特首个线下乐园——泡泡玛特城市乐园 POP LAND正式开园。 约4万平方米的空间中,泡泡玛特使…...
【什么是闭包? 闭包产生的原因? 闭包有哪些表现形式?】
JS闭包 什么是闭包?闭包产生的原因?闭包有哪些表现形式? 什么是闭包? 闭包是指一个函数可以访问并操作在其作用域之外的变量的能力。在 JavaScript 中,每当函数被创建时,就会创建一个闭包。 以下是一个简单的闭包示例…...
JackJson和FastJson
前言: fastjson是一款强大的json格式转换工具,我个人在开发中就非常喜欢用fastjson;但是由于某些原因,导致fastjson会有一些漏洞,因此在漏洞扫描后需要修复都是要求我们升级版本,或者替换为jackjson&#…...
SpringCloud学习一
单体应用存在的问题 随着业务的发展,开发变得越来越复杂。 修改、新增某个功能,需要对整个系统进行测试、重新部署。 一个模块出现问题,很可能导致整个系统崩溃。 多个开发团队同时对数据进行管理,容易产生安全漏洞。 各个模块…...
罗湖做网站联系电话/哪里有培训班
一、利用django往前端的web图表传参因为echarts其核心是利用js的底层技术,所以python-django往前端的echarts传递参数又可以转化成python-django像前端的js传递参数的问题,这里介绍一种很简单的方法。二、1、方法核心:通过django往前端传递参…...
wordpress仿小刀主题/百度识图查另一半情头
Cron表达式是一个表示时间周期的字符串。分为6或7个域,每一个域代表一个含义。 格式:{秒} {分} {时} {日} {月} {周} {年(可选)} 摘抄自:https://blog.csdn.net/weixin_39921689/article/details/111860299...
c语言开发网站后端/网络营销的理解
npm的包安装分为本地安装(local)、全局安装(global)两种,从敲的命令行来看,差别只是有没有-g而已,比如 npm install grunt # 本地安装 npm install -g grunt-cli # 全局安装这两种安装方式有什么…...
最好网站开发公司电话/常州seo排名收费
对于请求参数的编码处理基本上分为get和post两种情况。 1、POST index.html <!DOCTYPE html> <head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8"><title>HTML范例</title> </head> <body>…...
WordPress如何添加cnzz/辽阳网站seo
winform 仪表盘相关下载链接://download.csdn.net/download/floweroflvoe/10432601?utm_sourcebbsseo 控件首次拖拽上来是这样的: Demo中美化之后是这样的: 这是修改demo后的成果: 其中颜色是一点一点…...
安装网站源码/综合型b2b电子商务平台网站
职场一分钱一分货 人家拿高薪,不是全靠运气 工作年限越久,不代表越值钱 一年经验用了五年,无法改变 因为经历不等于经验 加班“重灾区”的互联网行业以996闻名,每天超过11小时的工作时长每年位居各行业之首。 工作内容重复&…...