Editable
— это расширяемый фреймворк для создания редактора богатого текста, который акцентирует внимание на стабильности, управляемости и производительности. Для достижения этих целей мы не использовали встроенное свойство contenteditable
, а вместо этого создали собственный рендерер, что позволяет нам лучше контролировать поведение редактора. Теперь вы можете не беспокоиться о проблемах совместимости между платформами и браузерами (например, Selection
, Input
), сосредоточившись на логике вашего бизнеса.
Вы можете просмотреть демонстрацию здесь: https://docs.editablejs.com/playground
Почему не используется рендеринг с помощью canvas
?
Хотя рендеринг с помощью canvas
может быть быстрее, чем рендеринг с помощью DOM, опыт разработки при помощи canvas
неудобен и требует написания большего количества кода.
Почему используется React
для рендера?
React
делает плагины более гибкими и имеет хорошую экосистему. Однако производительность React
не так высока, как у нативного DOM.
В идеальном фронтенде для богатого текста должны выполняться следующие условия:
diff
)Vue
, Solid-js
и SvelteJS
. Я обнаружил, что Solid-js
удовлетворяет первым двум требованиям, но каждый свойство обёрнут в прокси, что может вызвать проблемы при сравнении чистых объектов JavaScript с помощью ===
во время разработки расширений.Чтобы повысить производительность, мы вероятно будем рефакторить его для использования нативного рендера DOM в дальнейшем развитии.
На данный момент React
удовлетворяет следующим двум стандартам:
При последующем рефакторинге мы попробуем найти баланс между этими четырьмя стандартами.
На текущий момент вам всё ещё потребуется использовать
React
для данной версии, однако в будущих версиях мы планируем рефакторить его для использования нативного рендера DOM.
Установите зависимости @editablejs/models
и @editablejs/editor
:
npm i --save @editablejs/models @editablejs/editor
Вот минимальный пример текстового редактора, который можно редактировать:
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="Введите содержимое..." />
</EditableProvider>
);
};
```## Модель данных
`@editablejs/models` предоставляет модель данных для описания состояния редактора и операций над этим состоянием.
```ts
{
type: 'параграф',
children: [
{
type: 'текст',
text: 'Привет, мир!'
}
]
}
Как можно заметить, её структура очень схожа с Slate, и мы не создали новую модель данных, а использовали модель данных Slate напрямую и расширили её (добавили структуры данных и операции, связанные с сеткой и списками). Опираясь на эти зрелые и отличные структуры данных, наш редактор становится более стабильным.
Мы упаковали все API Slate в @editablejs/models
, так что вы можете найти все API Slate в @editablejs/models
.
Если вам незнакомо Slate, вы можете обратиться к его документации: https://docs.slatejs.org/
На данный момент мы предлагаем некоторые готовые плагины, которые реализуют базовые возможности, а также поддерживают горячие клавиши
, синтаксис Markdown
, сериализацию Markdown
, десериализацию Markdown
, сериализацию HTML
и десериализацию HTML
.### Общие плагины
@editablejs/plugin-context-menu
предоставляет контекстное меню правого щелчка мыши. Поскольку мы не используем некоторые функции встроенного контекстного меню contenteditable
, нам нужно определить свою собственную функциональность для контекстного меню правого щелчка.
@editablejs/plugin-align
для выравнивания текста@editablejs/plugin-blockquote
для цитат@editablejs/plugin-codeblock
для блоков кода@editablejs/plugin-font
включает цвет шрифта, фоновый цвет и размер шрифта@editablejs/plugin-heading
для заголовков@editablejs/plugin-hr
для горизонтальных линий@editablejs/plugin-image
для изображений@editablejs/plugin-indent
для отступов@editablejs/plugin-leading
для межстрочного интервала@editablejs/plugin-link
для ссылок@editablejs/plugin-list
включает нумерованные списки, маркированные списки и списки задач@editablejs/plugin-mark
включает жирный, курсив, код
@editablejs/plugin-mention
для упоминаний@editablejs/plugin-table
для таблицСпособ использования отдельного плагина с примером plugin-mark
:```tsx
import { withMark } from '@editablejs/mark'
const editor = React.useMemo(() => { const editor = withEditable(createEditor()) return withMark(editor) }, [])
Вы также можете использовать следующий метод для быстрого использования вышеупомянутых общих плагинов через `withPlugins` в `@editablejs/plugins`:
```tsx
import { withPlugins } from '@editablejs/plugins'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withPlugins(editor)
}, [])
Плагин @editablejs/plugin-history
предоставляет возможность отмены и повтора действий.
import { withHistory } from '@editablejs/plugin-history'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withHistory(editor)
}, [])
Когда вы разрабатываете приложения документов или блогов, обычно у вас есть отдельный заголовок и основной контент, который часто реализуется с помощью input
или textarea
, расположенных вне редактора. В совместно используемой среде, поскольку он независим от редактора, требуется дополнительная работа для обеспечения реального времени синхронизации заголовка.
Плагин @editablejs/plugin-title
решает эту проблему, используя первый дочерний узел редактора как заголовок, интегрируя его в общую структуру данных редактора таким образом, чтобы он имел те же возможности, что и сам редактор.
import { withTitle } from '@editablejs/plugin-title'
const editor = React.useMemo(() => {
const editor = withEditable(createEditor())
return withTitle(editor)
}, [])
У него также есть отдельное свойство заполнителя для установки заполнителя для заголовка.```tsx return withTitle(editor, { placeholder: 'Введите заголовок' })
### Плагин Yjs
Плагин `@editablejs/plugin-yjs` предоставляет поддержку для Yjs, что позволяет синхронизировать данные редактора в реальном времени с другими клиентами.
Необходимо установить следующие зависимости:
- yjs — основная библиотека Yjs
- @editablejs/yjs-websocket — библиотека Yjs для работы с WebSocket
Кроме того, она также предоставляет реализацию сервера Node.js, которую можно использовать для настройки службы Yjs:
```ts
import startServer from '@editablejs/yjs-websocket/server'
startServer()
@editablejs/plugin-yjs
— плагин Yjs для использования с редакторомnpm i yjs @editablejs/yjs-websocket @editablejs/plugin-yjs
import * as Y from 'yjs'
import { withYHistory, withYjs, YjsEditor, withYCursors, CursorData, useRemoteStates } from '@editablejs/plugin-yjs'
import { WebsocketProvider } from '@editablejs/yjs-websocket'
// Создаем документ yjs
const document = React.useMemo(() => new Y.Doc(), [])
// Создаем провайдера websocket
const provider = React.useMemo(() => {
return typeof window === 'undefined'
? null
: new WebsocketProvider(yjsServiceAddress, 'editable', document, {
connect: false,
})
}, [document])
// Создаем редактор
const editor = React.useMemo(() => {
// Получаем поле содержимого из документа yjs, которое является типом XmlText
const sharedType = document.get('content', Y.XmlText) as Y.XmlText
let editor = withYjs(withEditable(createEditor()), sharedType, { autoConnect: false })
if (provider) {
// Синхронизация курсора с другими клиентами
editor = withYCursors(editor, provider.awareness, {
data: {
name: 'Test User',
color: '#f00',
},
})
}
// Запись истории
editor = withHistory(editor)
// Запись истории yjs
editor = withYHistory(editor)
}, [provider])
```// Подключаемся к сервису yjs
React.useEffect(() => {
provider?.connect();
return () => {
provider?.disconnect();
};
}, [provider]);
Создание пользовательского плагина очень просто. Мы просто должны перехватить метод renderElement
, а затем определить, является ли текущий узел тем, который нам нужен. Если да, мы будем отображать наш пользовательский компонент.
import { Editable } from '@editablejs/editor';
import { Element, Editor } from '@editablejs/models';
// Определяем тип плагина
export interface MyPlugin extends Element {
type: 'my-plugin';
// ... Вы также можете определить другие свойства
}
``````javascript
export const MyPlugin = {
// Определяем, является ли узел плагином MyPlugin
isMyPlugin(editor: Editor, element: Element): element is MyPlugin {
return Element.isElement(value) && element.type === 'my-plugin';
}
};
``````markdown
const с_my_plugin = <T extends Изменяемый>(редактор: T) => {
const { является_пустым, отрисовка_элемента } = редактор;
// Перехват метода является_пустым. Если это узел для MyPlugin, вернуть true
// Кроме метода является_пустым, есть также методы такие как `является_блоком` `является_строкой`, которые можно перехватывать по мере необходимости.
редактор.является_пустым = (элемент) => {
return MyPlugin.isMyPlugin(редактор, элемент) || является_пустым(элемент);
};
// Перехват метода отрисовка_элемента. Если это узел для MyPlugin, отрисовать пользовательский компонент
// атрибуты - это атрибуты узла, нам нужно передать их в пользовательский компонент
// дети - это дочерние узлы узла, содержащие дочерние узлы узла. Мы должны их отрисовать
// элемент - это текущий узел, и вы можете найти свои пользовательские свойства в нем
редактор.отрисовка_элемента = ({ атрибуты, дети, элемент }) => {
if (MyPlugin.isMyPlugin(редактор, элемент)) {
return <div {...атрибуты}>
<div>My Plugin</div>
{дети}
</div>;
}
return отрисовка_элемента({ атрибуты, дети, элемент });
};
};
```<details>
<summary>Возврат редактора</summary>
<p>````tsx
return editor;
}
@editablejs/serializer
предоставляет сериализатор, который может сериализовать данные редактора в форматы html
, text
и markdown
.
Преобразователи сериализации для плагинов уже реализованы, поэтому вы можете использовать их напрямую.
// сериализатор HTML
import { HTMLSerializer } from '@editablejs/serializer/html';
// импортируем преобразователь сериализатора HTML плагина plugin-mark, а преобразователи других плагинов аналогичны
import { withMarkHTMLSerializerTransform } from '@editablejs/plugin-mark/serializer/html';
// используем преобразователь
HTMLSerializer.withEditor(editor, withMarkHTMLSerializerTransform, {});
// сериализуем в HTML
const html = HTMLSerializer.transformWithEditor(editor, { type: 'paragraph', children: [{ text: 'hello', bold: true }] });
// вывод: <p><strong>hello</strong></p>
// сериализатор текста
import { TextSerializer } from '@editablejs/serializer/text';
// импортируем преобразователь сериализатора текста плагина plugin-mention
import { withMentionTextSerializerTransform } from '@editablejs/plugin-mention/serializer/text';
// используем преобразователь
TextSerializer.withEditor(editor, withMentionTextSerializerTransform, {});
// сериализуем в текст
const text = TextSerializer.transformWithEditor(editor, { type: 'paragraph', children: [{ text: 'hello' }, {
type: 'mention',
children: [{ text: '' }],
user: {
name: 'User',
id: '1',
},
}] });
// вывод: hello @User
```tsx // сериализатор Markdown import { MarkdownSerializer } from '@editablejs/serializer/markdown' // импортируем преобразователь сериализатора Markdown плагина plugin-mark import { withMarkMarkdownSerializerTransform } from '@editablejs/plugin-mark/serializer/markdown' // используем преобразователь MarkdownSerializer.withEditor(editor, withMarkMarkdownSerializerTransform, {}) // сериализуем в Markdown const markdown = MarkdownSerializer.transformWithEditor(editor, { type: 'paragraph', children: [{ text: 'привет', bold: true }] }) // вывод: **привет** ```
import { withHTMLSerializerTransform } from '@editablejs/plugins/serializer/html'
import { withTextSerializerTransform } from '@editablejs/plugins/serializer/text'
import { withMarkdownSerializerTransform, withMarkdownSerializerPlugin } from '@editablejs/plugins/serializer/markdown'
useLayoutEffect(() => {
withMarkdownSerializerPlugin(editor)
withTextSerializerTransform(editor)
withHTMLSerializerTransform(editor)
withMarkdownSerializerTransform(editor)
}, [editor])
@editablejs/serializer
предоставляет десериализатор, который может преобразовать данные в форматах html
, text
и markdown
в данные редактора.
Преобразования десериализации для предоставленных плагинов уже реализованы, поэтому вы можете использовать их напрямую.
Использование аналогично сериализации, за исключением того, что путь к импортированию пакета следует изменить с @editablejs/serializer
на @editablejs/deserializer
.
Добро пожаловать 🌟 Звезды и 📥 PRs! Давайте будем работать вместе над созданием лучшего редактора.### Десериализация
@editablejs/serializer
предоставляет десериализатор, который может преобразовать данные в форматах html
, text
и markdown
в данные редактора.
Преобразования десериализации для предоставленных плагинов уже реализованы, поэтому вы можете использовать их напрямую.
Использование аналогично сериализации, за исключением того, что путь к импортированию пакета следует изменить с @editablejs/serializer
на @editablejs/deserializer
.
Добро пожаловать 🌟 Звезды и 📥 PRs! Давайте будем работать вместе над созданием лучшего редактора текста!
Руководство по внесению вклада находится здесь, пожалуйста, прочтите его. Если у вас есть хороший плагин, пожалуйста, поделитесь им с нами.
Особая благодарность компании Sparticle за её поддержку и вклад в сообщество открытого программного обеспечения.
Наконец, спасибо всем, кто вносил свой вклад в этот проект! (ключ эмодзи):
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://claviering.github.io/"><img src="https://avatars.githubusercontent.com/u/16227832?v=4?s=100" width="100px;" alt="Kevin Lin"/><br /><sub><b>Kevin Lin</b></sub></a><br /><a href="https://github.com/big-camel/Editable/commits?author=claviering" title="Код">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://yaokailun.github.io/"><img src="https://avatars.githubusercontent.com/u/11460856?v=4?s=100" width="100px;" alt="kailunyao"/><br /><sub><b>kailunyao</b></sub></a><br /><a href="https://github.com/big-camel/Editable/commits?author=YaoKaiLun" title="Код">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ren-chen2021"><img src="https://avatars.githubusercontent.com/u/88533891?v=4?s=100" width="100px;" alt="ren.chen"/><br /><sub><b>ren.chen</b></sub></a><br /><a href="https://github.com/big-camel/Editable/commits?author=ren-chen2021" title="Документация">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/byoungd"><img src="https://avatars.githubusercontent.com/u/16145783?v=4?s=100" width="100px;" alt="han"/><br /><sub><b>han</b></sub></a><br /><a href="https://github.com/big-camel/Editable/commits?author=byoungd" title="Документация">📖</a></td>
</tr>
</tbody>
</table><!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
```<!-- ALL-CONTRIBUTORS-LIST:END -->
Проект следует спецификации [all-contributors](https://github.com/all-contributors/all-contributors). Всякие виды вкладов приветствуются!
## Благодарности
Мы хотели бы поблагодарить следующие открытые проекты за их вклад:
- [Slate](https://github.com/ianstormtaylor/slate) — предоставляет поддержку для моделирования данных.
- [Yjs](https://github.com/yjs/yjs) — предоставляет базовую поддержку для CRDTs, используемых для совместной работы над редактированием.
- [React](https://github.com/facebook/react) — предоставляет поддержку для слоя представлений.
- [Zustand](https://github.com/pmndrs/zustand) — минимальный инструмент управления состоянием на фронтенде.
- [Другие зависимости](https://github.com/editablejs/editable/network/dependencies)
Мы используем следующие открытые проекты для помощи в создании лучшего процесса разработки:
- [Turborepo](https://github.com/vercel/turbo) — pnpm + turbo является отличным менеджером монорепозитория и системы сборки.
## Лицензия
Подробности см. в [LICENSE](https://github.com/editablejs/editable/blob/main/LICENSE).
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )