Vue3项目中集成最强的tinymce富文本编辑器
前言
tinymce是目前公认的最好的富文本编辑器。本篇文章将详细说明在vue3项目中如何集成tinymce,并将 tinymce封装成组件使用
最终效果
正文
1. 安装依赖,
这里我们使用了tinymce5,最新已经到tinymce6了。不同版本之间插件上会有差异
npm install tinymce@5.10.2
2. 安装汉化包
我们在 vue3 项目的public/resource/ 目录下创建文件夹 tinymce,在tinymce下再创建 langs文件夹用来存放语言汉化包
我们将 这个中文包https://gitee.com/shuiche/tinymce-vue3/blob/master/langs/zh_CN.js下载后放到langs文件夹下
3. 迁移ui皮肤文件
我们在 node_modules/tinymce 中找到 skins 文件夹,将其复制到 public/resource/tinymce/ 里
4. 封装组件
在src/components/ 下新建 Tinymce 文件夹。在 Tinymce/ 文件夹下新建3个文件: helper.js , tinymce.js, Tinymce.vue
内容分别是:
helper.js
// ==========helper.js==========
const validEvents = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid'
]
const isValidKey = (key) => validEvents.indexOf(key) !== -1
export const bindHandlers = (initEvent, listeners, editor) => {
Object.keys(listeners)
.filter(isValidKey)
.forEach((key) => {
const handler = listeners[key]
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor)
} else {
editor.on(key.substring(2), (e) => handler(e, editor))
}
}
})
}
tinymce.js
// ==========tinymce.js==========
// Any plugins you want to setting has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
export const plugins = [
'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus template textpattern visualblocks visualchars wordcount'
]
export const toolbar = [
'fontsizeselect lineheight searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample',
'hr bullist numlist link preview anchor pagebreak insertdatetime media forecolor backcolor fullscreen'
]
// ==========Tinymce.vue==========
<template>
<div class="prefixCls" :style="{ width: containerWidth }">
<textarea
:id="tinymceId"
ref="elRef"
:style="{ visibility: 'hidden' }"
></textarea>
</div>
</template>
<script setup>
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default/icons'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/code'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/hr'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/media'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/noneditable'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/print'
import 'tinymce/plugins/save'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/spellchecker'
import 'tinymce/plugins/tabfocus'
import 'tinymce/plugins/template'
import 'tinymce/plugins/textpattern'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'
// import 'tinymce/plugins/table';
import { computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, defineProps, defineEmits, getCurrentInstance } from 'vue'
import { toolbar, plugins } from './tinymce'
import { buildShortUUID } from '@/utils/uuid'
import { bindHandlers } from './helper'
import { onMountedOrActivated } from '@/hooks/core/onMountedOrActivated'
import { isNumber } from '@/utils/is'
const props = defineProps({
options: {
type: Object,
default: () => {}
},
value: {
type: String
},
toolbar: {
type: Array,
default: toolbar
},
plugins: {
type: Array,
default: plugins
},
modelValue: {
type: String
},
height: {
type: [Number, String],
required: false,
default: 400
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
showImageUpload: {
type: Boolean,
default: true
}
})
const emits = defineEmits(['change', 'update:modelValue', 'inited', 'init-error'])
const { attrs } = getCurrentInstance()
const tinymceId = ref(buildShortUUID('tiny-vue'))
const containerWidth = computed(() => {
const width = props.width
if (isNumber(width)) {
return `${width}px`
}
return width
})
const editorRef = ref(null)
const fullscreen = ref(false)
const elRef = ref(null)
const tinymceContent = computed(() => props.modelValue)
const initOptions = computed(() => {
const { height, options, toolbar, plugins } = props
const publicPath = '/'
return {
selector: `#${unref(tinymceId)}`,
height,
toolbar,
menubar: 'file edit insert view format table',
plugins,
language_url: '/resource/tinymce/langs/zh_CN.js',
language: 'zh_CN',
branding: false,
default_link_target: '_blank',
link_title: false,
object_resizing: false,
auto_focus: true,
skin: 'oxide',
skin_url: '/resource/tinymce/skins/ui/oxide',
content_css: '/resource/tinymce/skins/ui/oxide/content.min.css',
...options,
setup: (editor) => {
editorRef.value = editor
editor.on('init', (e) => initSetup(e))
}
}
})
const disabled = computed(() => {
const { options } = props
const getdDisabled = options && Reflect.get(options, 'readonly')
const editor = unref(editorRef)
if (editor) {
editor.setMode(getdDisabled ? 'readonly' : 'design')
}
return getdDisabled ?? false
})
watch(
() => attrs.disabled,
() => {
const editor = unref(editorRef)
if (!editor) {
return
}
editor.setMode(attrs.disabled ? 'readonly' : 'design')
}
)
onMountedOrActivated(() => {
if (!initOptions.value.inline) {
tinymceId.value = buildShortUUID('tiny-vue')
}
nextTick(() => {
setTimeout(() => {
initEditor()
}, 30)
})
})
onBeforeUnmount(() => {
destory()
})
onDeactivated(() => {
destory()
})
function destory () {
if (tinymce !== null) {
// tinymce?.remove?.(unref(initOptions).selector!);
}
}
function initSetup (e) {
const editor = unref(editorRef)
if (!editor) {
return
}
const value = props.modelValue || ''
editor.setContent(value)
bindModelHandlers(editor)
bindHandlers(e, attrs, unref(editorRef))
}
function initEditor () {
const el = unref(elRef)
if (el) {
el.style.visibility = ''
}
tinymce
.init(unref(initOptions))
.then((editor) => {
emits('inited', editor)
})
.catch((err) => {
emits('init-error', err)
})
}
function setValue (editor, val, prevVal) {
if (
editor &&
typeof val === 'string' &&
val !== prevVal &&
val !== editor.getContent({ format: attrs.outputFormat })
) {
editor.setContent(val)
}
}
function bindModelHandlers (editor) {
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents
watch(
() => props.modelValue,
(val, prevVal) => {
setValue(editor, val, prevVal)
}
)
watch(
() => props.value,
(val, prevVal) => {
setValue(editor, val, prevVal)
},
{
immediate: true
}
)
editor.on(normalizedEvents || 'change keyup undo redo', () => {
const content = editor.getContent({ format: attrs.outputFormat })
emits('update:modelValue', content)
emits('change', content)
})
editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state
})
}
function handleImageUploading (name) {
const editor = unref(editorRef)
if (!editor) {
return
}
editor.execCommand('mceInsertContent', false, getUploadingImgName(name))
const content = editor?.getContent() ?? ''
setValue(editor, content)
}
function handleDone (name, url) {
const editor = unref(editorRef)
if (!editor) {
return
}
const content = editor?.getContent() ?? ''
const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? ''
setValue(editor, val)
}
function getUploadingImgName (name) {
return `[uploading:${name}]`
}
</script>
<style lang="scss" scoped>
.prefixCls{
position: relative;
line-height: normal;
}
textarea {
z-index: -1;
visibility: hidden;
}
</style>
在Tinymce中引入了两个外部函数和一个hook,具体内容是:
// ==== isNumber 函数====
const toString = Object.prototype.toString
export function is (val, type) {
return toString.call(val) === `[object ${type}]`
}
export function isNumber (val) {
return is(val, 'Number')
}
// ==== buildShortUUID 函数====
export function buildShortUUID (prefix = '') {
const time = Date.now()
const random = Math.floor(Math.random() * 1000000000)
unique++
return prefix + '_' + random + unique + String(time)
}
// ==== onMountedOrActivated hook====
import { nextTick, onMounted, onActivated } from 'vue'
export function onMountedOrActivated (hook) {
let mounted
onMounted(() => {
hook()
nextTick(() => {
mounted = true
})
})
onActivated(() => {
if (mounted) {
hook()
}
})
}
使用组件
使用时非常简单,我们用 v-model 和 @change 即可使用。
<Tinymce v-model="content" @change="handleChange" width="100%" />
// ......
let content = ref('')
function handleChange (item) {
console.log('change', item)
}
后记
如有问题,可以留言沟通
博客中所涉及到的图片都有版权,请谨慎使用