Skip to content

lezzhao/moku

Repository files navigation

Moku 墨块

core version ui version license

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>

Vue 中使用

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

React 中使用

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="输入 / 快速插入..."
    />
  )
}

📖 编辑器组件

<document-editor> 文档编辑器

飞书/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 }

API

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> 富文本编辑器

传统富文本编辑器,包含顶部工具栏。

<rich-text-editor
  placeholder="开始输入..."
  toolbar
  statusbar
></rich-text-editor>

属性

属性 类型 默认值 说明
placeholder string '' 占位文本
readonly boolean false 是否只读
toolbar boolean true 是否显示工具栏
statusbar boolean false 是否显示状态栏

🎯 Block 类型

类型 说明 斜杠命令
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)

🤝 协同编辑(CRDT)

使用 @moku/crdt 包启用实时协同编辑功能。

安装

pnpm add @moku/crdt yjs y-websocket

使用

import '@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 提供编辑器核心逻辑,可独立使用。

使用 Editor 类

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:packages

📄 License

MIT © 2024

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors