欢迎点击领取 -《前端面试题进阶指南》:前端登顶之巅-最全面的前端知识点梳理总结
*分享一个使用比较久的🪜
简介
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>