Moku(墨块) 是一个现代化的块级富文本编辑器,类似飞书/Notion 的编辑体验。基于 Web Components 构建,可在任何前端框架中使用。
- 🎨 飞书/Notion 风格 - 斜杠命令菜单、Block 操作手柄、悬浮工具栏
- 🧱 块级编辑 - 支持多种 Block 类型(段落、标题、列表、引用、代码等)
- 📦 Web Components - 框架无关,可在 Vue、React、Angular 等框架中使用
- 🔧 模块化架构 - 核心逻辑与 UI 分离,可独立使用
- 📝 富文本格式 - 粗体、斜体、下划线、删除线、行内代码、颜色
- ↩️ 撤销/重做 - 基于增量 Diff 的历史记录系统
- 📁 文件上传 - 内置图片、视频、音频上传支持
- 🤝 协同编辑 - 可选 CRDT 支持(需配合
@moku/crdt) - 🎯 TypeScript - 完整的类型定义
# pnpm (推荐)
pnpm add @moku/ui @moku/core
# npm
npm install @moku/ui @moku/core
# yarn
yarn add @moku/ui @moku/core<!DOCTYPE html>
<html>
<head>
<script type="module">
import '@moku/ui'
</script>
</head>
<body>
<!-- 飞书风格文档编辑器 -->
<document-editor placeholder="输入 / 快速插入..."></document-editor>
<!-- 传统富文本编辑器 -->
<rich-text-editor placeholder="开始输入..."></rich-text-editor>
</body>
</html><template>
<document-editor
ref="editorRef"
placeholder="输入 / 快速插入..."
@change="handleChange"
@selection-change="handleSelectionChange"
@ready="handleReady"
/>
</template>
<script setup lang="ts">
import '@moku/ui'
import { ref } from 'vue'
import type { DocumentEditor } from '@moku/ui'
const editorRef = ref<DocumentEditor>()
function handleReady() {
console.log('编辑器已就绪')
}
function handleChange(e: CustomEvent) {
console.log('内容变化:', e.detail.text)
console.log('Block 数量:', e.detail.blockCount)
}
function handleSelectionChange(e: CustomEvent) {
console.log('选区变化:', e.detail.selection)
}
// 获取/设置内容
function getContent() {
return editorRef.value?.getSnapshot()
}
function setContent(snapshot) {
editorRef.value?.setSnapshot(snapshot)
}
</script>import '@moku/ui'
import { useRef, useEffect } from 'react'
import type { DocumentEditor } from '@moku/ui'
// 声明 Web Component 类型
declare global {
namespace JSX {
interface IntrinsicElements {
'document-editor': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
placeholder?: string
readonly?: boolean
},
HTMLElement
>
}
}
}
function App() {
const editorRef = useRef<DocumentEditor>(null)
useEffect(() => {
const editor = editorRef.current
if (!editor) return
const handleChange = (e: CustomEvent) => {
console.log('内容变化:', e.detail)
}
editor.addEventListener('change', handleChange as EventListener)
return () => editor.removeEventListener('change', handleChange as EventListener)
}, [])
return (
<document-editor
ref={editorRef}
placeholder="输入 / 快速插入..."
/>
)
}飞书/Notion 风格的块级编辑器,包含斜杠菜单、Block 手柄、悬浮工具栏等高级功能。
<document-editor
placeholder="输入 / 快速插入..."
statusbar
></document-editor>| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
placeholder |
string |
'' |
占位文本 |
readonly |
boolean |
false |
是否只读 |
statusbar |
boolean |
true |
是否显示状态栏 |
| 事件 | 说明 | 详情 |
|---|---|---|
ready |
初始化完成 | { editor } |
change |
内容变化 | { text, blockCount, snapshot } |
selection-change |
选区变化 | { selection, attributes, selectedText, isCrossBlock } |
file-upload |
文件上传完成 | { result, fileInfo, renderedContent } |
const editor = document.querySelector('document-editor')
// 内容操作
editor.value // 获取纯文本
editor.value = 'Hello World' // 设置文本
editor.getSnapshot() // 获取完整快照
editor.setSnapshot(snapshot) // 设置快照
editor.clear() // 清空内容
// 编辑操作
editor.format({ bold: true }) // 格式化选区
editor.addBlock('heading1') // 添加 Block
editor.undo() // 撤销
editor.redo() // 重做
// 信息获取
editor.blockCount // Block 数量
editor.characterCount // 字符数
editor.canUndo // 是否可撤销
editor.canRedo // 是否可重做
// 焦点控制
editor.focus() // 聚焦编辑器传统富文本编辑器,包含顶部工具栏。
<rich-text-editor
placeholder="开始输入..."
toolbar
statusbar
></rich-text-editor>| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
placeholder |
string |
'' |
占位文本 |
readonly |
boolean |
false |
是否只读 |
toolbar |
boolean |
true |
是否显示工具栏 |
statusbar |
boolean |
false |
是否显示状态栏 |
| 类型 | 说明 | 斜杠命令 |
|---|---|---|
paragraph |
段落 | /text |
heading1 |
一级标题 | /h1 |
heading2 |
二级标题 | /h2 |
heading3 |
三级标题 | /h3 |
quote |
引用 | /quote |
code |
代码块 | /code |
list |
列表 | /list |
todo |
待办事项 | /todo |
image |
图片 | /image |
video |
视频 | /video |
audio |
音频 | /audio |
| 快捷键 | 功能 |
|---|---|
Ctrl/Cmd + B |
粗体 |
Ctrl/Cmd + I |
斜体 |
Ctrl/Cmd + U |
下划线 |
Ctrl/Cmd + Z |
撤销 |
Ctrl/Cmd + Shift + Z |
重做 |
Ctrl/Cmd + A |
选择当前 Block |
/ |
打开斜杠命令菜单 |
Enter |
新建段落 |
Backspace |
删除/合并 Block |
const editor = document.querySelector('document-editor')
// 配置自定义上传处理
editor.configureFileUpload({
uploadHandler: async (fileInfo, onProgress) => {
const formData = new FormData()
formData.append('file', fileInfo.file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await response.json()
return {
success: true,
url: data.url,
fileId: data.id,
}
},
onSuccess: (result, fileInfo) => {
console.log('上传成功:', result.url)
},
onError: (error, fileInfo) => {
console.error('上传失败:', error)
},
})// 上传图片
await editor.uploadFiles('image')
// 上传视频
await editor.uploadFiles('video')
// 上传任意文件
await editor.uploadFiles()
// 上传指定文件
await editor.uploadFile(file)使用 @moku/crdt 包启用实时协同编辑功能。
pnpm add @moku/crdt yjs y-websocketimport '@moku/crdt'
const editor = document.querySelector('document-editor')
// 启用 CRDT 模式
editor.enableCRDT({
websocketUrl: 'wss://your-server.com',
roomName: 'my-document',
user: {
name: '张三',
color: '#ff6b6b',
},
})
// 禁用 CRDT 模式
editor.disableCRDT()
// 检查状态
console.log(editor.isCRDTEnabled) // true/false@moku/core 提供编辑器核心逻辑,可独立使用。
import { Editor } from '@moku/core'
const editor = new Editor()
// 挂载到 DOM
editor.mount(document.getElementById('editor'))
// 文本操作
editor.insertText('block_id', 0, 'Hello')
editor.deleteText('block_id', 0, 5)
editor.formatText('block_id', 0, 5, { bold: true })
// Block 操作
editor.insertBlock(0, 'heading1')
editor.deleteBlock('block_id')
editor.setBlockType('block_id', 'quote')
editor.moveBlock('block_id', 2)
// 选区操作
editor.setCursor('block_id', 5)
editor.setSelection({ anchor: {...}, focus: {...} })
editor.formatSelection({ italic: true })
// 历史操作
editor.undo()
editor.redo()
// 序列化
const snapshot = editor.toJSON()
editor.fromJSON(snapshot)
// 事件订阅
const unsubscribe = editor.subscribe((event) => {
console.log('事件:', event.type, event.blockId)
})import type {
// Block 相关
BlockType,
BlockData,
// 文本相关
Attributes,
State,
// 选区相关
Selection,
SelectionRange,
Position,
// 编辑器相关
EditorSnapshot,
EditorEvent,
EditorCommand,
// 文件相关
FileData,
FileStorageType,
} from '@moku/core'@moku/
├── shared/ # 共享工具函数(内部包,不发布)
├── core/ # 核心逻辑(数据模型、历史记录、选区管理)
├── ui/ # UI 组件(Web Components)
└── crdt/ # CRDT 适配器(可选,协同编辑支持)
| 包 | 说明 | 发布 |
|---|---|---|
@moku/core |
核心编辑器逻辑 | ✅ |
@moku/ui |
Web Components UI | ✅ |
@moku/crdt |
CRDT 协同编辑 | ✅ |
@moku/shared |
共享工具函数 | ❌ 内部使用 |
# 安装依赖
pnpm install
# 启动开发服务器
pnpm play
# 构建所有包
pnpm build
# 测试
pnpm test
# 代码检查
pnpm lint
# 类型检查
pnpm typecheck# 构建所有包
pnpm build
# 发布到 npm(需要先登录)
pnpm publish:packagesMIT © 2024