@tdh-editor/mohai-vue2
v1.0.0
Published
🌸A modern WYSIWYG rich-text editor using tiptap and Element UI for Vue.js
Downloads
23
Maintainers
Readme
TDH Tiptap Editor
TDH Tiptap Editor 是一个基于 Tiptap 2、ProseMirror 和 Element UI 的 Vue 2 所见即所得富文本编辑器。
编辑器以扩展(Extension)为基本功能单元。通过扩展配置文档节点、文本样式、表格、图片、自动保存、导出和自定义菜单等功能,并通过 Tiptap Editor 实例扩展编辑行为。
特性
- Vue 2 和 TypeScript 支持,npm 包名为
@tdh-editor/mohai-vue2。 - HTML 和 ProseMirror JSON 内容格式。
- 菜单栏、气泡菜单、只读、全屏、预览和打印。
- 标题、列表、任务列表、引用、代码块、表格、链接、图片和 iframe。
- 字体、字号、文字颜色、文字高亮、对齐、缩进、行高和字间距。
- 搜索、格式清除、格式刷、范围高亮和流式内容更新。
- 本地图片粘贴、拖拽、网络地址插入和异步图片上传。
- 基于
localStorage的自动保存,以及自定义保存回调。 - Word(
.docx)导出、文件名配置和页边距配置。 - 中文(
zh)和英文(en)界面,默认语言为中文。 - 自定义 Extension、菜单按钮、菜单插槽、内容插槽和底部插槽。
兼容性
- Vue
^2.6.10 - Tiptap
2.x - 包管理器:pnpm
9.15.9 - Element UI 不包含在编辑器构建产物中。宿主项目应安装并注册 Element UI、tdh-element-ui 或其他提供兼容组件和服务 API 的组件库。
示例与本地开发
项目使用 pnpm workspace 管理编辑器源码和 examples 示例应用。
pnpm install
pnpm dev示例应用包含基础用法、扩展总览、气泡菜单、占位符、只读、标题、事件、内容输出、搜索、范围高亮、图片、自定义插槽和流式输出等页面。开发服务器启动后,访问终端输出的地址即可查看示例。
根目录脚本:
pnpm dev # 启动 examples 开发服务器
pnpm build:lib # 构建编辑器库到 lib/
pnpm build:example # 构建 examples 应用
pnpm build # 等同于 pnpm build:lib
pnpm lint # 检查 src 和 examples
pnpm lint:source # 只检查 src在 examples 目录执行以下命令,可单独操作示例应用:
pnpm --dir examples dev
pnpm --dir examples build
pnpm --dir examples lint安装
pnpm add @tdh-editor/mohai-vue2 element-ui采用 tdh-element-ui 或其他兼容实现时,应将组件库依赖和样式路径替换为对应包的配置。
全局注册
import Vue from 'vue';
import ElementUI, { MessageBox } from 'element-ui';
import { ElementTiptapPlugin } from '@tdh-editor/mohai-vue2';
import 'element-ui/lib/theme-chalk/index.css';
import '@tdh-editor/mohai-vue2/lib/index.css';
Vue.use(ElementUI);
// 图片上传和 iframe 插入功能使用 $prompt 服务。
Vue.prototype.$prompt = MessageBox.prompt;
Vue.use(ElementTiptapPlugin, {
lang: 'zh',
spellcheck: true,
});
// 完成注册后,可在组件中使用 <el-tiptap />。编辑器依赖以下 Element UI 全局组件。按需引入组件库时,应注册这些组件:
Button、Checkbox、ColDialogDropdown、DropdownMenu、DropdownItemForm、FormItemInput、InputNumberPopover、Tooltip、Upload
图片上传和 iframe 插入还依赖 Loading 服务以及 MessageBox.prompt。上述组件和服务必须使用与 ElementTiptapPlugin 相同的 Vue 实例注册。
局部注册
局部注册方式如下:
<template>
<el-tiptap
v-model="content"
:extensions="extensions"
/>
</template>
<script>
import {
Doc,
ElementTiptap,
Paragraph,
Text,
} from '@tdh-editor/mohai-vue2';
export default {
components: {
'el-tiptap': ElementTiptap,
},
data() {
return {
content: '<p>示例内容</p>',
extensions: [
Doc.configure(),
Text.configure(),
Paragraph.configure(),
],
};
},
};
</script>局部注册仅影响编辑器组件的注册方式,不改变 Element UI 组件的注册要求及语言、拼写检查的插件级默认配置。单实例配置可通过 lang、spellcheck 属性覆盖插件默认值。
基础用法
组件仅自动附加占位符和内容属性扩展,不会自动添加文档结构扩展。编辑器配置至少应包含 Doc、Text 和 Paragraph:
<template>
<el-tiptap
v-model="content"
:extensions="extensions"
placeholder="请输入内容..."
lang="zh"
/>
</template>
<script>
import {
Bold,
BulletList,
Doc,
Heading,
History,
Italic,
ListItem,
OrderedList,
Paragraph,
Text,
Underline,
} from '@tdh-editor/mohai-vue2';
export default {
data() {
return {
content: '<h2>TDH Tiptap Editor</h2><p>示例内容。</p>',
extensions: [
Doc.configure(),
Text.configure(),
Paragraph.configure(),
Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
Bold.configure(),
Underline.configure(),
Italic.configure(),
ListItem.configure(),
BulletList.configure(),
OrderedList.configure(),
History.configure(),
],
};
},
};
</script>菜单按钮的顺序由 extensions 中的扩展顺序决定。扩展默认显示在菜单栏中;配置 bubble: true 后,扩展菜单项可显示在气泡菜单中:
Bold.configure({ bubble: true });
// 仅显示在气泡菜单,不显示在菜单栏
Underline.configure({ bubble: true, menubar: false });扩展
所有扩展均从包入口导出,并通过 .configure(options) 配置。
文档与文本
| 扩展 | 说明 |
| --- | --- |
| Doc | 文档根节点 |
| Title | 文档标题节点;需配合 Doc.configure({ title: true }) |
| Text | 文本节点 |
| Paragraph | 段落 |
| Heading | 标题,默认支持 1 到 6 级 |
| Blockquote | 引用 |
| Code、CodeBlock | 行内代码和代码块 |
| HardBreak、HorizontalRule | 换行和水平分隔线 |
| TrailingNode | 在文档末尾保留可编辑段落 |
标题示例:
Doc.configure({ title: true }),
Title.configure({ placeholder: '文档标题' }),
Text.configure(),
Paragraph.configure(),列表、表格和媒体
| 扩展 | 依赖或说明 |
| --- | --- |
| ListItem、BulletList、OrderedList | 无序列表和有序列表的依赖组合 |
| TodoItem、TodoList | 任务列表的依赖组合;TodoItem.configure({ nested: true }) 开启嵌套任务列表 |
| Table、TableHeader、TableCell、TableRow | 表格的依赖组合;Table.configure({ resizable: true }) 开启列宽调整 |
| Image | 网络图片、本地图片、粘贴和拖拽图片 |
| Iframe | 插入 iframe |
图片异步上传:
Image.configure({
uploadRequest: async (file) => {
const response = await uploadImage(file);
return response.url;
},
});图片首先以 base64 形式插入编辑器。uploadRequest 返回最终 URL 后,编辑器将使用该 URL 替换临时地址;返回空字符串时保留 base64 图片,抛出异常时删除临时图片。
文本样式与段落格式
| 扩展 | 说明 |
| --- | --- |
| Bold、Italic、Underline、Strike | 常用文字样式 |
| Link | 链接,默认支持粘贴链接和在新标签页打开 |
| Color、TextColor | 文字颜色;示例中同时注册两个扩展 |
| TextHighlight | 多颜色文字高亮 |
| TextStyle、FontType、FontSize | 字体、字号和文字样式基础;字体或字号功能需要注册 TextStyle |
| TextAlign | 左对齐、居中、右对齐和两端对齐 |
| Indent | 增加或减少缩进 |
| LineHeight | 行高 |
| LetterSpacing | 字间距 |
字体和字号示例:
TextStyle.configure(),
FontType.configure({
customFonts: [
{ name: '宋体', value: 'SimSun' },
{ name: '黑体', value: 'SimHei' },
],
customFontMode: 'prepend', // prepend 或 replace
}),
FontSize.configure(),工具扩展
| 扩展 | 说明 |
| --- | --- |
| History | 撤销和重做 |
| FormatClear | 清除格式 |
| FormatPainter | 复制和应用格式 |
| Preview | 预览当前 HTML 内容 |
| Print | 打印当前内容,支持 Mod-p 快捷键 |
| Fullscreen | 全屏编辑模式 |
| SelectAll | 全选内容 |
| Search | 搜索文本、区分大小写、全词匹配和结果定位 |
| PageMargin | 调整编辑区和 Word 导出的页边距 |
| Export | 导出为 Word,格式为 .docx |
| CodeView | 基于 CodeMirror 编辑 HTML |
| HighlightRange | 按文本索引添加、移除、替换和定位范围高亮 |
CodeMirror 代码视图
CodeView 需要额外安装并传入 CodeMirror 实例:
pnpm add codemirrorimport codemirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/xml/xml.js';
import 'codemirror/addon/edit/closetag.js';
CodeView.configure({
codemirror,
codemirrorOptions: {
styleActiveLine: true,
autoCloseTags: true,
},
});搜索
注册 Search 后,通过 Tiptap Editor 实例调用搜索命令:
const matches = this.$refs.editor.editor.commands.search({
searchText: '关键字',
caseSensitive: false,
wholeWord: false,
index: 1,
});命令返回匹配范围数组;index 从 1 开始,超出有效范围时使用第一个匹配结果。
范围高亮
HighlightRange 使用文档纯文本索引定位范围:
const { commands } = this.$refs.editor.editor;
commands.addHighlight([
{
id: 'review-1',
startIndex: 0,
endIndex: 8,
highlightColor: '#ffff00',
},
]);
commands.scrollToHighlight('review-1');
commands.replaceHighlight({
id: 'review-1',
replaceText: '替换后的内容',
});
commands.removeHighlight('review-1');Props
以下属性由 el-tiptap 组件提供。除特别说明外,默认值以源码定义为准。
| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| extensions | Array | [] | Tiptap 扩展数组;应至少配置 Doc、Text、Paragraph |
| content | String | '' | 初始内容,支持通过 v-model 双向绑定 |
| contentType | 'html' \| 'json' \| 'markdown' | 'html' | 初始 content 的输入格式;设置为 markdown 时支持 Markdown 输入 |
| output | 'html' \| 'json' \| 'markdown' | 'html' | onUpdate、保存回调和 getEditorOutput 的输出格式;不影响 content 的输入解析 |
| placeholder | String | '' | 空内容时的占位文本 |
| editorProperties | Object | {} | 传入 Tiptap Editor 的其他配置,例如 editorProps |
| readonly | Boolean | false | 是否只读 |
| spellcheck | Boolean | 插件配置值 | 是否启用浏览器拼写检查 |
| tooltip | Boolean | true | 是否显示菜单按钮提示 |
| lang | String | 插件配置值(默认 zh) | 支持 zh 和 en |
| width、height | String \| Number | undefined | 编辑器尺寸;数字会按 px 处理 |
| showMenubar | Boolean | true | 是否显示菜单栏 |
| charCounterCount | Boolean | true | 是否显示字数统计 |
| menuBubbleOptions | Object | {} | 传入 Tiptap BubbleMenu 的配置,例如 shouldShow、pluginKey、tippyOptions |
| editorClass | String \| Array \| Object | undefined | 编辑器根节点 class |
| editorContentClass | String \| Array \| Object | undefined | 编辑器内容区域 class |
| editorMenubarClass | String \| Array \| Object | undefined | 菜单栏 class |
| editorBubbleMenuClass | String \| Array \| Object | undefined | 气泡菜单 class |
| editorFooterClass | String \| Array \| Object | undefined | 底部区域 class |
| autoSave | Boolean | false | 是否启用定时自动保存 |
| autoSaveInterval | Number | 300000 | 自动保存间隔,单位为毫秒 |
| storageKey | String | 'el-tiptap-autosave' | 默认 localStorage 保存键 |
| saveHandler | Function | undefined | 自定义保存回调,签名为 (content, isAutoSave) => void \| Promise<void> |
| downloadFileName | String | 时间戳文件名 | Export 导出 Word 时使用的文件名,扩展名会自动补全为 .docx |
editorProperties
通过 editorProperties.editorProps 配置 ProseMirror 编辑器属性:
<el-tiptap
:extensions="extensions"
:editor-properties="{
editorProps: {
handleKeyDown(view, event) {
console.log('keydown', event.key);
},
},
}"
/>组件会接管 content、editable、extensions、onFocus、onBlur、onPaste、onDrop、onTransaction 和 onUpdate。这些字段不应通过 editorProperties 覆盖,请使用组件 Props 或事件监听。
内容输出
默认输出格式为 HTML:
<el-tiptap
v-model="content"
:extensions="extensions"
output="html"
@onUpdate="handleUpdate"
/>配置为 JSON 输出:
<el-tiptap
v-model="content"
:extensions="extensions"
output="json"
/>output="json" 时,content、onUpdate 和保存相关 API 使用 JSON 字符串,而不是 JSON 对象。JSON 结构遵循 Tiptap/ProseMirror 文档结构。
配置 Markdown 输入时,Markdown 会先转换为 HTML,再交给当前 schema 解析;输入和输出格式可以分别配置:
<el-tiptap
v-model="content"
:extensions="extensions"
content-type="markdown"
output="html"
placeholder="Markdown 输入,HTML 输出"
/>也可以让输入和输出都使用 Markdown:
<el-tiptap
v-model="content"
:extensions="extensions"
content-type="markdown"
output="markdown"
placeholder="请输入 Markdown 内容..."
/>自定义节点会通过节点的 renderHTML 输出为 HTML,并在 Markdown 往返时保留。已有 CustomSlot 节点可直接在 Markdown 中使用原始 HTML,例如:
段落中的自定义节点:<span data-type="customSlot" data-slot-name="custom" data-xx="demo"></span>也可以单独使用导出的 markdownToHTML 和 htmlToMarkdown 方法。
自动保存与手动保存
启用 autoSave 后,编辑器按 autoSaveInterval 定时保存。未配置 saveHandler 时,内容保存到浏览器 localStorage:
<el-tiptap
ref="editor"
v-model="content"
:extensions="extensions"
:auto-save="true"
:auto-save-interval="60000"
storage-key="article-1001"
@onSave="handleSave"
/>启用自动保存时,默认页脚显示保存状态,并提供“从缓存中恢复”和“保存”操作。自动保存会跳过空文档和未发生变化的内容;手动调用 save() 仍执行保存。
业务接口场景下,通过 saveHandler 指定保存逻辑。回调返回 Promise 时,Promise resolve 后更新保存状态并触发 onSave:
<el-tiptap
ref="editor"
:content="content"
:extensions="extensions"
:save-handler="saveContent"
@onSave="handleSave"
/>export default {
methods: {
async saveContent(content, isAutoSave) {
await api.saveArticle({
id: this.articleId,
content,
autoSave: isAutoSave,
});
},
handleSave(content) {
console.log('保存成功', content);
},
},
};手动保存与获取编辑器输出:
await this.$refs.editor.save();
const output = this.$refs.editor.getEditorOutput();
const persistableOutput = this.$refs.editor.getEditorOutput(true);getEditorOutput(true) 和保存流程会移除搜索、范围高亮等临时标记,用于持久化或导出;不传参数时返回当前编辑器的完整输出。
Word 导出与页边距
注册 Export 扩展后,菜单栏提供 Word 导出功能。注册 PageMargin 后,可在编辑器中调整页边距,导出时使用当前页边距:
<template>
<el-tiptap
:extensions="extensions"
download-file-name="业务文书"
/>
</template>
<script>
import {
Doc,
Export,
PageMargin,
Paragraph,
Text,
} from '@tdh-editor/mohai-vue2';
export default {
data() {
return {
extensions: [
Doc.configure(),
Text.configure(),
Paragraph.configure(),
Export.configure(),
PageMargin.configure(),
],
};
},
};
</script>导出格式为 .docx。未设置 downloadFileName 时,文件名默认为 document_<timestamp>.docx。导出在浏览器端完成,不依赖服务端接口。
事件
事件按 Vue 事件语法监听:
| 事件 | 参数 | 说明 |
| --- | --- | --- |
| onInit | { editor } | Tiptap Editor 创建完成 |
| onTransaction | { editor, ...payload } | 文档事务更新 |
| onFocus | { editor, ...payload } | 编辑器获得焦点 |
| onBlur | { editor, ...payload } | 编辑器失去焦点 |
| onPaste | { editor, ...payload } | 粘贴内容 |
| onDrop | { editor, ...payload } | 拖放内容 |
| onUpdate | output, { editor, transaction } | 文档内容更新;output 受 output 属性影响 |
| onSave | output | 保存成功;output 为可持久化内容 |
示例:
<el-tiptap
:extensions="extensions"
@onInit="handleInit"
@onUpdate="handleUpdate"
@onSave="handleSave"
/>methods: {
handleInit({ editor }) {
this.editor = editor;
},
handleUpdate(output, { editor, transaction }) {
console.log(output, editor, transaction);
},
handleSave(output) {
console.log('saved:', output);
},
}插槽
menubar 和 menububble
menubar 和 menububble 插槽均提供 { editor, t }:
<el-tiptap :extensions="extensions">
<template #menubar="{ editor }">
<el-button
:type="editor.isActive('bold') ? 'primary' : ''"
@click="editor.chain().focus().toggleBold().run()"
>
加粗
</el-button>
</template>
<template #menububble="{ editor }">
<el-button @click="editor.chain().focus().toggleItalic().run()">
斜体
</el-button>
</template>
</el-tiptap>配置上述插槽后,对应区域渲染调用方提供的内容。气泡菜单仍可通过 menuBubbleOptions 配置行为。
footer
footer 插槽提供 { editor },用于替换默认的字数统计和保存状态区域:
<el-tiptap :extensions="extensions">
<template #footer="{ editor }">
<span>当前字数:{{ editor.state.doc.textContent.length }}</span>
</template>
</el-tiptap>MenuSlot 和 CustomSlot
MenuSlot 用于在内置菜单流程中插入指定名称的菜单插槽:
MenuSlot.configure({ slotName: 'extra-menu' });<el-tiptap :extensions="extensions">
<template #extra-menu="{ editor }">
<el-button @click="editor.commands.setParagraph()">
正文
</el-button>
</template>
</el-tiptap>CustomSlot 用于在文档中插入由 Vue 组件渲染的自定义内容节点。以下示例基于已有的基础扩展数组 extensions:
<template>
<el-tiptap :extensions="extensionsWithSlot">
<template #custom="{ node }">
<custom-component :data="node" />
</template>
</el-tiptap>
</template>
<script>
import { CustomSlot } from '@tdh-editor/mohai-vue2';
export default {
computed: {
extensionsWithSlot() {
return [...this.extensions, CustomSlot.configure()];
},
},
};
</script>自定义扩展和菜单
扩展可通过 menubar、bubble 和 menu 选项控制菜单位置及渲染组件。自定义扩展示例:
import { Bold } from '@tdh-editor/mohai-vue2';
import CustomButton from './CustomButton.vue';
export default Bold.extend({
addOptions() {
return {
...this.parent?.(),
menu: (editorContext) => ({
component: CustomButton,
componentProps: {
editorContext,
},
componentEvents: {
confirm: () => editorContext.editor.commands.toggleBold(),
},
}),
};
},
});此外,可使用内置的 CustomMenu 创建菜单按钮:
CustomMenu.configure({
menubar: true,
bubble: true,
menuItems: [
{
key: 'review',
label: '审查',
svgIcon: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2l2.9 6.2 6.8.8-5 4.7 1.4 6.7L12 17l-6.1 3.4 1.4-6.7-5-4.7 6.8-.8L12 2z" /></svg>',
},
],
onMenuClick({ key, selectedText, from, to }) {
console.log(key, selectedText, from, to);
},
});onMenuClick 接收菜单键、选中文本以及选区的 from、to 位置。完整示例参见 examples/views/CustomMenubar.vue 和 examples/views/BubbleMenu.vue。
插件配置与国际化
插件配置:
Vue.use(ElementTiptapPlugin, {
lang: 'en',
spellcheck: false,
});支持语言:
zh:默认语言en
单个编辑器可通过 lang 属性设置语言:
<el-tiptap lang="en" :extensions="extensions" />目录结构
src/ 编辑器组件、扩展、菜单和工具函数
examples/ Vue 2 示例应用
lib/ 构建后的 npm 包产物
types/ 补充的 TypeScript 类型声明