@norio-office/rich-text
v0.3.0
Published
A reusable Vue 3 rich text editor component built with Tiptap 3.
Maintainers
Readme
@norio-office/rich-text
个人开发,未全方位测试,用于生产请多测试一下 有BUG 请发邮件至:[email protected]
中文 | English
中文
@norio-office/rich-text 是一个基于 Vue 3 和 Tiptap 3 的富文本编辑器组件,内置文档式编辑界面、图片/视频/文件块、表格、公式、倒计时、高亮块、大纲、预览模式、导出能力。
安装
npm install @norio-office/rich-text组件样式会作为独立 CSS 文件发布。建议在应用入口显式引入:
import '@norio-office/rich-text/style.css'如果你的包管理器没有自动安装 peer dependencies,请同时安装运行时 peer 包:
npm install vue @tiptap/core @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-placeholder @tiptap/extension-code-block @tiptap/extension-code-block-lowlight @tiptap/extension-font-family @tiptap/extension-subscript @tiptap/extension-superscript @tiptap/extension-table @tiptap/extension-table-cell @tiptap/extension-table-header @tiptap/extension-table-row @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-text-style @tiptap/extension-underline lowlighthtml2canvas、jspdf、katex、marked、plyr 已作为普通 dependencies 随包安装。
快速开始
<script setup lang="ts">
import { ref } from 'vue'
import type { JSONContent } from '@tiptap/core'
import { RichTextEditor } from '@norio-office/rich-text'
import '@norio-office/rich-text/style.css'
const content = ref<JSONContent | null>(null)
</script>
<template>
<RichTextEditor v-model="content" />
</template>也可以使用默认导出:
import RichTextEditor from '@norio-office/rich-text'Props
| Prop | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| modelValue | JSONContent \| null | null | 编辑器内容,支持 v-model。 |
| documentName | string | '' | 内置导出下载的文件名基础名;组件会自动追加 .pdf、.png 或 .html。 |
| editable | boolean | true | 是否允许编辑。 |
| mode | 'edit' \| 'preview' | 'edit' | 编辑模式或预览模式。 |
| watermark | RichTextEditorWatermarkOptions \| null | null | 文档页面文本水印配置;不传或 text 为空时不显示水印。 |
| showOutline | boolean | true | 是否显示编辑器内置大纲。设为 false 后可以配合外部 RichTextOutline 组件自行放置目录。 |
| outlinePlacement | 'left' \| 'right' | 'right' | 大纲面板位置。 |
| messages | RichTextEditorMessages \| null | null | 覆盖内置 UI 文案。 |
| enabledExportItems | RichTextEditorExportItemKey[] \| null | null | 导出菜单白名单。 |
| enabledInsertMenuItems | RichTextEditorInsertMenuItemKey[] \| null | null | 插入菜单白名单。 |
| enabledToolbarActions | RichTextEditorToolbarActionKey[] \| null | null | 工具栏能力白名单。 |
| placeholder | string | '' | 空内容占位文案。 |
| mention | boolean | false | 是否启用 @ 提及功能;只有设为 true 时输入 @ 才会打开候选面板,insertMention() 才会插入提及节点。 |
| uploadImage | RichTextEditorUploadHook \| null | null | 内置图片选择、图片块上传时调用;返回最终可访问 URL 后组件插入图片。 |
| uploadVideo | RichTextEditorUploadHook \| null | null | 内置视频选择、视频块上传时调用;返回最终可访问 URL 后组件插入视频。 |
| uploadFile | RichTextEditorUploadHook \| null | null | 本地文件选择器上传时调用;返回最终可访问 URL 后组件插入文件卡片。 |
| onUploadError | RichTextEditorUploadErrorHandler \| null | null | 上传失败回调;同时也会触发 upload-error 事件。 |
| mentionProvider | RichTextEditorMentionProvider \| null | null | @ 提及候选数据提供函数,兼容用法。 |
| onMentionSearch | RichTextEditorMentionProvider \| null | null | @mention-search 函数式事件对应的异步候选加载函数。 |
白名单 props 不传时表示启用全部内置项;传入数组后,仅数组里的项目会显示并可用。
水印
watermark 只作为页面视觉层渲染,不写入编辑器 JSON,也不会影响 getText() 的纯文本结果。PDF、图片和打印导出会保留水印。
<RichTextEditor
v-model="content"
:watermark="{
text: '内部资料',
color: 'rgba(37, 99, 235, 0.12)',
fontSize: 20,
rotate: -24,
showInEdit: true,
}"
/>| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| text | string | - | 水印文字,必填。 |
| color | string | 'rgba(15, 23, 42, 0.12)' | 水印文字颜色。 |
| fontSize | number | 18 | 水印字号,单位 px。 |
| rotate | number | -24 | 水印倾斜角度,单位 deg。 |
| showInEdit | boolean | true | 编辑状态下是否显示水印;预览状态始终按配置显示。 |
<RichTextEditor
v-model="content"
:enabled-export-items="['html', 'image']"
:enabled-insert-menu-items="['image', 'local-file', 'blockquote']"
:enabled-toolbar-actions="['blockquote']"
/>可用 Key
导出菜单:
type RichTextEditorExportItemKey = 'pdf' | 'html' | 'image' | 'print'插入菜单:
type RichTextEditorInsertMenuItemKey =
| 'image'
| 'video'
| 'table'
| 'local-file'
| 'columns'
| 'highlight-block'
| 'date'
| 'code-block'
| 'formula'
| 'blockquote'
| 'emoji'
| 'link'
| 'divider'
| 'countdown'
| 'markdown-import'工具栏:
type RichTextEditorToolbarActionKey = 'blockquote'事件
| 事件 | Payload | 说明 |
| --- | --- | --- |
| update:modelValue | JSONContent | 内容更新,用于 v-model。 |
| change | JSONContent | 内容更新事件。 |
| local-file-upload | RichTextEditorLocalFilePayload | 用户通过本地文件选择器插入文件后触发。 |
| local-file-click | RichTextEditorLocalFilePayload | 用户点击本地文件卡片非下载按钮区域时触发。 |
| local-file-download | RichTextEditorLocalFilePayload | 用户点击本地文件卡片下载按钮时触发。 |
| upload-error | RichTextEditorUploadErrorPayload | 图片、视频或文件上传 hook 失败时触发。 |
| mention-search | RichTextEditorMentionProviderPayload | 函数式监听写法,对应 onMentionSearch prop;输入 @ 后加载候选数据,可返回数组或 Promise。 |
| mention-item-click | RichTextEditorMentionItem | 点击弹窗候选项或已插入的提及节点时触发。 |
| mention-submit | RichTextEditorMentionItem | 点击弹窗 提及 按钮并插入提及节点后触发。 |
mention-search 没有出现在组件的 defineEmits 中;Vue 模板里的 @mention-search="handler" 会作为 onMentionSearch 函数式 prop 传入组件。
@ 提及
mention 默认关闭。需要提及能力时请显式传入 :mention="true";未开启时输入 @ 只会保留普通文本,@mention-search 不会被调用,insertMention() 会返回 false。
输入 @ 会打开候选弹窗。宿主项目通过 @mention-search 异步返回候选数据,组件不会内置默认人员或文档数据。推荐使用 @mention-search,mentionProvider 仅作为函数 prop 兼容保留。
type: 1 表示人,type: 2 表示文档。人和文档共用 id、name、type、avatar、icon、tag、updatedAt 等字段;文档传入 tag 时会在标题旁显示标签文字,不传则不显示;人没有头像时会用名字最后两个字和固定亮色颜色池生成头像。
<script setup lang="ts">
import { ref } from 'vue'
import type { JSONContent } from '@tiptap/core'
import { RichTextEditor } from '@norio-office/rich-text'
import type {
RichTextEditorMentionItem,
RichTextEditorMentionProviderPayload,
} from '@norio-office/rich-text'
const content = ref<JSONContent | null>(null)
const mentionItems: RichTextEditorMentionItem[] = [
{ id: 'user-1001', name: '崔国强', type: 1 },
{ id: 'user-1002', name: '刘佳宁', type: 1 },
{ id: 'doc-2001', name: 'IT资产管理系统', type: 2, tag: '外部', updatedAt: '2025年12月1日' },
{ id: 'doc-2002', name: '客户满意度调研表', type: 2, updatedAt: '2025年12月4日' },
]
async function handleMentionSearch(payload: RichTextEditorMentionProviderPayload) {
const query = payload.query.trim().toLowerCase()
await new Promise((resolve) => window.setTimeout(resolve, 120))
return mentionItems.filter((item) => {
const matchedType = payload.type === 'all' || item.type === payload.type
const matchedKeyword = !query || item.name.toLowerCase().includes(query)
return matchedType && matchedKeyword
})
}
function handleMentionItemClick(item: RichTextEditorMentionItem) {
console.log('clicked mention item:', item)
}
function handleMentionSubmit(item: RichTextEditorMentionItem) {
console.log('inserted mention:', item)
}
</script>
<template>
<RichTextEditor
v-model="content"
:mention="true"
@mention-search="handleMentionSearch"
@mention-item-click="handleMentionItemClick"
@mention-submit="handleMentionSubmit"
/>
</template>点击候选项只会触发 mention-item-click 并选中当前项;点击底部 提及 按钮才会插入结构化提及节点并触发 mention-submit。关闭弹窗或不点击 提及 时,编辑器会保留普通 @ 文本。
自定义文案
编辑器默认 UI 文案为中文。你可以用 messages 覆盖任意 key:
<RichTextEditor
v-model="content"
:messages="{
'insert.localFile': '附件',
'export.label': '下载',
}"
/>常用文案 key:
| Key | 默认中文 |
| --- | --- |
| insert.label | 插入 |
| insert.section.general | 通用 |
| insert.section.apps | 小应用 |
| insert.section.external | 外部内容 |
| insert.image | 图片 |
| insert.video | 视频 |
| insert.table | 表格 |
| insert.localFile | 本地文件 |
| insert.columns | 分栏 |
| insert.highlightBlock | 高亮块 |
| insert.date | 日期 |
| insert.codeBlock | 代码块 |
| insert.formula | 公式 |
| insert.blockquote | 引用 |
| insert.emoji | 表情符号 |
| insert.link | 超链接 |
| insert.divider | 分隔线 |
| insert.countdown | 倒计时 |
| insert.markdownImport | Markdown 导入 |
| export.label | 导出 |
| export.pdf | 导出 PDF |
| export.pdf.loading | 导出 PDF 中... |
| export.html | 导出 HTML |
| export.html.loading | 导出 HTML 中... |
| export.image | 导出图片 |
| export.image.loading | 导出图片中... |
| print.label | 打印 |
| print.loading | 打印中... |
| quote.apply | 应用引用 |
| quote.cancel | 取消引用 |
| quote.borderColor | 边框颜色 |
| quote.backgroundColor | 背景颜色 |
| outline.label | 大纲 |
| outline.collapse | 收起大纲 |
| outline.empty.description | 对文档内容应用“标题”样式,即可自动生成大纲。 |
| outline.empty.tip | 点击左下角“大纲”按钮可以随时展开或收起。 |
| status.wordCountUnit | 个字 |
| status.presentation.enter | 演示 |
| status.presentation.exit | 退出演示 |
| status.fullscreen.enter | 全屏 |
| status.fullscreen.exit | 退出全屏 |
| countdown.selectTime | 请选择时间 |
| countdown.settingsTitle | 倒计时设置 |
| formula.insertTitle | 插入 LaTeX 公式 |
预览模式
mode="preview" 会切换到只读预览外壳。预览模式下工具栏隐藏,编辑禁用,大纲仍可放在左侧或右侧。
<RichTextEditor
v-model="content"
mode="preview"
outline-placement="right"
/>窄屏下预览模式会自动缩放页面画布,让文档在手机上保持可读。
实例 API
通过 Vue ref 获取组件实例方法:
<script setup lang="ts">
import { ref } from 'vue'
import { RichTextEditor } from '@norio-office/rich-text'
import type { RichTextEditorInstance } from '@norio-office/rich-text'
const editorRef = ref<RichTextEditorInstance | null>(null)
</script>
<template>
<RichTextEditor ref="editorRef" />
</template>可用方法:
| 方法 | 返回值 | 说明 |
| --- | --- | --- |
| exportPdf() | Promise<Blob \| null> | 导出 PDF。 |
| exportImage(options?) | Promise<Blob \| null> | 导出图片,支持 PNG/JPEG。 |
| exportHtml() | string \| null | 导出 HTML 字符串。 |
| insertImage(payload) | boolean | 插入 1 到 4 张图片。 |
| insertVideo(payload) | boolean | 插入视频块。 |
| insertFile(payload) | boolean | 插入链接/文件预览块。 |
| insertLocalFile(payload) | boolean | 插入本地文件卡片。 |
| insertMention(payload) | boolean | 插入一个结构化行内提及节点;mention 未开启时返回 false。 |
| openLocalFilePicker() | void | 打开本地文件选择器。 |
| focus() | void | 聚焦编辑器。 |
| getJSON() | JSONContent \| null | 获取当前 JSON 内容。 |
| getText() | string | 获取当前纯文本内容,不包含 HTML 标签。 |
| getImages() | RichTextEditorImagePayload[] | 获取文档内所有图片信息。 |
| getVideos() | RichTextEditorVideoPayload[] | 获取文档内所有视频信息。 |
| getFiles() | RichTextEditorCollectedFilePayload[] | 获取文档内所有文件信息;kind: 'file' 表示链接/文件预览块,kind: 'local-file' 表示本地文件卡片。 |
| getOutlineItems() | RichTextEditorOutlineItem[] | 获取当前目录项。 |
| getActiveOutlinePos() | number \| null | 获取当前激活目录项的文档位置。 |
| focusOutlineItem(pos) | boolean | 聚焦并跳转到指定目录项。 |
| onOutlineChange(handler) | () => void | 监听目录变化,返回取消监听函数。 |
外置目录组件
内置目录默认跟随编辑器一起渲染。如果需要把目录放到任意侧栏、抽屉或自定义布局里,可以关闭内置目录,并把编辑器实例传给 RichTextOutline。
<script setup lang="ts">
import { ref } from 'vue'
import { RichTextEditor, RichTextOutline } from '@norio-office/rich-text'
import type { RichTextEditorInstance } from '@norio-office/rich-text'
const editorRef = ref<RichTextEditorInstance | null>(null)
</script>
<template>
<RichTextEditor ref="editorRef" v-model="content" :show-outline="false" />
<aside class="outline-sidebar">
<RichTextOutline :editor="editorRef" />
</aside>
</template>RichTextOutline 会自动订阅编辑器目录状态,点击条目时会调用 focusOutlineItem 跳转到对应标题。组件自身最小高度为 200px,默认根据内容自适应增高;如果需要固定高度或内部滚动,请在外层套一个容器控制高度。可选 props:open、placement、title、collapseTitle、emptyDescription、emptyTip、showCollapse;事件:select、toggle、change。
上传并插入
推荐集成流程:
- 在业务层上传文件。
- 等待接口返回可访问 URL。
- 调用组件实例方法插入返回内容。
如果使用编辑器内置的图片、视频、本地文件选择器或页面拖拽上传,请传入上传 hook。编辑器只负责把用户选中的 File 交给业务侧上传,并在 hook 返回 URL 后插入内容;组件不会内置业务上传接口,也不会自动回退为 base64/blob URL。
页面拖拽上传会按文件类型分流:image/* 走图片上传,video/* 走视频上传,其他格式全部走 uploadFile 并插入本地文件卡片。
<script setup lang="ts">
import type {
RichTextEditorUploadInput,
RichTextEditorUploadResult,
RichTextEditorUploadErrorPayload,
} from '@norio-office/rich-text'
async function uploadToServer(payload: RichTextEditorUploadInput): Promise<RichTextEditorUploadResult> {
const form = new FormData()
form.append('file', payload.file)
form.append('kind', payload.kind)
const response = await fetch('/api/upload', {
method: 'POST',
body: form,
})
if (!response.ok) {
throw new Error('上传失败')
}
return await response.json()
}
function handleUploadError(payload: RichTextEditorUploadErrorPayload) {
console.error(payload.kind, payload.fileName, payload.error)
}
</script>
<template>
<RichTextEditor
v-model="content"
:upload-image="uploadToServer"
:upload-video="uploadToServer"
:upload-file="uploadToServer"
@local-file-click="(file) => console.log('file clicked', file)"
@upload-error="handleUploadError"
/>
</template>onUploadError callback prop 和 @upload-error 事件都能接收上传失败信息。通常选择一种接入方式即可,避免业务层重复处理。
插入图片:
async function uploadImage(file: File) {
const imageUrl = await yourUploadApi(file)
editorRef.value?.insertImage({
src: imageUrl,
name: file.name,
alt: file.name,
description: '',
})
}一次最多插入 4 张图片:
editorRef.value?.insertImage([
{ src: 'https://cdn.example.com/a.png', name: 'a.png' },
{ src: 'https://cdn.example.com/b.png', name: 'b.png' },
])插入视频:
async function uploadVideo(file: File) {
const videoUrl = await yourUploadApi(file)
editorRef.value?.insertVideo({
src: videoUrl,
name: file.name,
mimeType: file.type || 'video/mp4',
description: 'video description',
})
}插入文件链接/预览块:
async function uploadFile(file: File) {
const fileUrl = await yourUploadApi(file)
editorRef.value?.insertFile({
url: fileUrl,
name: file.name,
displayMode: 'text',
})
}插入本地文件卡片:
editorRef.value?.insertLocalFile({
url: 'https://cdn.example.com/files/demo.txt',
name: 'demo.txt',
size: 2048,
mimeType: 'text/plain',
})导出示例
async function handleExportPdf() {
const blob = await editorRef.value?.exportPdf()
if (!blob) return
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'demo.pdf'
link.click()
URL.revokeObjectURL(url)
}
async function handleExportImage() {
const blob = await editorRef.value?.exportImage({
type: 'image/png',
})
if (!blob) return
const url = URL.createObjectURL(blob)
window.open(url, '_blank')
}
function handleExportHtml() {
const html = editorRef.value?.exportHtml()
if (!html) return
console.log(html)
}类型导入
import type {
OfficeColorIconProps,
OfficeIconProps,
RichTextEditorAlign,
RichTextEditorCollectedFilePayload,
RichTextEditorExportItemKey,
RichTextEditorFilePayload,
RichTextEditorImageExportOptions,
RichTextEditorImagePayload,
RichTextEditorInsertMenuItemKey,
RichTextEditorInstance,
RichTextEditorLocalFilePayload,
RichTextEditorMentionItem,
RichTextEditorMentionProvider,
RichTextEditorMentionProviderPayload,
RichTextEditorMentionType,
RichTextEditorMessages,
RichTextEditorOutlineChangeHandler,
RichTextEditorOutlineItem,
RichTextEditorOutlineState,
RichTextEditorProps,
RichTextEditorToolbarActionKey,
RichTextEditorUploadErrorHandler,
RichTextEditorUploadErrorPayload,
RichTextEditorUploadHook,
RichTextEditorUploadInput,
RichTextEditorUploadKind,
RichTextEditorUploadResult,
RichTextEditorWatermarkOptions,
RichTextEditorVideoPayload,
} from '@norio-office/rich-text'包导出
import RichTextEditor, {
RichTextEditor as NamedRichTextEditor,
RichTextOutline,
OfficeIcon,
OfficeColorIcon,
ScrollArea,
monoIconNames,
colorIconNames,
} from '@norio-office/rich-text'包内文档
- 使用指南:
docs/usage.md
English
@norio-office/rich-text is a reusable rich text editor component for Vue 3 and Tiptap 3. It ships with a document-style editing UI, image/video/file blocks, tables, formulas, countdown blocks, highlight blocks, outline navigation, preview mode, export APIs.
Installation
npm install @norio-office/rich-textThe component styles are published as a separate CSS file. Import it in your application entry:
import '@norio-office/rich-text/style.css'If your package manager does not auto-install peer dependencies, install the runtime peer packages as well:
npm install vue @tiptap/core @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-placeholder @tiptap/extension-code-block @tiptap/extension-code-block-lowlight @tiptap/extension-font-family @tiptap/extension-subscript @tiptap/extension-superscript @tiptap/extension-table @tiptap/extension-table-cell @tiptap/extension-table-header @tiptap/extension-table-row @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-text-style @tiptap/extension-underline lowlighthtml2canvas, jspdf, katex, marked, and plyr are regular dependencies and are installed with the package.
Quick Start
<script setup lang="ts">
import { ref } from 'vue'
import type { JSONContent } from '@tiptap/core'
import { RichTextEditor } from '@norio-office/rich-text'
import '@norio-office/rich-text/style.css'
const content = ref<JSONContent | null>(null)
</script>
<template>
<RichTextEditor v-model="content" />
</template>Default import is also supported:
import RichTextEditor from '@norio-office/rich-text'Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| modelValue | JSONContent \| null | null | Editor content. Supports v-model. |
| documentName | string | '' | Base filename for built-in downloads. The component appends .pdf, .png, or .html. |
| editable | boolean | true | Enables or disables editing. |
| mode | 'edit' \| 'preview' | 'edit' | Edit mode or read-only preview mode. |
| watermark | RichTextEditorWatermarkOptions \| null | null | Text watermark configuration for the document page. No watermark is rendered when omitted or when text is empty. |
| showOutline | boolean | true | Shows the built-in outline panel. Set it to false when placing an external RichTextOutline component yourself. |
| outlinePlacement | 'left' \| 'right' | 'right' | Outline panel placement. |
| messages | RichTextEditorMessages \| null | null | Overrides built-in UI labels. |
| enabledExportItems | RichTextEditorExportItemKey[] \| null | null | Export menu whitelist. |
| enabledInsertMenuItems | RichTextEditorInsertMenuItemKey[] \| null | null | Insert menu whitelist. |
| enabledToolbarActions | RichTextEditorToolbarActionKey[] \| null | null | Toolbar action whitelist. |
| placeholder | string | '' | Placeholder text for empty content. |
| mention | boolean | false | Enables @ mention. Only when true, typing @ opens the candidate popup and insertMention() can insert a mention node. |
| uploadImage | RichTextEditorUploadHook \| null | null | Upload hook required by built-in image pickers and image drops. The editor inserts the returned URL. |
| uploadVideo | RichTextEditorUploadHook \| null | null | Upload hook required by built-in video pickers and video drops. The editor inserts the returned URL. |
| uploadFile | RichTextEditorUploadHook \| null | null | Upload hook required by the local file card picker. The editor inserts the returned URL. |
| onUploadError | RichTextEditorUploadErrorHandler \| null | null | Optional callback for upload errors. |
| mentionProvider | RichTextEditorMentionProvider \| null | null | Compatibility function prop for loading @ mention candidates. |
| onMentionSearch | RichTextEditorMentionProvider \| null | null | Async candidate loader used by the function-style @mention-search event. |
When whitelist props are omitted, all built-in options remain enabled. Once a list is passed, only listed items are visible and usable.
Watermark
watermark is rendered as a visual page layer only. It is not written into the editor JSON and does not affect getText(). PDF, image, and print exports keep the watermark.
<RichTextEditor
v-model="content"
:watermark="{
text: 'Internal',
color: 'rgba(37, 99, 235, 0.12)',
fontSize: 20,
rotate: -24,
showInEdit: true,
}"
/>| Field | Type | Default | Description |
| --- | --- | --- | --- |
| text | string | - | Watermark text. Required. |
| color | string | 'rgba(15, 23, 42, 0.12)' | Watermark text color. |
| fontSize | number | 18 | Watermark font size in px. |
| rotate | number | -24 | Watermark rotation in degrees. |
| showInEdit | boolean | true | Whether to show the watermark while editing. Preview mode still renders it when configured. |
<RichTextEditor
v-model="content"
:enabled-export-items="['html', 'image']"
:enabled-insert-menu-items="['image', 'local-file', 'blockquote']"
:enabled-toolbar-actions="['blockquote']"
/>Available Keys
Export menu:
type RichTextEditorExportItemKey = 'pdf' | 'html' | 'image' | 'print'Insert menu:
type RichTextEditorInsertMenuItemKey =
| 'image'
| 'video'
| 'table'
| 'local-file'
| 'columns'
| 'highlight-block'
| 'date'
| 'code-block'
| 'formula'
| 'blockquote'
| 'emoji'
| 'link'
| 'divider'
| 'countdown'
| 'markdown-import'Toolbar:
type RichTextEditorToolbarActionKey = 'blockquote'Events
| Event | Payload | Description |
| --- | --- | --- |
| update:modelValue | JSONContent | Content update event used by v-model. |
| change | JSONContent | Content update event. |
| local-file-upload | RichTextEditorLocalFilePayload | Emitted after a local file is picked and inserted. |
| local-file-click | RichTextEditorLocalFilePayload | Emitted when the user clicks the non-download area of a local file card. |
| local-file-download | RichTextEditorLocalFilePayload | Emitted when the local file card download button is clicked. |
| upload-error | RichTextEditorUploadErrorPayload | Emitted when an image, video, or file upload hook fails. |
| mention-search | RichTextEditorMentionProviderPayload | Function-style listener syntax for the onMentionSearch prop; loads candidates after the user types @; may return an array or a Promise. |
| mention-item-click | RichTextEditorMentionItem | Emitted when a popup candidate or inserted mention node is clicked. |
| mention-submit | RichTextEditorMentionItem | Emitted after the popup 提及 button inserts the selected mention. |
mention-search is not declared in the component's defineEmits. In Vue templates, @mention-search="handler" is passed to the component as the onMentionSearch function prop.
@ Mention
mention is disabled by default. Pass :mention="true" to enable it. When it is disabled, typing @ stays as plain text, @mention-search is not called, and insertMention() returns false.
Typing @ opens the mention popup when mention is enabled. The host application provides candidates with async @mention-search; the reusable editor does not ship built-in people or document data. Prefer @mention-search; mentionProvider remains available as a compatibility function prop.
type: 1 means person and type: 2 means document. People and documents share the same shape: id, name, type, avatar, icon, tag, updatedAt, and related metadata. When a document has tag, the popup displays that label next to the title; when it is omitted, no label is shown. When a person has no avatar, the editor displays the last two characters of the name on a stable color selected from a bright 20-color pool.
<script setup lang="ts">
import { ref } from 'vue'
import type { JSONContent } from '@tiptap/core'
import { RichTextEditor } from '@norio-office/rich-text'
import type {
RichTextEditorMentionItem,
RichTextEditorMentionProviderPayload,
} from '@norio-office/rich-text'
const content = ref<JSONContent | null>(null)
const mentionItems: RichTextEditorMentionItem[] = [
{ id: 'user-1001', name: 'Cui Guoqiang', type: 1 },
{ id: 'user-1002', name: 'Liu Jianing', type: 1 },
{ id: 'doc-2001', name: 'IT Asset Management System', type: 2, tag: 'External', updatedAt: '2025-12-01' },
{ id: 'doc-2002', name: 'Customer Satisfaction Survey', type: 2, updatedAt: '2025-12-04' },
]
async function handleMentionSearch(payload: RichTextEditorMentionProviderPayload) {
const query = payload.query.trim().toLowerCase()
await new Promise((resolve) => window.setTimeout(resolve, 120))
return mentionItems.filter((item) => {
const matchedType = payload.type === 'all' || item.type === payload.type
const matchedKeyword = !query || item.name.toLowerCase().includes(query)
return matchedType && matchedKeyword
})
}
function handleMentionItemClick(item: RichTextEditorMentionItem) {
console.log('clicked mention item:', item)
}
function handleMentionSubmit(item: RichTextEditorMentionItem) {
console.log('inserted mention:', item)
}
</script>
<template>
<RichTextEditor
v-model="content"
:mention="true"
@mention-search="handleMentionSearch"
@mention-item-click="handleMentionItemClick"
@mention-submit="handleMentionSubmit"
/>
</template>Clicking a candidate only selects it and emits mention-item-click; clicking the bottom 提及 button inserts the structured inline mention and emits mention-submit. Closing the popup without submitting keeps the typed @ as normal text.
Custom Labels
The default UI labels are Chinese. Use messages to override only the keys you need:
<RichTextEditor
v-model="content"
:messages="{
'insert.localFile': 'Attachment',
'export.label': 'Download',
}"
/>Common message keys:
| Key | Default Chinese | English meaning |
| --- | --- | --- |
| insert.label | 插入 | Insert |
| insert.section.general | 通用 | General |
| insert.section.apps | 小应用 | Apps |
| insert.section.external | 外部内容 | External content |
| insert.image | 图片 | Image |
| insert.video | 视频 | Video |
| insert.table | 表格 | Table |
| insert.localFile | 本地文件 | Local file |
| insert.columns | 分栏 | Columns |
| insert.highlightBlock | 高亮块 | Highlight block |
| insert.date | 日期 | Date |
| insert.codeBlock | 代码块 | Code block |
| insert.formula | 公式 | Formula |
| insert.blockquote | 引用 | Quote |
| insert.emoji | 表情符号 | Emoji |
| insert.link | 超链接 | Link |
| insert.divider | 分隔线 | Divider |
| insert.countdown | 倒计时 | Countdown |
| insert.markdownImport | Markdown 导入 | Markdown import |
| export.label | 导出 | Export |
| export.pdf | 导出 PDF | Export PDF |
| export.pdf.loading | 导出 PDF 中... | Exporting PDF... |
| export.html | 导出 HTML | Export HTML |
| export.html.loading | 导出 HTML 中... | Exporting HTML... |
| export.image | 导出图片 | Export image |
| export.image.loading | 导出图片中... | Exporting image... |
| print.label | 打印 | Print |
| print.loading | 打印中... | Printing... |
| quote.apply | 应用引用 | Apply quote |
| quote.cancel | 取消引用 | Cancel quote |
| quote.borderColor | 边框颜色 | Border color |
| quote.backgroundColor | 背景颜色 | Background color |
| outline.label | 大纲 | Outline |
| outline.collapse | 收起大纲 | Collapse outline |
| outline.empty.description | 对文档内容应用“标题”样式,即可自动生成大纲。 | Apply heading styles to generate an outline. |
| outline.empty.tip | 点击左下角“大纲”按钮可以随时展开或收起。 | Use the Outline button to show or hide the outline. |
| status.wordCountUnit | 个字 | characters |
| status.presentation.enter | 演示 | Present |
| status.presentation.exit | 退出演示 | Exit presentation |
| status.fullscreen.enter | 全屏 | Fullscreen |
| status.fullscreen.exit | 退出全屏 | Exit fullscreen |
| countdown.selectTime | 请选择时间 | Select time |
| countdown.settingsTitle | 倒计时设置 | Countdown settings |
| formula.insertTitle | 插入 LaTeX 公式 | Insert LaTeX formula |
Preview Mode
Use mode="preview" to switch the component into a read-only preview shell. In preview mode, the toolbar is hidden, editing is disabled, and the outline can be placed on either side.
<RichTextEditor
v-model="content"
mode="preview"
outline-placement="right"
/>On narrow screens, preview mode automatically scales the page canvas so the document stays readable on phones.
Instance API
Use a Vue ref to access the editor instance methods:
<script setup lang="ts">
import { ref } from 'vue'
import { RichTextEditor } from '@norio-office/rich-text'
import type { RichTextEditorInstance } from '@norio-office/rich-text'
const editorRef = ref<RichTextEditorInstance | null>(null)
</script>
<template>
<RichTextEditor ref="editorRef" />
</template>Available methods:
| Method | Return | Description |
| --- | --- | --- |
| exportPdf() | Promise<Blob \| null> | Exports a PDF. |
| exportImage(options?) | Promise<Blob \| null> | Exports an image as PNG or JPEG. |
| exportHtml() | string \| null | Exports an HTML string. |
| insertImage(payload) | boolean | Inserts 1 to 4 images. |
| insertVideo(payload) | boolean | Inserts a video block. |
| insertFile(payload) | boolean | Inserts a link/file preview block. |
| insertLocalFile(payload) | boolean | Inserts a local file card. |
| insertMention(payload) | boolean | Inserts one structured inline mention node; returns false when mention is disabled. |
| openLocalFilePicker() | void | Opens the local file picker. |
| focus() | void | Focuses the editor. |
| getJSON() | JSONContent \| null | Returns the current JSON document. |
| getText() | string | Returns the current plain text content without HTML tags. |
| getImages() | RichTextEditorImagePayload[] | Returns all images in the document. |
| getVideos() | RichTextEditorVideoPayload[] | Returns all videos in the document. |
| getFiles() | RichTextEditorCollectedFilePayload[] | Returns all files in the document. kind: 'file' means a link/file preview block, and kind: 'local-file' means a local file card. |
| getOutlineItems() | RichTextEditorOutlineItem[] | Returns the current outline items. |
| getActiveOutlinePos() | number \| null | Returns the document position of the active outline item. |
| focusOutlineItem(pos) | boolean | Focuses and scrolls to an outline item. |
| onOutlineChange(handler) | () => void | Subscribes to outline changes and returns an unsubscribe function. |
External Outline Component
The editor renders its built-in outline by default. To place the outline in any sidebar, drawer, or custom layout, hide the built-in panel and pass the editor instance to RichTextOutline.
<script setup lang="ts">
import { ref } from 'vue'
import { RichTextEditor, RichTextOutline } from '@norio-office/rich-text'
import type { RichTextEditorInstance } from '@norio-office/rich-text'
const editorRef = ref<RichTextEditorInstance | null>(null)
</script>
<template>
<RichTextEditor ref="editorRef" v-model="content" :show-outline="false" />
<aside class="outline-sidebar">
<RichTextOutline :editor="editorRef" />
</aside>
</template>RichTextOutline subscribes to the editor outline state automatically. Clicking an item calls focusOutlineItem on the editor instance. The component has a 200px minimum height and grows with its content by default; wrap it with your own container when you need a fixed height or internal scrolling. Optional props: open, placement, title, collapseTitle, emptyDescription, emptyTip, and showCollapse. Events: select, toggle, and change.
Upload And Insert
Recommended integration flow:
- Upload the asset from your own business layer.
- Wait for the API to return the final accessible URL.
- Call the editor instance method to insert the returned content.
The built-in image, video, and local-file pickers, as well as page-level drag
upload, follow the same boundary: the editor only passes the selected File to
uploadImage, uploadVideo, or uploadFile, then inserts the URL returned by
that hook. The package does not upload to a business API by itself and does not
fall back to base64/blob URLs.
Page-level drag upload routes files by MIME type: image/* uses image upload,
video/* uses video upload, and every other file type uses uploadFile and is
inserted as a local file card.
<script setup lang="ts">
import type {
RichTextEditorUploadInput,
RichTextEditorUploadResult,
RichTextEditorUploadErrorPayload,
} from '@norio-office/rich-text'
async function uploadToServer(payload: RichTextEditorUploadInput): Promise<RichTextEditorUploadResult> {
const form = new FormData()
form.append('file', payload.file)
form.append('kind', payload.kind)
const response = await fetch('/api/upload', {
method: 'POST',
body: form,
})
if (!response.ok) {
throw new Error('Upload failed')
}
return await response.json()
}
function handleUploadError(payload: RichTextEditorUploadErrorPayload) {
console.error(payload.kind, payload.fileName, payload.error)
}
</script>
<template>
<RichTextEditor
v-model="content"
:upload-image="uploadToServer"
:upload-video="uploadToServer"
:upload-file="uploadToServer"
@local-file-click="(file) => console.log('file clicked', file)"
@upload-error="handleUploadError"
/>
</template>Both the onUploadError callback prop and the @upload-error event can receive upload failures. In most apps, choose one style to avoid duplicate business handling.
Insert image:
async function uploadImage(file: File) {
const imageUrl = await yourUploadApi(file)
editorRef.value?.insertImage({
src: imageUrl,
name: file.name,
alt: file.name,
description: '',
})
}You can insert up to 4 images in one call:
editorRef.value?.insertImage([
{ src: 'https://cdn.example.com/a.png', name: 'a.png' },
{ src: 'https://cdn.example.com/b.png', name: 'b.png' },
])Insert video:
async function uploadVideo(file: File) {
const videoUrl = await yourUploadApi(file)
editorRef.value?.insertVideo({
src: videoUrl,
name: file.name,
mimeType: file.type || 'video/mp4',
description: 'video description',
})
}Insert file link or preview:
async function uploadFile(file: File) {
const fileUrl = await yourUploadApi(file)
editorRef.value?.insertFile({
url: fileUrl,
name: file.name,
displayMode: 'text',
})
}Insert a local file card:
editorRef.value?.insertLocalFile({
url: 'https://cdn.example.com/files/demo.txt',
name: 'demo.txt',
size: 2048,
mimeType: 'text/plain',
})Export Example
async function handleExportPdf() {
const blob = await editorRef.value?.exportPdf()
if (!blob) return
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'demo.pdf'
link.click()
URL.revokeObjectURL(url)
}
async function handleExportImage() {
const blob = await editorRef.value?.exportImage({
type: 'image/png',
})
if (!blob) return
const url = URL.createObjectURL(blob)
window.open(url, '_blank')
}
function handleExportHtml() {
const html = editorRef.value?.exportHtml()
if (!html) return
console.log(html)
}Type Imports
import type {
OfficeColorIconProps,
OfficeIconProps,
RichTextEditorAlign,
RichTextEditorCollectedFilePayload,
RichTextEditorExportItemKey,
RichTextEditorFilePayload,
RichTextEditorImageExportOptions,
RichTextEditorImagePayload,
RichTextEditorInsertMenuItemKey,
RichTextEditorInstance,
RichTextEditorLocalFilePayload,
RichTextEditorMentionItem,
RichTextEditorMentionProvider,
RichTextEditorMentionProviderPayload,
RichTextEditorMentionType,
RichTextEditorMessages,
RichTextEditorOutlineChangeHandler,
RichTextEditorOutlineItem,
RichTextEditorOutlineState,
RichTextEditorProps,
RichTextEditorToolbarActionKey,
RichTextEditorUploadErrorHandler,
RichTextEditorUploadErrorPayload,
RichTextEditorUploadHook,
RichTextEditorUploadInput,
RichTextEditorUploadKind,
RichTextEditorUploadResult,
RichTextEditorWatermarkOptions,
RichTextEditorVideoPayload,
} from '@norio-office/rich-text'Package Exports
import RichTextEditor, {
RichTextEditor as NamedRichTextEditor,
RichTextOutline,
OfficeIcon,
OfficeColorIcon,
ScrollArea,
monoIconNames,
colorIconNames,
} from '@norio-office/rich-text'Package Docs
- Usage guide:
docs/usage.md
