{"id":13571762,"url":"https://github.com/editablejs/editable","last_synced_at":"2025-04-04T08:32:16.848Z","repository":{"id":40430402,"uuid":"478852732","full_name":"editablejs/editable","owner":"editablejs","description":"🌱 A collaborative rich-text editor framework that focuses on stability, controllability, extensibility, and performance. 一款强到离谱的富文本编辑器框架，专注于稳定性、可控性、扩展性和性能。","archived":false,"fork":false,"pushed_at":"2024-04-21T15:20:18.000Z","size":4340,"stargazers_count":1753,"open_issues_count":45,"forks_count":115,"subscribers_count":16,"default_branch":"main","last_synced_at":"2024-05-23T05:39:05.690Z","etag":null,"topics":["editable","react-editor","rich-editor","slate-editor","text-editor"],"latest_commit_sha":null,"homepage":"https://docs.editablejs.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/editablejs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-04-07T06:19:28.000Z","updated_at":"2024-05-22T06:38:37.000Z","dependencies_parsed_at":"2023-12-15T10:05:02.273Z","dependency_job_id":"6c2de083-34d8-40b5-b516-9754bcaffe6a","html_url":"https://github.com/editablejs/editable","commit_stats":{"total_commits":500,"total_committers":9,"mean_commits":55.55555555555556,"dds":0.498,"last_synced_commit":"161179f3b7d8d1c325be63e375abbf9d76f71844"},"previous_names":[],"tags_count":779,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/editablejs%2Feditable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/editablejs%2Feditable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/editablejs%2Feditable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/editablejs%2Feditable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/editablejs","download_url":"https://codeload.github.com/editablejs/editable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246612590,"owners_count":20805398,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["editable","react-editor","rich-editor","slate-editor","text-editor"],"created_at":"2024-08-01T14:01:05.905Z","updated_at":"2025-04-04T08:32:11.839Z","avatar_url":"https://github.com/editablejs.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"[![zh-CN](https://img.shields.io/badge/lang-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-red.svg?longCache=true\u0026style=flat-square 'zh-CN')](README.zh-CN.md)\n\n# Editable\n\n`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~~](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/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.\n\n## preview\n![preview](/assets/preview.png)\n\nYou can see a demo here: https://docs.editablejs.com/playground\n\n\n---\n\n- Why not use `canvas` rendering?\n\n  Although `canvas` rendering may be faster than DOM rendering in terms of performance, the development experience of `canvas` is not good and requires writing more code.\n\n- Why use `React` for rendering?\n\n  `React` makes plugins more flexible and has a good ecosystem. However, React's performance is not as good as native DOM.\n\n  In my ideal frontend framework for rich text, it should be like this:\n\n  1. No virtual DOM\n  2. No diff algorithm\n  3. No proxy object\n\n  Therefore, I compared frontend frameworks such as `Vue`, `Solid-js`, and `SvelteJS` and found that `Solid-js` meets the first two criteria, but each property is wrapped in a `proxy`, which may cause problems when comparing with pure JS objects using `===` during extension development.\n\n  To improve performance, we are likely to refactor it for native DOM rendering in future development.\n\nCurrently, React meets the following two standards:\n\n- [x] Development experience\n- [x] Plugin extensibility\n- [ ] Cross-frontend compatibility\n- [ ] Rendering performance\n\nIn the subsequent refactoring selection, we will try to balance these four standards as much as possible.\n\n## Quick Start\n\n\u003e Currently, you still need to use it with `React` for the current version, but we will refactor it for native DOM rendering in future versions.\n\nInstall `@editablejs/models` and `@editablejs/editor` dependencies:\n\n```bash\nnpm i --save @editablejs/models @editablejs/editor\n```\n\nHere's a minimal text editor that you can edit:\n\n```tsx\nimport * as React from 'react'\nimport { createEditor } from '@editablejs/models'\nimport { EditableProvider, ContentEditable, withEditable } from '@editablejs/editor'\n\nconst App = () =\u003e {\n\n  const editor = React.useMemo(() =\u003e withEditable(createEditor()), [])\n\n  return (\n  \u003cEditableProvider editor={editor}\u003e\n    \u003cContentEditable placeholder=\"Please enter content...\" /\u003e\n  \u003c/EditableProvider\u003e)\n}\n```\n\n## Data Model\n\n`@editablejs/models` provides a data model for describing the state of the editor and operations on the editor state.\n\n```ts\n{\n  type: 'paragraph',\n  children: [\n    {\n      type: 'text',\n      text: 'Hello World'\n    }\n  ]\n}\n```\n\nAs you can see, its structure is very similar to [`Slate`](https://github.com/ianstormtaylor/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.\n\nWe have encapsulated all of Slate's APIs into `@editablejs/models`, so you can find all of Slate's APIs in @editablejs/models.\n\nIf you are not familiar with Slate, you can refer to its documentation: https://docs.slatejs.org/\n\n## Plugins\n\nCurrently, 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`.\n\n### Common Plugins\n\n- `@editablejs/plugin-context-menu` provides 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.\n- `@editablejs/plugin-align` for text alignment\n- `@editablejs/plugin-blockquote` for block quotes\n- `@editablejs/plugin-codeblock` for code blocks\n- `@editablejs/plugin-font` includes font color, background color, and font size\n- `@editablejs/plugin-heading` for headings\n- `@editablejs/plugin-hr` for horizontal lines\n- `@editablejs/plugin-image` for images\n- `@editablejs/plugin-indent` for indentation\n- `@editablejs/plugin-leading` for line spacing\n- `@editablejs/plugin-link` for links\n- `@editablejs/plugin-list` includes ordered lists, unordered lists, and task lists\n- `@editablejs/plugin-mark` includes `bold`, `italic`, `strikethrough`, `underline`, `superscript`, `subscript`, and `code`\n- `@editablejs/plugin-mention` for mentions\n- `@editablejs/plugin-table` for tables\n\nThe usage method of a single plugin, taking `plugin-mark` as an example:\n\n```tsx\nimport { withMark } from '@editablejs/mark'\n\nconst editor = React.useMemo(() =\u003e {\n  const editor = withEditable(createEditor())\n  return withMark(editor)\n}, [])\n```\n\nYou can also use the following method to quickly use the above common plugins via `withPlugins` in `@editablejs/plugins`:\n\n```tsx\nimport { withPlugins } from '@editablejs/plugins'\n\nconst editor = React.useMemo(() =\u003e {\n  const editor = withEditable(createEditor())\n  return withPlugins(editor)\n}, [])\n```\n\n### History Plugin\n\nThe `@editablejs/plugin-history` plugin provides undo and redo functionality.\n\n```tsx\nimport { withHistory } from '@editablejs/plugin-history'\n\nconst editor = React.useMemo(() =\u003e {\n  const editor = withEditable(createEditor())\n  return withHistory(editor)\n}, [])\n```\n\n### Title Plugin\n\nWhen 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.\n\nThe `@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.\n\n```tsx\nimport { withTitle } from '@editablejs/plugin-title'\nconst editor = React.useMemo(() =\u003e {\n  const editor = withEditable(createEditor())\n  return withTitle(editor)\n}, [])\n```\n\nIt also has a separate placeholder property for setting the placeholder for the title.\n\n```tsx\nreturn withTitle(editor, {\n  placeholder: 'Please enter a title'\n})\n```\n\n### Yjs Plugin\n\nThe `@editablejs/plugin-yjs` plugin provides support for Yjs, which can synchronize the editor's data in real-time to other clients.\n\nYou need to install the following dependencies:\n\n- yjs The core library of Yjs\n\n  @editablejs/yjs-websocket Yjs websocket communication library\n\n  In addition, it also provides the implementation of the nodejs server, which you can use to set up a yjs service:\n  ```ts\n  import startServer from '@editablejs/yjs-websocket/server'\n\n  startServer()\n  ```\n- `@editablejs/plugin-yjs` Yjs plugin used with the editor\n\n```bash\nnpm i yjs @editablejs/yjs-websocket @editablejs/plugin-yjs\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eInstructions:\u003c/summary\u003e\n\u003cp\u003e\n\n```tsx\nimport * as Y from 'yjs'\nimport { withYHistory, withYjs, YjsEditor, withYCursors, CursorData, useRemoteStates } from '@editablejs/plugin-yjs'\nimport { WebsocketProvider } from '@editablejs/yjs-websocket'\n\n// Create a yjs document\nconst document = React.useMemo(() =\u003e new Y.Doc(), [])\n// Create a websocket provider\nconst provider = React.useMemo(() =\u003e {\n  return typeof window === 'undefined'\n      ? null\n      : new WebsocketProvider(yjsServiceAddress, 'editable', document, {\n          connect: false,\n        })\n}, [document])\n// Create an editor\nconst editor = React.useMemo(() =\u003e {\n  // Get the content field from yjs document, which is of type XmlText\n  const sharedType = document.get('content', Y.XmlText) as Y.XmlText\n  let editor = withYjs(withEditable(createEditor()), sharedType, { autoConnect: false })\n  if (provider) {\n    // Synchronize cursors with other clients\n    editor = withYCursors(editor, provider.awareness, {\n      data: {\n        name: 'Test User',\n        color: '#f00',\n      },\n    })\n  }\n  // History record\n  editor = withHistory(editor)\n  // yjs history record\n  editor = withYHistory(editor)\n}, [provider])\n\n// Connect to yjs service\nReact.useEffect(() =\u003e {\n  provider?.connect()\n  return () =\u003e {\n    provider?.disconnect()\n  }\n}, [provider])\n\n```\n\u003c/p\u003e\n\u003c/details\u003e\n\n### Custom Plugin\n\nCreating 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.\n\n\u003cdetails\u003e\n  \u003csummary\u003eAn example of a custom plugin:\u003c/summary\u003e\n\u003cp\u003e\n\n```tsx\nimport { Editable } from '@editablejs/editor'\nimport { Element, Editor } from '@editablejs/models'\n\n// Define the type of the plugin\nexport interface MyPlugin extends Element {\n  type: 'my-plugin'\n  // ... You can also define other properties\n}\n\nexport const MyPlugin = {\n  // Determine if a node is a plugin for MyPlugin\n  isMyPlugin(editor: Editor, element: Element): element is MyPlugin {\n    return Element.isElement(value) \u0026\u0026 element.type === 'my-plugin'\n  }\n}\n\nexport const withMyPlugin = \u003cT extends Editable\u003e(editor: T) =\u003e {\n  const { isVoid, renderElement } = editor\n  // Intercept the isVoid method. If it is a node for MyPlugin, return true\n  // Besides the isVoid method, there are also methods such as `isBlock` `isInline`, which can be intercepted as needed.\n  editor.isVoid = element =\u003e {\n    return MyPlugin.isMyPlugin(editor, element) || isVoid(element)\n  }\n  // Intercept the renderElement method. If it is a node for MyPlugin, render the custom component\n  // attributes are the attributes of the node, we need to pass it to the custom component\n  // children are the child nodes of the node, which contains the child nodes of the node. We must render them\n  // element is the current node, and you can find your custom properties in it\n  editor.renderElement = ({ attributes, children, element }) =\u003e {\n    if (MyPlugin.isMyPlugin(editor, element)) {\n      return \u003cdiv {...attributes}\u003e\n        \u003cdiv\u003eMy Plugin\u003c/div\u003e\n        {children}\n        \u003c/div\u003e\n    }\n    return renderElement({ attributes, children, element })\n  }\n\n  return editor\n}\n```\n\u003c/p\u003e\n\u003c/details\u003e\n\n### Serialization\n\n`@editablejs/serializer` provides a serializer that can serialize editor data into `html`, `text`, and `markdown` formats.\n\nThe serialization transformers for the plugins provided have already been implemented, so you can use them directly.\n\n\u003cdetails\u003e\n\u003csummary\u003eHTML Serialization\u003c/summary\u003e\n\u003cp\u003e\n\n```tsx\n  // html serializer\nimport { HTMLSerializer } from '@editablejs/serializer/html'\n// import the HTML serializer transformer of the plugin-mark plugin, and other plugins are the same\nimport { withMarkHTMLSerializerTransform } from '@editablejs/plugin-mark/serializer/html'\n// use the transformer\nHTMLSerializer.withEditor(editor, withMarkHTMLSerializerTransform, {})\n// serialize to HTML\nconst html = HTMLSerializer.transformWithEditor(editor, { type: 'paragraph', children: [{ text: 'hello', bold: true }] })\n// output: \u003cp\u003e\u003cstrong\u003ehello\u003c/strong\u003e\u003c/p\u003e\n```\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eText Serialization\u003c/summary\u003e\n\u003cp\u003e\n\n```tsx\n// text serializer\nimport { TextSerializer } from '@editablejs/serializer/text'\n// import the Text serializer transformer of the plugin-mention plugin\nimport { withMentionTextSerializerTransform } from '@editablejs/plugin-mention/serializer/text'\n// use the transformer\nTextSerializer.withEditor(editor, withMentionTextSerializerTransform, {})\n// serialize to Text\nconst text = TextSerializer.transformWithEditor(editor, { type: 'paragraph', children: [{ text: 'hello' }, {\n  type: 'mention',\n  children: [{ text: '' }],\n  user: {\n    name: 'User',\n    id: '1',\n  },\n}] })\n// output: hello @User\n```\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMarkdown Serialization\u003c/summary\u003e\n\u003cp\u003e\n\n```tsx\n// markdown serializer\nimport { MarkdownSerializer } from '@editablejs/serializer/markdown'\n// import the Markdown serializer transformer of the plugin-mark plugin\nimport { withMarkMarkdownSerializerTransform } from '@editablejs/plugin-mark/serializer/markdown'\n// use the transformer\nMarkdownSerializer.withEditor(editor, withMarkMarkdownSerializerTransform, {})\n// serialize to Markdown\nconst markdown = MarkdownSerializer.transformWithEditor(editor, { type: 'paragraph', children: [{ text: 'hello', bold: true }] })\n// output: **hello**\n```\n\u003c/p\u003e\n\u003c/details\u003e\n\nEvery plugin requires importing its own serialization converter, which is cumbersome, so we provide the serialization converters for all built-in plugins in `@editablejs/plugins`.\n\n```tsx\nimport { withHTMLSerializerTransform } from '@editablejs/plugins/serializer/html'\nimport { withTextSerializerTransform } from '@editablejs/plugins/serializer/text'\nimport { withMarkdownSerializerTransform, withMarkdownSerializerPlugin } from '@editablejs/plugins/serializer/markdown'\n\nuseLayoutEffect(() =\u003e {\n  withMarkdownSerializerPlugin(editor)\n  withTextSerializerTransform(editor)\n  withHTMLSerializerTransform(editor)\n  withMarkdownSerializerTransform(editor)\n}, [editor])\n```\n\n### Deserialization\n\n`@editablejs/serializer` provides a deserializer that can deserialize data in `html`, `text`, and `markdown` formats into editor data.\n\nThe deserialization transformers for the plugins provided have already been implemented, so you can use them directly.\n\nThe usage is similar to serialization, except that the package path for importing needs to be changed from `@editablejs/serializer` to `@editablejs/deserializer`.\n\n## Contributors ✨\n\nWelcome 🌟 Stars and 📥 PRs! Let's work together to build a better rich text editor!\n\nThe [contributing guide](CONTRIBUTING.md) is here, please feel free to read it. If you have a good plugin, please share it with us.\n\nSpecial thanks to [Sparticle](https://www.sparticle.com) for their support and contribution to the open source community.\n[![sparticle](/assets/sparticle-logo.png)](https://www.sparticle.com)\n\nFinally, thank you to everyone who has contributed to this project! ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://claviering.github.io/\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/16227832?v=4?s=100\" width=\"100px;\" alt=\"Kevin Lin\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eKevin Lin\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/big-camel/Editable/commits?author=claviering\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://yaokailun.github.io/\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/11460856?v=4?s=100\" width=\"100px;\" alt=\"kailunyao\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003ekailunyao\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/big-camel/Editable/commits?author=YaoKaiLun\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/ren-chen2021\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/88533891?v=4?s=100\" width=\"100px;\" alt=\"ren.chen\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eren.chen\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/big-camel/Editable/commits?author=ren-chen2021\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/byoungd\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/16145783?v=4?s=100\" width=\"100px;\" alt=\"han\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003ehan\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/big-camel/Editable/commits?author=byoungd\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n\n## Thanks\n\nWe would like to thank the following open-source projects for their contributions:\n\n- [Slate](https://github.com/ianstormtaylor/slate) - provides support for data modeling.\n- [Yjs](https://github.com/yjs/yjs) - provides basic support for CRDTs, used for collaborative editing support.\n- [React](https://github.com/facebook/react) - provides support for the view layer.\n- [Zustand](https://github.com/pmndrs/zustand) - a minimal front-end state management tool.\n- [Other dependencies](https://github.com/editablejs/editable/network/dependencies)\n\nWe use the following open-source projects to help us build a better development experience:\n\n- [Turborepo](https://github.com/vercel/turbo) -- pnpm + turbo is a great monorepo manager and build system.\n\n## License\n\nSee [LICENSE](https://github.com/editablejs/editable/blob/main/LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feditablejs%2Feditable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feditablejs%2Feditable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feditablejs%2Feditable/lists"}