npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@norio-office/rich-text

v0.3.0

Published

A reusable Vue 3 rich text editor component built with Tiptap 3.

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 lowlight

html2canvasjspdfkatexmarkedplyr 已作为普通 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-searchmentionProvider 仅作为函数 prop 兼容保留。

type: 1 表示人,type: 2 表示文档。人和文档共用 idnametypeavataricontagupdatedAt 等字段;文档传入 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:openplacementtitlecollapseTitleemptyDescriptionemptyTipshowCollapse;事件:selecttogglechange

上传并插入

推荐集成流程:

  1. 在业务层上传文件。
  2. 等待接口返回可访问 URL。
  3. 调用组件实例方法插入返回内容。

如果使用编辑器内置的图片、视频、本地文件选择器或页面拖拽上传,请传入上传 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-text

The 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 lowlight

html2canvas, 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:

  1. Upload the asset from your own business layer.
  2. Wait for the API to return the final accessible URL.
  3. 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