Editable
🌱 A collaborative rich-text editor framework that focuses on stability, controllability, extensibility, and performance. 一款强到离谱的富文本编辑器框架,专注于稳定性、可控性、扩展性和性能。
Install / Use
/learn @editablejs/EditableREADME
Editable
Editable is an extensible rich text editor framework that focuses on stability, controllability, and performance. To achieve this, we did not use the native editable attribute ~~contenteditable~~, but instead used a custom renderer that allows us to better control the editor's behavior. From now on, you no longer have to worry about cross-platform and browser compatibility issues (such as Selection, Input), just focus on your business logic.
preview

You can see a demo here: https://docs.editablejs.com/playground
-
Why not use
canvasrendering?Although
canvasrendering may be faster than DOM rendering in terms of performance, the development experience ofcanvasis not good and requires writing more code. -
Why use
Reactfor rendering?Reactmakes plugins more flexible and has a good ecosystem. However, React's performance is not as good as native DOM.In my ideal frontend framework for rich text, it should be like this:
- No virtual DOM
- No diff algorithm
- No proxy object
Therefore, I compared frontend frameworks such as
Vue,Solid-js, andSvelteJSand found thatSolid-jsmeets the first two criteria, but each property is wrapped in aproxy, which may cause problems when comparing with pure JS objects using===during extension development.To improve performance, we are likely to refactor it for native DOM rendering in future development.
Currently, React meets the following two standards:
- [x] Development experience
- [x] Plugin extensibility
- [ ] Cross-frontend compatibility
- [ ] Rendering performance
In the subsequent refactoring selection, we will try to balance these four standards as much as possible.
Quick Start
Currently, you still need to use it with
Reactfor the current version, but we will refactor it for native DOM rendering in future versions.
Install @editablejs/models and @editablejs/editor dependencies:
npm i --save @editablejs/models @editablejs/editor
Here's a minimal text editor that you can edit:
import * as React from 'react'
import { createEditor } from '@editablejs/models'
import { EditableProvider, ContentEditable, withEditable } from '@editablejs/editor'
const App = () => {
const editor = React.useMemo(() => withEditable(createEditor()), [])
return (
<EditableProvider editor={editor}>
<ContentEditable placeholder="Please enter content..." />
</EditableProvider>)
}
Data Model
@editablejs/models provides a data model for describing the state of the editor and operations on the editor state.
{
type: 'paragraph',
children: [
{
type: 'text',
text: 'Hello World'
}
]
}
As you can see, its structure is very similar to Slate, and we did not create a new data model, but directly used Slate's data model and extended it (added Grid, List related data structures and operations). Depending on these mature and excellent data structures can make our editor more stable.
We have encapsulated all of Slate's APIs into @editablejs/models, so you can find all of Slate's APIs in @editablejs/models.
If you are not familiar with Slate, you can refer to its documentation: https://docs.slatejs.org/
Plugins
Currently, we provide some out-of-the-box plugins that not only implement basic functionality, but also provide support for keyboard shortcuts, Markdown syntax, Markdown serialization, Markdown deserialization, HTML serialization, and HTML deserialization.
Common Plugins
@editablejs/plugin-context-menuprovides a right-click menu. Since we do not use some of the functionality of the native contenteditable menu, we need to define our own right-click menu functionality.@editablejs/plugin-alignfor text alignment@editablejs/plugin-blockquotefor block quotes@editablejs/plugin-codeblockfor code blocks@editablejs/plugin-fontincludes font color, background color, and font size@editablejs/plugin-headingfor headings@editablejs/plugin-hrfor horizontal lines@editablejs/plugin-imagefor images@editablejs/plugin-indentfor indentation@editablejs/plugin-leadingfor line spacing@editablejs/plugin-linkfor links@editablejs/plugin-listincludes ordered lists, unordered lists, and task lists@editablejs/plugin-markincludesbold,italic,strikethrough,underline,superscript,subscript, andcode@editablejs/plugin-mentionfor mentions@editablejs/plugin-tablefor tables
The usage method of a single plugin, taking plugin-mark as an example:
import { withMark } from '@editablejs/mark'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withMark(editor)
}, [])
You can also use the following method to quickly use the above common plugins via withPlugins in @editablejs/plugins:
import { withPlugins } from '@editablejs/plugins'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withPlugins(editor)
}, [])
History Plugin
The @editablejs/plugin-history plugin provides undo and redo functionality.
import { withHistory } from '@editablejs/plugin-history'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withHistory(editor)
}, [])
Title Plugin
When developing document or blog applications, we usually have a separate title and main content, which is often implemented using an input or textarea outside of the editor. If in a collaborative environment, since it is independent of the editor, additional work is required to achieve real-time synchronization of the title.
The @editablejs/plugin-title plugin solves this problem by using the editor's first child node as the title, integrating it into the editor's entire data structure so that it can have the same features as the editor.
import { withTitle } from '@editablejs/plugin-title'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withTitle(editor)
}, [])
It also has a separate placeholder property for setting the placeholder for the title.
return withTitle(editor, {
placeholder: 'Please enter a title'
})
Yjs Plugin
The @editablejs/plugin-yjs plugin provides support for Yjs, which can synchronize the editor's data in real-time to other clients.
You need to install the following dependencies:
-
yjs The core library of Yjs
@editablejs/yjs-websocket Yjs websocket communication library
In addition, it also provides the implementation of the nodejs server, which you can use to set up a yjs service:
import startServer from '@editablejs/yjs-websocket/server' startServer() -
@editablejs/plugin-yjsYjs plugin used with the editor
npm i yjs @editablejs/yjs-websocket @editablejs/plugin-yjs
<details>
<summary>Instructions:</summary>
<p>
import * as Y from 'yjs'
import { withYHistory, withYjs, YjsEditor, withYCursors, CursorData, useRemoteStates } from '@editablejs/plugin-yjs'
import { WebsocketProvider } from '@editablejs/yjs-websocket'
// Create a yjs document
const document = React.useMemo(() => new Y.Doc(), [])
// Create a websocket provider
const provider = React.useMemo(() => {
return typeof window === 'undefined'
? null
: new WebsocketProvider(yjsServiceAddress, 'editable', document, {
connect: false,
})
}, [document])
// Create an editor
const editor = React.useMemo(() => {
// Get the content field from yjs document, which is of type XmlText
const sharedType = document.get('content', Y.XmlText) as Y.XmlText
let editor = withYjs(withEditable(createEditor()), sharedType, { autoConnect: false })
if (provider) {
// Synchronize cursors with other clients
editor = withYCursors(editor, provider.awareness, {
data: {
name: 'Test User',
color: '#f00',
},
})
}
// History record
editor = withHistory(editor)
// yjs history record
editor = withYHistory(editor)
}, [provider])
// Connect to yjs service
React.useEffect(() => {
provider?.connect()
return () => {
provider?.disconnect()
}
}, [provider])
</p>
</details>
Custom Plugin
Creating a custom plugin is very simple. We just need to intercept the renderElement method, and then determine if the current node is the one we need. If it is, we will render our custom component.
import { Editable } from '@editablejs/editor'
import { Element, Editor } from '@editablejs/models'
// Define the type of the plugin
export interface MyPlugin extends Element {
type: 'my-plugin'
// ... You can also define other properties
}
export const MyPlugin = {
// Determine if a node is a plugin for MyPlugin
isMyPlugin(editor: Editor, element: Element): element is MyPlugin {
return Element.isElement(value) && element.type === 'my-plugin'
}
}
export const withMyPlugin = <T extends Editable>(editor: T) => {
const { isVoid, renderElement } = editor
// Intercept the isVoid method. If it is a node for MyPlugin, return true
// Besides the isVoid method, there are also methods such as `isBlock` `isInline`, which can be intercepted as needed.
editor.isVoid = element => {
return MyPlugin.isMyPlugin(editor, element) || isVoid(element)
}
// Intercept the renderElement method. If it is a node for MyPlugin, render the custom component
// attributes are the attributes of the node, we need to pass it to the custom component
// children are the child nod
Related Skills
node-connect
347.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.0kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
