前言
tinymce是目前公認的最好的富文本編輯器。本篇文章將詳細說明在vue3項目中如何集成tinymce,並將 tinymce封裝成組件使用
最終效果
正文
1. 安裝依賴,
這裏我們使用了tinymce5,最新已經到tinymce6了。不同版本之間插件上會有差異
npm install [email protected]
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)
}
後記
如有問題,可以留言溝通