Foreword
tinymce is currently recognized as the best rich text editor. This article will explain in detail how to integrate tinymce in the vue3 project and encapsulate tinymce into a component.
final effect
Text
1. Install dependencies,
Here we use tinymce5, and the latest version is tinymce6. There will be differences in plug-ins between different versions
npm install [email protected]
2. Install the Chinese package
We create the folder tinymce in the public/resource/ directory of the vue3 project, and create the langs folder under tinymce to store the language Chinese package.
We will download this Chinese package https://gitee.com/shuiche/tinymce-vue3/blob/master/langs/zh_CN.js and put it in the langs folder
3. Migrate ui skin files
We find the skins folder in node_modules/tinymce and copy it to public/resource/tinymce/
4. Encapsulated Component
Create a new Tinymce folder under src/components/. Create 3 new files in the Tinymce/ folder: helper.js, tinymce.js, Tinymce.vue
The contents are:
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 & amp; & amp; 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(() => { destroy() }) onDeactivated(() => { destroy() }) function destroy () { 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 & amp; & amp; typeof val === 'string' & amp; & amp; val !== prevVal & amp; & amp; 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>
Two external functions and a hook are introduced in Tinymce. The specific contents are:
// ==== isNumber function ==== 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 function ==== 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() } }) }
Use components
It is very simple to use, we can use v-model and @change.
<Tinymce v-model="content" @change="handleChange" width="100%" /> //...... let content = ref('') function handleChange (item) { console.log('change', item) }
Postscript
If you have any questions, you can leave a message to communicate