minditor

minditor

专注开发与用户体验的块级富文本编辑器

Minditor是一款块级富文本编辑器,致力于提升开发效率和用户体验。该编辑器提供丰富的内置组件和插件,支持内联块插入和多框架组件开发。通过独特的插件系统,Minditor简化了开发流程,同时集成了Markdown命令、图片上传和代码编辑等功能,为用户呈现专业水准的编辑环境。

Minditor富文本编辑器插件系统Block组件InlineBlockGithub开源项目
<div style="text-align: center;padding: 10px;margin-bottom: 50px;"> <img src="https://yellow-cdn.veclightyear.com/835a84d5/5c608e5e-8ee1-4c10-a6ac-fa3cbd66f586.svg" /> <div id="badges"> <img alt="Static Badge" src="https://img.shields.io/badge/Microsoft_Edge-121.0-green"> <img alt="Static Badge" src="https://img.shields.io/badge/Chrome-122.0-green"> <img alt="Static Badge" src="https://img.shields.io/badge/Firefox-123.0-green"> <img alt="Static Badge" src="https://img.shields.io/badge/Safari-17.3-green"> <img alt="NPM Version" src="https://img.shields.io/npm/v/minditor"> <img alt="GitHub License" src="https://img.shields.io/github/license/minditor/minditor"> <a href="https://github.com/minditor/minditor"> <img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/minditor/minditor"> </a> </div> <div id="languages"> <a href="/README.md">English</a> <a href="/README-zh_CN.md">中文</a> </div> </div>

1. Minditor是什么

Minditor是一款注重终端用户编辑体验和开发者体验的块级富文本编辑器。它具有以下特点:

  1. 提供基础组件和插件,如图片上传和markdown命令,官方团队会持续完善。目标是默认提供与专业编辑工具相匹配的用户体验。
  2. 支持内联插入的InlineBlock。
  3. 编辑器的Block/InlineBlock可以使用任何框架的组件实现。
  4. 除了常见的事件和API外,插件系统还默认为开发者提供了一组重要的响应式状态,简化了插件的开发。

我们的官方网站首页就是使用Minditor制作和渲染的。你可以在首页上体验几乎所有的功能。快来看看吧。

<div> <img src="https://yellow-cdn.veclightyear.com/835a84d5/04f17fcd-9f12-416a-bd68-f2f7227fd65b.png" alt="Minditor截图"/> </div>

2. 编辑功能

2.1 复制/粘贴/剪切/重做/撤销

默认支持右键菜单和键盘快捷键进行复制、粘贴、剪切、撤销和重做操作。

2.2 markdown命令

Minditor提供以下命令以便快速编辑:

  • 输入#加空格插入标题,#的数量决定标题级别。
  • 在标题中输入1.加空格插入自动编号。
  • 在段落开头输入-加空格插入无序列表。
  • 在段落开头输入1.加空格插入有序列表。
  • 输入+[任意字符]+插入行内代码。
  • 在段落开头输入```+[语言名称]插入代码编辑器。

2.3 范围工具插件

鼠标悬停在选中的文本上会显示一个浮动工具栏,可以更改字体样式或创建链接。

<div> <img src="https://yellow-cdn.veclightyear.com/835a84d5/8f5cb61c-4a64-4641-ab64-9c71e9bda63f.png" alt="范围工具插件" width="1764"/> </div>

2.4 插入建议插件

输入"/"会触发一个下拉菜单,提供可插入的组件。如果在空行开头输入"/",可插入的Block和InlineBlock会同时出现。

<div> <img src="https://yellow-cdn.veclightyear.com/835a84d5/fe825465-ed7d-4481-bfd7-5c2b04a26730.png" alt="建议插件" width="1702"/> </div>

2.5 块工具插件

鼠标移到任何Block上会在左侧显示一个浮动工具栏,提供默认的删除、复制、粘贴和插入Block的工具。

2.6 目录插件

Minditor支持在文档外显示由标题组成的大纲,如官网首页左侧所示。

2.7 图片块

Minditor的图片组件允许用户将图片上传到指定接口或AWS S3存储桶,或者使用URL.createObjectURL将图片直接编码保存到文档中。Minditor已经包含了一个默认组件InlineImageBlock,用于将图片编码并保存到文档中。

<div> <img src="https://yellow-cdn.veclightyear.com/835a84d5/43f34d1d-db2a-40be-8396-98c6c55e9d4a.gif" alt="建议插件" width="978"/> </div> 你也可以使用`createImageBlock`并提供适当的参数来创建一个将图片上传到指定地址的组件:
import {createImageBlock} from 'minditor' // @params // - type: 'xhr'|'aws' // - config: Uppy.XHRConfig|Uppy.AWS3Config const ImageBlock = createImageBlock( 'xhr', {endpoint: 'https://your-domain.com/upload'} )

图片块使用Uppy作为上传工具。更多配置选项请参考https://uppy.io/docs/xhr-upload/

2.8 网格块

网格块中的单元格支持所有文档功能,相当于一个嵌入的小型文档。你可以在空段落使用左侧的块工具插入网格。也可以通过在段落开头输入"/"使用插入建议插件获取。网格支持的功能包括:

<div> <img src="https://yellow-cdn.veclightyear.com/835a84d5/70e1d6b8-d325-468c-aca7-dd9fc38b323d.png" alt="建议插件" width="752"/> </div>

我们目前正在开发一个更强大的电子表格工具,包括增强的布局和数据处理能力。这个工具未来也会作为一个组件集成到Minditor中,所以目前的网格没有大规模的增强计划。

2.9 代码块

Minditor使用CodeMirror 6作为其代码组件。默认支持以下语言:

• javascript (别名 js/jsx) • typescript (别名 ts/tsx) • python • php • cpp • java • sql • json • rust • css • html • sass • less • xml • yaml

你可以在段落开头输入 ``` + [语言名称]来插入特定语言的代码块。也可以使用块工具或建议工具插入。

3. 开发

3.1 安装和初始化

npm i minditor

Minditor本身可以在任何DOM节点中渲染,不需要任何外部布局或样式要求。 如果你只是用它来渲染最简单的文本,可以直接使用以下方法渲染:

import { Code, Grid, Heading, InlineImageBlock, InlineCode, Link, OLItem, Paragraph, Text, ULItem, InlineImageBlock } from "minditor";

const rootElement = document.getElementById('root')! const types = { Paragraph, Text, Heading, OLItem, ULItem, InlineCode, Code, Link, Grid, Image: InlineImageBlock } const data = { name: 'MyDoc', children: [{ type: 'Paragraph', content: [{type:'Text', value: '这是我的第一个文档。'}] }] }

const doc = new Document(rootElement, data, types) doc.render()

对于需要完整插件且视图高度自动适应外部容器的场景,我们建议使用`scaffold`创建视图:

```typescript

import {
    Code,
    createBlockTool,
    createRangeTool,
    createSuggestionTool,
    defaultBlockWidgets,
    defaultFormatWidgets,
    defaultMarkdownPlugins,
    defaultSuggestionWidgets,
    Grid,
    Heading,
    InlineImageBlock,
    InlineCode,
    Link,
    OLItem,
    Paragraph,
    scaffold,
    Text,
    ULItem,
    createTOCTool
} from "minditor";

const root= document.getElementById('root')!
const types = {
    Paragraph,
    Text,
    Heading,
    OLItem,
    ULItem,
    InlineCode,
    Code,
    Link,
    Grid,
    Image: InlineImageBlock
}

const plugins = [
    ...defaultMarkdownPlugins,
    createBlockTool(defaultBlockWidgets),
    createRangeTool( defaultFormatWidgets ),
    createSuggestionTool(defaultSuggestionWidgets),
    createTOCTool()
]
const result = scaffold(root, {data: jsonData, types, plugins}, { debug: true })
result.render()

3.2 开发InlineBlock

InlineBlock可以内联嵌入,开发InlineBlock非常简单。让我们以InlineCode为例:

export class InlineCode extends InlineComponent { static displayName = 'InlineCode' constructor(public data: InlineCodeData) { super(); } render() { return <span style={{display:'inline-block', background:'#eee', padding:'4px 8px'}}>{this.data.value}</span> } toText() { return this.data.value } }

有了InlineBlock后,需要提供用户使用的方法。有三种方式:

  1. 在Minditor的SuggestionTool中显示一个Widget。当用户输入/时,会出现一个下拉菜单,显示这个Block,用户可以点击使用。
  2. 实现一个Plugin,监听特定的用户输入,当输入满足某些条件时插入一个InlineBlock,类似markdown。

作为示例,让我们看看如何创建一个Range Tool的Widget,用于添加字体颜色和背景颜色:

class ColorWidget extends RangeWidget { static displayName = `ColorRangeWidget` useColor = (color: string) => { this.document.view.formatCurrentRange({color}) } useBackgroundColor = (backgroundColor: string) => { this.document.view.formatCurrentRange({backgroundColor}) } render() { const hover = atom(false) const pickerStyle = () => { return ({ display: hover() ? 'block' : 'none', position: 'absolute', top: '100%', left: 0, transform: 'translateX(-50%)', }) } const picker = ( <div style={pickerStyle}> <ColorPicker onColorClick={this.useColor} onBackgroundColorClick={this.useBackgroundColor}/> </div> ) return ( <div style={{display:'flex', position:'relative', width:24, height:24, alignItems: 'center',justifyContent: 'center'}} onmouseenter={() => hover(true)} onmouseleave={() => hover(false)} > <span style={{cursor: 'pointer',marginLeft:8, fontSize:18}}>A</span> {picker} </div> ) } }

有了这个widget,你可以通过将它传入createRangeTool函数来使用它:

const plugins = [ //... 其他插件 createRangeTool( [...defaultFormatWidgets, ColorWidget] ), ]

关于Plugin的开发,下面会提供更详细的示例。

3.3 开发Block

开发Block组件在本质上与开发Inline组件相似。不同之处在于如何使其可供用户使用;它被注册在BlockTool或Insert Suggestion Tool中供用户访问。注意,BlockTool中的Widget和Insert Suggestion Tool中的Widget是可以互换的。为了进一步简化Widget的开发,Minditor提供了一个createSuggestionWidget函数来协助快速开发widget。这里,我们以插入Code Widget为例:

type CommonInsertHandleProps = { insert: (initialData: InlineData|BlockData) => void } function CodeInsertHandle({insert}: CommonInsertHandleProps) { const onGridChange = (lang: string) => { insert({type: 'Code', language:lang, value: '', content: []}) activated(false) } const activated = atom(false) const languages = Object.keys(CodeBlock.langToPlugin) return ( <div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', cursor: 'pointer', width: '100%' }} onmouseenter={() => activated(true)} onmouseleave={() => activated(false)} > <Code size={18}/> <span style={{marginLeft: 8, fontSize: 14, whiteSpace:'nowrap'}}>+ 代码</span> <Right size={18}/> <div style={() => ({ display: activated() ? 'block' : 'none', position: 'absolute', left: 'calc(100% - 8px)', top: 0, transform: 'translateY(-50%)', paddingLeft: 18, background: 'transparent', })} > <div style={{ border: '1px solid #eee', background: '#fff', boxShadow: '2px 2px 5px #dedede', maxHeight: 400, overflowY: 'auto' }} > <CodeLanguagePicker onChange={onGridChange} languages={languages}/> </div> </div> </div> ) } const CodeBlockWidget = createSuggestionWidget(CodeInsertHandle, 'Code', true)

3.3.1 使用React开发Block

我们将演示如何使用React开发Block,以创建一个日历块为例。

import { Component } from 'minditor' import { createElement } from 'react'; import { createRoot } from 'react-dom/client'; class Calendar extends Component { static displayName = 'Calendar' render() { const container = document.createElement('div') container.contentEditable = 'false' const root = createRoot(container); root.render(createElement(DayPicker, {mode: 'single'})); return container } }

3.4 开发Plugin

在Minditor中,Plugin具有控制整个文档的能力,可以实现几乎任何需求。它可以根据用户输入执行一次性命令,也可以拥有自己独立的界面以支持更复杂的操作。

3.4.1 执行一次性命令

让我们以markdown添加标题的能力为例,看看如何开发一个Plugin。

class Plugin extends Plugin{ public static displayName = `createHeading` public static activateEvents = { inputChar: onInputKey(' ') } run({} : PluginRunArgv) : boolean | undefined{ const initialCharacters = '#' const { view, history } = this.document const startRange = view.state.selectionRange() const { startText, startBlock, isEndFull,isCollapsed, endText } = startRange! // 1. 标题只能在段落内容中生成。 if (!(startBlock instanceof Paragraph || startBlock instanceof Heading)) return false // 2. 只能在段落开头。 if (startBlock.firstChild !== startText) return false // 3. startText中的文本应该匹配# if (startText.data.value.slice(0, initialCharacters.length) !== initialCharacters) return false // 4. 删除最后的空格 const textToMatch = startText.data.value.slice(0, startText.data.value.length - 1) const matchedText = reversMatchStr(textToMatch, initialCharacters) if (matchedText === false) return false // 开始替换 history.openPacket(startRange) // 1. 删除startText中的initialCharacters和空格 const newTextAfterCursor = startText.data.value.slice(initialCharacters.length + 1) view.updateText(newTextAfterCursor, startText) // 2. 获取中间的所有文本 const titleTextFrag = view.deleteBetween(startText, null, startBlock) // 3. 用新的Heading块替换 const newBlock = createBlock.call(this, titleTextFrag) view.replace(newBlock, startBlock, this.document.content) view.setCursor(newBlock, 0) const endRange = new DocRange(newBlock, newBlock.firstChild!, 0, newBlock, newBlock.firstChild!, 0, ) history.closePacket(endRange) return true } }

3.4.2 渲染UI

插件使用一个轻量级的响应式前端框架Axii来渲染UI。开发者只需要记住以下基本用法:

  • atom()包裹需要响应式访问和操作的数据。
  • computed包裹计算数据。
  • 对于需要维护响应式DOM结构/属性,使用函数来表示它们,不需要computed包裹。

让我们以Range Tool为例,看看如何使用Axii和系统提供的响应式状态来构建一个响应式界面。

class OneRangeTool extends RangeTool { public rangeWidgets: RangeWidget[] constructor(public document: Document) { super(document); this.rangeWidgets = RangeWidgets.map(RangeWidgetClass => { return new RangeWidgetClass(this.document) }) } calculatePosition(outsideDocBoundary: boolean) { const {visibleRangeRect, lastMouseUpPositionAfterRangeChange, selectionRange, hasRange} = this.document.view.state const positionAttrs = { position: 'fixed' } as any positionAttrs.position = 'fixed' // 根据最后鼠标位置决定模态框位置 if (lastMouseUpPositionAfterRangeChange()!.top > (visibleRangeRect.raw!.top + visibleRangeRect.raw!.height / 2)) { positionAttrs.top = visibleRangeRect.raw!.top + visibleRangeRect.raw!.height positionAttrs.bottom = undefined } else { positionAttrs.top = undefined positionAttrs.bottom = -(visibleRangeRect.raw!.top) } positionAttrs.left = lastMouseUpPositionAfterRangeChange()!.left // 如果插件UI在内容同一个容器中,使用absolute定位 if(!outsideDocBoundary) { const boundaryRect = this.document.view.getContainerBoundingRect()! positionAttrs.position = 'absolute' if (lastMouseUpPositionAfterRangeChange()!.top > (visibleRangeRect.raw!.top + visibleRangeRect.raw!.height / 2)) { positionAttrs.top -= boundaryRect.top } else { positionAttrs.bottom += boundaryRect.top } positionAttrs.left -= boundaryRect.left } return positionAttrs } render(outsideDocBoundary: boolean) { const style = () => { const { lastMouseUpPositionAfterRangeChange, hasRange} = this.document.view.state if (!lastMouseUpPositionAfterRangeChange() || !hasRange()){ return {display: 'none'} } const positionAttrs = this.calculatePosition(outsideDocBoundary) return { display: 'block', ...positionAttrs, } } return <div style={style} data-testid="rangeTool-container"> <div style={{display:'flex', whiteSpace: 'nowrap'}}> {() => this.rangeWidgets.map((widget: RangeWidget) => { return widget.render() })} </div> </div> } }

3.4.3 内置响应式状态

系统提供以下内置响应式状态:

  • lastActiveDeviceType:用户最后活跃的设备,鼠标或键盘。
  • lastMouseUpPositionAfterRangeChange:选区变化后最后一次mouseup事件发生的位置。
  • mousePosition:鼠标当前位置。
  • selectionRange:以Minditor DocRange类型表示的选区对象。
  • hasRange:是否存在选区。
  • rangeBeforeComposition:输入法出现前的选区。
  • lastMouseEnteredBlock:最后一个触发mouseenter事件的Block。
  • visibleRangeRect:选区的boundRect对象。
  • bodyViewPortSize:body可视区域大小。

4. 路线图

4.1 自动完成插件

支持基于用户上下文的自动完成,为集成AI副驾驶提供基础。

4.2 文件上传块

更多的文件上传和预览类型。

4.3 数据表格

支持具有完整排序、筛选等功能的数据表格。

4.4 块级协同编辑

块级协同编辑,支持块级锁定和单独授权。

4.5 更多来自社区的需求

我们欢迎用户提交更多功能需求。

5. 支持

Minditor目前由Zhenyu Hou独立开发和维护。您的支持将用于雇佣开发人员继续维护这个项目并开发新功能。所有支持费用都用于开发开源、免费且供社区所有人使用的功能。 在https://patreon.com/sskyy赞助这个项目。

编辑推荐精选

GPT Plus|Pro充值

GPT Plus|Pro充值

GPT充值

支持 ChatGPT Plus / Pro 充值服务,支付便捷,自动发货,售后可查。

GPT Image 2中文站

GPT Image 2中文站

AI 图片生成平台

GPT Image 2 是面向用户的 AI 图片生成平台,支持文生图、图生图及多模型创意工作流。

Vecbase

Vecbase

你的AI Agent团队

Vecbase 是专为 AI 团队打造的智能工作空间,将数据管理、模型协作与知识沉淀整合于一处。算法、产品与业务在同一平台无缝协同,让从数据到 AI 应用的落地更快一步。

音述AI

音述AI

全球首个AI音乐社区

音述AI是全球首个AI音乐社区,致力让每个人都能用音乐表达自我。音述AI提供零门槛AI创作工具,独创GETI法则帮助用户精准定义音乐风格,AI润色功能支持自动优化作品质感。音述AI支持交流讨论、二次创作与价值变现。针对中文用户的语言习惯与文化背景进行专门优化,支持国风融合、C-pop等本土音乐标签,让技术更好地承载人文表达。

QoderWork

QoderWork

阿里Qoder团队推出的桌面端AI智能体

QoderWork 是阿里推出的本地优先桌面 AI 智能体,适配 macOS14+/Windows10+,以自然语言交互实现文件管理、数据分析、AI 视觉生成、浏览器自动化等办公任务,自主拆解执行复杂工作流,数据本地运行零上传,技能市场可无限扩展,是高效的 Agentic 生产力办公助手。

lynote.ai

lynote.ai

一站式搞定所有学习需求

不再被海量信息淹没,开始真正理解知识。Lynote 可摘要 YouTube 视频、PDF、文章等内容。即时创建笔记,检测 AI 内容并下载资料,将您的学习效率提升 10 倍。

AniShort

AniShort

为AI短剧协作而生

专为AI短剧协作而生的AniShort正式发布,深度重构AI短剧全流程生产模式,整合创意策划、制作执行、实时协作、在线审片、资产复用等全链路功能,独创无限画布、双轨并行工业化工作流与Ani智能体助手,集成多款主流AI大模型,破解素材零散、版本混乱、沟通低效等行业痛点,助力3人团队效率提升800%,打造标准化、可追溯的AI短剧量产体系,是AI短剧团队协同创作、提升制作效率的核心工具。

seedancetwo2.0

seedancetwo2.0

能听懂你表达的视频模型

Seedance two是基于seedance2.0的中国大模型,支持图像、视频、音频、文本四种模态输入,表达方式更丰富,生成也更可控。

nano-banana纳米香蕉中文站

nano-banana纳米香蕉中文站

国内直接访问,限时3折

输入简单文字,生成想要的图片,纳米香蕉中文站基于 Google 模型的 AI 图片生成网站,支持文字生图、图生图。官网价格限时3折活动

扣子-AI办公

扣子-AI办公

职场AI,就用扣子

AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!

下拉加载更多