API редактора
createEditor() возвращает дескриптор IEditor. Он предоставляет операции над документом, ограниченный API блоков, типизированные подписки на события, корневой элемент и состояние жизненного цикла. Внутренние изменяемые диспетчеры намеренно недоступны.
Дескриптор редактора
interface IEditor {
save(): EditorDocument
render(data: EditorDocument): void
clear(): void
focus(): void
/** Восстановить ровно один предыдущий зафиксированный шаг истории. */
undo(): boolean
/** Повторно применить ровно один ранее отменённый шаг истории. */
redo(): boolean
setReadOnly(readOnly: boolean): void
insertInlinePlugin(type: string, data?: Record<string, string>): boolean
destroy(): void
readonly isReady: boolean
readonly readOnly: boolean
readonly canUndo: boolean
readonly canRedo: boolean
readonly blocks: EditorBlocksApi
readonly events: EditorEventSubscriptions
readonly rootElement: HTMLElement
}После destroy() свойство isReady остаётся доступным и возвращает false; повторный вызов destroy() безопасен. Обращение к любому другому свойству или методу выбрасывает ошибку Editor instance is destroyed.
save()
Сериализует все блоки, проверяет данные плагинов, переносит внутристрочные виджеты и возвращает отделённый EditorDocument. Сохранение выполняется синхронно, поскольку методы save() блочных плагинов синхронны. Ошибка сериализации или проверки выбрасывается вызывающей стороне.
try {
const document = editor.save()
await storage.put(document)
} catch (error) {
reportSaveFailure(error)
}При строгой проверке ошибочные данные блока приводят к исключению до возвращения документа. Ошибка save() плагина передаётся в диагностику, если она включена, и повторно выбрасывается вызывающей стороне. Если приложение сохраняет данные асинхронно, передайте возвращённый снимок в его асинхронное хранилище, как показано выше.
render(data)
Нормализует и целиком заменяет живой документ. Операция неделима: Rector подготавливает замену до её фиксации. Она создаёт один шаг истории и устанавливает каретку в восстановленную позицию, если ядро её передало, иначе — в конец первого блока. Метод предназначен для загрузки или выбора другого документа.
editor.render(documentFromStorage)Не вызывайте render() ради изменения одного блока. Используйте API блоков или контекст изменения владеющего плагина.
clear()
Заменяет документ одним пустым блоком типа defaultBlock, делает его текущим и создаёт один шаг истории. Метод не запрашивает и не восстанавливает фокус браузера. Если активный блок был в фокусе, замена его DOM может привести к потере фокуса; когда приложению нужно оставить фокус в редакторе, затем вызовите editor.focus(). Отмена восстанавливает прежний документ целиком, а повтор снова очищает его.
editor.clear()
editor.focus()focus()
Переносит фокус в текущий редактируемый блок. Сохранённые данные не меняются, запись истории не создаётся.
Управление историей
undo() восстанавливает предыдущий зафиксированный шаг документа, а redo() — следующий. Каждый метод возвращает true, только если восстановление действительно выполнено. Метод возвращает false, когда соответствующий стек пуст или редактор находится в режиме чтения. Свойства canUndo и canRedo сообщают ту же доступность, не меняя документ.
Доступность меняется сразу при вводе, не ожидая, пока tuning.undo.debounceMs закроет объединённый шаг. Новая ветвь ввода также немедленно делает повтор недоступным.
Чтобы синхронизировать кнопки приложения, подпишитесь на history:changed. Сразу после создания редактора прочитайте свойства один раз: исходное состояние сформировано ещё до того, как приложение может подписаться.
function syncHistory() {
undoButton.disabled = !editor.canUndo
redoButton.disabled = !editor.canRedo
}
undoButton.addEventListener('click', () => editor.undo())
redoButton.addEventListener('click', () => editor.redo())
const stopHistorySync = editor.events.on('history:changed', syncHistory)
syncHistory()Параметр tuning.undo.maxStack ограничивает число хранимых снимков. Новое изменение после отмены удаляет ветвь повтора. Границы транзакций и объединение последовательного ввода описаны в разделе Команды и история.
Режимы редактирования и чтения
Свойство readOnly сообщает текущий режим. setReadOnly(true) отключает интерактивное редактирование существующего экземпляра, а setReadOnly(false) снова подключает инфраструктуру редактирования. Передача уже установленного значения ничего не делает. Значение не логического типа приводит к TypeError.
editor.setReadOnly(true)
console.log(editor.readOnly) // true
editor.setReadOnly(false)При переходе Rector фиксирует ожидающий ввод текста, заново создаёт DOM плагинов с новым значением BlockMutationContext.readOnly, а затем подключает или освобождает служебные объекты редактирования. Документ и стеки истории сохраняются. Переход не создаёт шаг истории, не сообщает об изменении документа, не вызывает onChange, не запрашивает и не восстанавливает фокус браузера. Поскольку смонтированные элементы плагинов заменяются, текущий фокус может быть потерян; если после возврата в режим редактирования нужен фокус в редакторе, вызовите editor.focus(). После завершения отправляется событие readOnly:changed, затем history:changed, потому что в режиме чтения команды истории недоступны.
Режим чтения запрещает пользовательское редактирование, изменяющие элементы управления плагинов, вставку внутристрочных виджетов и восстановление истории. Разрешённые приложению методы документа (save(), render(), clear() и структурные команды editor.blocks) остаются доступными, чтобы приложение могло загрузить или заменить содержимое. Если жизненный цикл редактора и программное изменение документа не нужны, используйте рендерер документа.
Программные изменения документа, выполненные в режиме чтения, по-прежнему записываются в историю. До вызова setReadOnly(false) свойства canUndo и canRedo остаются равными false, а методы undo() и redo() недоступны. После возврата в режим редактирования записанные шаги можно отменять и повторять обычным образом.
Переход уничтожает и заново создаёт смонтированный элемент каждого плагина, поэтому прикладной код не должен хранить принадлежащие плагинам узлы DOM. Пользовательский плагин обязан освобождать связанные с элементом обработчики и ресурсы в destroy(element) и при каждом render(data, context) определять наличие интерактивных элементов по context.readOnly.
insertInlinePlugin(type, data?)
Вставляет зарегистрированный внутристрочный виджет в текущую позицию курсора или запускает его специальный процесс вставки. Возвращает false, если редактор находится в режиме чтения, тип неизвестен или подходящей позиции курсора нет.
const inserted = editor.insertInlinePlugin('mention', {
id: '42',
name: 'Ада',
})Точный набор ключей принадлежит плагину. Перед вызовом метода изучите страницу этого плагина.
rootElement
Возвращает корневой элемент Rector внутри контейнера. Он нужен для интеграции разметки, ограниченных стилями приложения областей и связей доступности. Не удаляйте, не переставляйте и не заменяйте дочерние узлы, принадлежащие Rector.
API блоков
editor.blocks содержит запросы и структурные команды. Возвращаемые EditorBlockView предоставляют идентификатор и безопасное состояние представления, но не опасные внутренние методы.
Запросы
| Метод | Результат |
|---|---|
getBlockByIndex(index) | блок по текущему индексу или undefined |
getBlockById(id) | блок с устойчивым идентификатором или undefined |
getCurrentBlock() | текущий блок или undefined |
getCurrentIndex() | текущий индекс; может быть равен -1, пока текущий блок не выбран |
getBlockCount() | количество блоков |
getBlockIndex(id) | текущий индекс заданного идентификатора или -1, если блока нет |
getSelectedBlocks() | представления выделенных блоков |
hasSelectedBlocks() | наличие блочного выделения |
API поддерживает перебор:
for (const block of editor.blocks) {
console.log(block.id, block.type)
}Перебор реализует открытый метод Symbol.iterator интерфейса EditorBlocksApi. Используйте показанный выше цикл for...of, а не вызывайте этот метод напрямую.
Команды выделения и фокуса
editor.blocks.setCurrentIndex(0)
editor.blocks.selectBlocks(['intro', 'body'])
editor.blocks.clearSelection()selectBlocks() и clearSelection() создают событие block:selected, только когда набор выбранных идентификаторов действительно изменился. Данные события содержат выбранные идентификаторы в порядке блоков документа. setCurrentIndex() меняет текущий блок для клавиатурных операций, но не меняет блочное выделение и не создаёт событие block:selected. EditorBlockView.focus() устанавливает фокус в блок, а isEmpty() использует правило пустоты соответствующего плагина.
Структурные команды
insert(type, data?, index?, id?, inline?): EditorBlockView | undefined
remove(index): void
move(fromIndex, toIndex): void
convert(index, type, data?): EditorBlockView | undefinedinsert() добавляет блок сразу после текущего, если index отсутствует; когда текущего блока нет, используется конец документа. Если id не передан, создаётся новый идентификатор. Карту inline следует передавать только при импорте уже сериализованных виджетов.
remove() и move() используют текущие индексы. Если порядок мог измениться, находите индекс по идентификатору непосредственно перед операцией.
convert() запрашивает экспортируемые данные исходного плагина, объединяет явно переданные данные и создаёт целевой плагин, сохраняя идентификатор блока при успешном преобразовании.
Каждый структурный метод уже является командой и создаёт отдельный шаг истории. Подробности приведены в разделе Команды и история.
Представление блока
interface EditorBlockView {
readonly id: string
readonly type: string
readonly element: HTMLElement
readonly contentElement: HTMLElement
readonly focused: boolean
readonly selected: boolean
readonly hasInlineTools: boolean
readonly canMerge: boolean
readonly version: number
focus(): void
isEmpty(): boolean
}element и contentElement предназначены для измерения и интеграции. Прикладной код не должен менять через них сохраняемое содержимое. version изменяется, когда Rector помечает блок изменённым; его можно использовать как признак наблюдения, но это не версия документа.
Подписки на события
editor.events предоставляет только on, off и once. on() и once() возвращают функцию отписки.
const stop = editor.events.on('block:moved', ({ blockId, from, to }) => {
console.log(blockId, from, to)
})
stop()Перечень событий
| Событие | Данные | Момент создания |
|---|---|---|
block:added | { blockId, index } | блок вставлен |
block:removed | { blockId, index } | блок удалён |
block:moved | { blockId, from, to } | блок перемещён |
block:converted | { blockId, from, to } | тип преобразован |
block:changed | { blockId } | изменение блока зафиксировано |
block:focused | { blockId } | фокус вошёл в блок |
block:blurred | { blockId } | фокус покинул блок |
block:selected | { blockIds } | пользователь или API блоков изменил выделение |
editor:ready | нет | сборка редактора завершена |
editor:willChange | нет | началась внешняя команда |
editor:changed | нет | изменение команды зафиксировано |
history:commit | нет | записан один шаг истории |
history:changed | { canUndo, canRedo } | доступность публичных команд истории изменилась |
readOnly:changed | { readOnly } | setReadOnly() завершил переход между режимами |
editor:destroyed | нет | освобождение ресурсов завершено |
toolbar:opened | { type } | открыта блочная или внутристрочная панель |
toolbar:closed | { type } | панель закрыта |
paste:applied | { startBlockId?, endBlockId? } | вставка из буфера зафиксирована |
dragHandle:clicked | нет | активирована рукоятка перетаскивания |
События создаются синхронно для наблюдения. Не выполняйте тяжёлую работу прямо в обработчике; планируйте её отдельно. Для сохранения используйте сериализованный документ из onChange.
Событие editor:ready создаётся во время сборки, до возвращения открытого дескриптора из createEditor(). Если приложению нужно уведомление о готовности, передайте onReady в конфигурации: подписка через editor.events после создания уже не увидит завершившееся событие.
Открытые утилиты
Корневая точка входа также экспортирует утилиты для пользовательских интеграций и расширений:
| Экспорт | Назначение |
|---|---|
uid() | создать шестизначный идентификатор из криптографически стойкой случайности браузера |
sanitizeHtml(html) | оставить поддерживаемое Rector подмножество внутристрочного форматирования и удалить остальную разметку |
escapeHtml(text) | преобразовать обычный текст в безопасную для HTML строку |
DocumentSchema | нормализовать и перенести между версиями EditorDocument без создания редактора |
InlinePluginRegistry | низкоуровневый реестр для собственной сборки редактора |
createDefaultInlineTools(options?) | создать набор встроенных инструментов форматирования |
createColorSwatchPlugin() | создать внутристрочный плагин образца цвета |
createMentionPlugin(options?) | создать внутристрочный плагин упоминания |
uid() создаёт возможный идентификатор; приложение, назначающее идентификаторы вне редактора, всё равно проверяет их уникальность в документе. Если разметка не требуется, используйте textContent. sanitizeHtml() ограничен моделью форматирования Rector и не заменяет проверку прав доступа.
В обычной интеграции не нужно создавать InlinePluginRegistry. Передавайте объекты через inlinePlugins, чтобы регистрацией и освобождением ресурсов владел createEditor(). В реестре, созданном вручную, типы плагинов и символы запуска должны быть уникальны, а владелец обязан вызвать destroy().
Фабрики расширений доступны из корневой точки входа для удобства. Импорты из отдельных подпутей, приведённые в документации расширений, предпочтительны, если приложению нужна наиболее узкая граница зависимости. См. раздел «Внутристрочные инструменты и плагины».
Параметры DocumentSchema описаны в Формате документа, а границы очистки — в разделе Безопасность и жизненный цикл.
Точки входа для типизации
Для контрактов, которые не должны попадать в итоговый исполняемый код, используйте import type:
import type {
EditorConfig,
EditorDocument,
IEditor,
} from '@shelamkoff/rector'
import type {
EditorTuning,
InlinePlugin,
InlinePluginContext,
} from '@shelamkoff/rector/types'@shelamkoff/rector — обычная точка входа приложения, которая также экспортирует поддерживаемые типы редактора. @shelamkoff/rector/core предоставляет тот же исполняемый редактор без вспомогательных экспортов корневой точки. @shelamkoff/rector/types — точка входа только для типов, предназначенная авторам расширений и сложных адаптеров; исполняемого кода в ней нет.
Открытые декларации разделены по назначению:
| Область | Основные контракты |
|---|---|
| документ и конфигурация | EditorDocument, BlockData, EditorConfig, EditorTuning, DocumentMigration |
| диагностика и проверка | EditorDiagnostic, EditorDiagnosticCode, DiagnosticThresholds, BlockValidationIssue |
| блочные расширения | BasePlugin, BlockPlugin, BlockMutationContext, BlockPluginConstructor, PluginRuntimeConfig, ToolboxEntry, PasteConfig, PasteEvent, TagPasteEvent, FilePasteEvent, PatternPasteEvent, ShortcutEntry, InlineControlContext, InlineControlGroup |
| внутристрочные расширения | InlineTool, InlineToolActionContext, InlineMutationContext, InlinePlugin, InlinePluginContext, InlineSelection, IInlinePluginRegistry |
| интеграция с приложением | IEditor, EditorBlocksApi, EditorBlockView, EditorEventSubscriptions, EditorEvents, CaretPosition |
| сложная сборка | IBlock, IBlockReader, IBlockManager, ISelectionManager, IBlockOperations, IEventBus, ICrossBlockSelection, IScopedI18n |
| локализация | LocaleValue, PluralForms, I18nMessages, MessageKey |
Прикладному коду обычно достаточно IEditor, EditorConfig, EditorDocument и контрактов реализуемых расширений. Интерфейсы менеджеров описывают границы зависимостей для собственной сборки, адаптеров и тестов. Они не предоставляют доступ к внутренним экземплярам менеджеров из createEditor(): в обычной интеграции используйте editor.blocks, editor.events и описанные методы IEditor.
Шаблон жизненного цикла
const editor = createEditor(config)
const unsubscribe = editor.events.on('history:commit', markDirty)
function disposeView() {
unsubscribe()
editor.destroy()
}При уничтожении редактора уничтожаются и зарегистрированные экземпляры плагинов. Не используйте те же объекты плагинов в новом редакторе; создайте новые экземпляры.