前言
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)
- }
后记
如有问题,可以留言沟通