npm i tinymce
我这里安装的版本是 5.10.2
安装 tinymce 时会安装所有开源的插件,在 node_modules/tinymce/plugins
下,在需要时直接引入就可以。
然后,node_modules 中找到 tinymce 目录,将目录中 skins 文件夹复制到新建的public/tinymce 文件夹中,然后去下载相关语言包,下载地址,放到 public/tinymce/language 中,后续需要引入。
tinymce 有三种模式:经典模式(classic,默认)、行内模式(inline)、清爽模式(Distraction-free),这里介绍最常用的经典模式,其它的模式可自行查看文档。
tinymce 的插件有开源插件和付费插件,目前开源插件能满足我的需求,我这边采用开源插件进行开发。
二、编辑器配置
1. 基本配置
添加最基本的配置:
<template>
<textarea :id="tinymceId" />
</template>
<script lang="ts">
import { defineComponent, computed, onMounted, onBeforeUnmount, unref } from 'vue'
import type { Editor, RawEditorSettings } from 'tinymce'
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/icons/default/icons'
export default defineComponent({
setup(){
const tinymceId = ref<string>(UUID())
const editorRef = ref<Editor>()
const initOptions = computed(():RawEditorSettings => {
const publicPath = __webpack_public_path__
return {
selector: `#${tinymceId.value}`,
language_url: `${publicPath}tinymce/langs/zh_CN.js`,
language: 'zh_CN',
skin_url: `${publicPath}tinymce/skins/ui/oxide`,
content_css: `${publicPath}tinymce/skins/ui/oxide/content.min.css`,
onMounted(() => {
tinymce.init(initOptions.value)
onBeforeUnmount(() => {
destory()
function destory() {
if (tinymce !== null) {
tinymce?.remove?.(unref(initOptions).selector!)
return { tinymceId }
</script>
效果如下:
2. 编辑器初始化
在初始化 setup 的钩子中可以进行初始化的操作:
向编辑器中填写初始化内容
设置编辑器的 只读/编辑状态
监听编辑器的相关操作
const initOptions = computed(() => {
return {
setup: (editor: Editor) => {
editorRef.value = editor
editor.on('init', initSetup)
function initSetup() {
const editor = unref(editorRef)
if (!editor) {
return
const value = props.value || ''
editor.setContent(value)
bindModelHandlers(editor)
function setValue(editor, val: string, prevVal?: string) {
editor
&& typeof val === 'string'
&& val !== prevVal
&& val !== editor.getContent()
editor.setContent(val)
function bindModelHandlers(editor: any) {
watch(() => props.value,
(val: string, prevVal) => setValue(editor, val, prevVal),
{ immediate: true },
watch(
() => props.disabled,
val => {
editor.setMode(val ? 'readonly' : 'design')
{ immediate: true },
editor.on('change keyup undo redo', () => {
const content = editor.getContent()
emit('update:value', content)
emit('change', content)
3. 图片上传配置
使用 images_upload_handler
可自定义上传处理逻辑,该自定义函数需提供三个参数:blobInfo、成功回调、失败回调 和 上传进度。使用该配置,则无需使用其他上传配置选项
const initOptions = computed(() => {
return {
images_upload_handler: handleImgUpload
function handleImgUpload(blobInfo, success, failure, progress) {
var xhr, formData;
var file = blobInfo.blob();
xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.open('POST', '/demo/upimg.php');
xhr.onload = function() {
var json;
if (xhr.status != 200) {
failFun('HTTP Error: ' + xhr.status);
return;
json = JSON.parse(xhr.responseText);
if (!json || typeof json.location != 'string') {
failFun('Invalid JSON: ' + xhr.responseText);
return;
succFun(json.location);
formData = new FormData();
formData.append('file', file, file.name );
xhr.send(formData);
最终效果图:
4. 完整版本代码
注意:paste_retain_style_properties
属性可以保留复制过来的相关样式,比如要保留字体大小、颜色、背景颜色,可以将其配置为 paste_retain_style_properties: 'font-size color background background-color'
,如果要保留所有样式可以设置为 all
,但是这样会造成代码量很大,并且这个属性将在 6 版本中移除,谨慎使用。
<template>
<textarea :id="tinymceId" />
</template>
<script lang="ts">
import {
defineComponent, computed, onMounted, ref, PropType, unref, watch, onBeforeUnmount,
} from 'vue'
import type { Editor, RawEditorSettings } from 'tinymce'
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/image'
import 'tinymce/plugins/toc'
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/table'
import 'tinymce/plugins/template'
import 'tinymce/plugins/textpattern'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'
import { plugins as initialPlugins, toolbar as initialToolbar, fontFormats } from './tinymce'
import { UUID } from 'uuid'
type Recordable<T = any> = Record<string, T>
export default defineComponent({
props: {
value: {
type
: String,
disabled: {
type: Boolean,
default: false
options: {
type: Object as PropType<Partial<RawEditorSettings>>,
default: () => ({}),
toolbar: {
type: String,
default: initialToolbar,
plugins: {
type: Array as PropType<string[]>,
default: initialPlugins,
height: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 400,
width: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 'auto',
emits: ['change', 'update:value'],
setup(props, { emit }) {
const tinymceId = ref<string>(UUID())
const editorRef = ref<Editor>()
const initOptions = computed((): RawEditorSettings => {
const publicPath = __webpack_public_path__
const {
height, options, toolbar, plugins,
} = props
return {
selector: `#${tinymceId.value}`,
language_url: `${publicPath}tinymce/langs/zh_CN.js`,
language: 'zh_CN',
skin_url: `${publicPath}tinymce/skins/ui/oxide`,
content_css: `${publicPath}tinymce/skins/ui/oxide/content.min.css`,
images_upload_handler: handleImgUpload,
images_file_types: 'jpeg,jpg,png,gif,bmp,webp',
convert_urls: false,
branding: false,
placeholder: '请输入内容',
toolbar,
plugins,
height,
toolbar_mode: 'sliding',
toolbar_sticky: true,
paste_block_drop: true,
paste_data_images: false,
font_formats: fontFormats,
paste_retain_style_properties: 'color border border-left border-right border-bottom border-top',
paste_webkit_styles: 'none',
paste_tab_spaces: 2,
content_style: `
html, body { height:100%; }
img { max-width:100%; display:block;height:auto; }
a { text-decoration: none; }
p { line-height:1.6; margin: 0px; }
table { word-wrap:break-word; word-break:break-all;max-width:100%; border:none; border-color:#999; }
.mce-object-iframe { width:100%; box-sizing:border-box; margin:0; padding:0; }
ul,ol { list-style-position:inside; }
...options,
setup: (editor: Editor) => {
editorRef.value = editor
editor.on('init', initSetup)
onMounted(() => {
tinymce.init(initOptions.value)
onBeforeUnmount(() => {
destory()
function destory() {
if (tinymce !== null) {
tinymce?.remove?.(unref(initOptions).selector!)
function handleImgUpload(blobInfo, success, failure, progress) {
console.log('blobInfo', blobInfo.blob(), blobInfo.filename())
const { type: fileType, name: fileName } = blobInfo.blob()
function initSetup() {
const editor = unref(editorRef)
if (!editor) {
return
const value = props.value || ''
editor.setContent(value)
bindModelHandlers(editor)
function setValue(editor: Recordable, val: string, prevVal?: string) {
editor
&& typeof val === 'string'
&& val !== prevVal
&& val !== editor.getContent()
editor.setContent(val)
function bindModelHandlers(editor: any) {
watch(
() => props.value,
(val: string, prevVal) => setValue(editor, val, prevVal),
{ immediate: true },
watch(
() => props.disabled,
val => {
editor.setMode(val ? 'readonly' : 'design')
{ immediate: true },
editor.on('change keyup undo redo', () => {
const content = editor.getContent()
emit('update:value', content)
emit('change', content)
return {
tinymceId,
</script>
tinymce.ts 文件里是 tinymce 的 plugins、toolbar、fontFormats 的配置,这里基本上使用了所有的开源插件,功能比较齐全
export const plugins = [
'advlist anchor autolink code codesample directionality fullscreen hr insertdatetime link lists nonbreaking noneditable pagebreak paste preview print save searchreplace tabfocus template textpattern visualblocks visualchars wordcount table image toc',
export const toolbar = 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | toc alignleft aligncenter alignright alignjustify lineheight | outdent indent | numlist bullist | forecolor backcolor | pagebreak | charmap emoticons | fullscreen preview save print | hr link image | anchor pagebreak | insertdatetime | blockquote removeformat subscript superscript code codesample | searchreplace'
export const fontFormats = '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif,Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats'
三、 属性配置汇总
width: '100%',
height: '100%',
menubar: false,
branding: false,
statusbar: true,
readonly: false,
resize: false,
branding: false,
placeholder: '请输入内容',
theme: 'silver',
skin_url: '/tinymce/skins/ui/oxide',
icons: 'custom',
icons_url: '/tinymce/icons/icons.js',
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
content_css: `/tinymce/skins/content/default`,
content_style: 'body, p{font-size: 12px}',
plugins: ['autosave help textpattern lineheight'],
toolbar: 'fontselect styleselect fontsizeselect restoredraft undo redo | bold italic underline strikethrough subscript superscript removeformat forecolor backcolor lineheight align outdent indent help',
toolbar_mode: 'sliding',
toolbar_sticky: true,
quickbars_selection_toolbar: 'bold italic underline strikethrough | link h2 h3 h4 blockquote',
quickbars_insert_toolbar: 'quickimage quicktable',
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 26px 36px 48px 56px',
font_formats: "微软雅黑='微软雅黑'; 宋体='宋体'; 黑体='黑体'; 仿宋='仿宋'; 楷体='楷体'; 隶书='隶书'; 幼圆='幼圆'; 方正舒体='方正舒体'; 方正姚体='方正姚体'; 等线='等线'; 华文彩云='华文彩云'; 华文仿宋='华文仿宋'; 华文行楷='华文行楷'; 华文楷体='华文楷体'; 华文隶书='华文隶书'; Andale Mono=andale mono,times; Arial=arial; Arial Black=arial black;avant garde; Book Antiqua=book antiqua;palatino; Comic Sans MS=comic sans ms; Courier New=courier new;courier; Georgia=georgia; Helvetica=helvetica; Impact=impact;chicago; Symbol=symbol; Tahoma=tahoma;arial; sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms; Verdana=verdana;geneva; Webdings=webdings; Wingdings=wingdings",
autosave_ask_before_unload: true,
autosave_interval: '3s',
autosave_prefix: `editor_${route.path}`,
autosave_retention: '300m',
images_upload_handler: (blobInfo, success, failure) => {
success('xxxx')
},
image_advtab: true,
paste_data_images: true,
paste_block_drop: true,
paste_as_text: true,
paste_retain_style_properties: 'color border',
templates: [{ title: '标题', description: '描述', content: '内容' }],
textpattern_patterns: [
{ start: '*', end: '*', format: 'italic' },
{ start: '**', end: '**', format: 'bold' },
{ start: '#', format: 'h1' },
{ start: '##', format: 'h2' },
{ start: '###', format: 'h3' },
{ start: '####', format: 'h4' },
{ start: '#####', format: 'h5' },
{ start: '######', format: 'h6' },
{ start: '1. ', cmd: 'InsertOrderedList' },
{ start: '* ', cmd: 'InsertUnorderedList' },
{ start: '- ', cmd: 'InsertUnorderedList' }
],
init_instance_callback: editor => {
editor.on('Input undo redo Change execCommand SetContent', (e) => {
$emit('change', editor.getContent())
setup: (editor) => {
Crystal
前端打杂师 @ 小红书
16.6k
粉丝