加载中...

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)
}

后记

如有问题,可以留言沟通

posted @ 2022-06-28 12:01  水车  阅读(16028)  评论(10编辑  收藏  举报