Внутристрочные инструменты и плагины
В Rector есть два механизма расширения, которые работают внутри текста, но решают разные задачи. Внутристрочный инструмент меняет форматирование, сохраняемое в текстовом поле блока. Внутристрочный плагин вставляет постоянный структурированный виджет с собственными сериализуемыми данными.
Выбор механизма
| Требование | Внутристрочный инструмент | Внутристрочный плагин |
|---|---|---|
| Полужирное начертание, подчёркивание, ссылка, размер шрифта, выравнивание | да | нет |
| Упоминание, товар, формула или ссылка на сущность | нет | да |
| Собственная запись в документе | нет | да, в block.inline |
| Работа с выделенным текстом | обычно | не обязательна |
| Всплывающий список подсказок | возможен, но используется редко | да |
| Восстановление отдельной фабрикой виджета в рендерере | нет | да |
Результат инструмента становится частью обычных текстовых данных плагина блока, например { text: 'Привет, <strong>мир</strong>' }. Виджет сохраняется как маркер в тексте и соответствующая запись в карте inline блока.
Интерактивное расширение вызывает предоставленную границу изменения один раз для каждого завершённого действия пользователя. Два отдельных нажатия являются двумя действиями и создают две команды, а одно нажатие, меняющее несколько связанных значений, остаётся одним действием.
Настройка внутристрочных инструментов
Если inlineTools не задан, Rector создаёт все встроенные инструменты. Пустой массив отключает общий набор. Строки выбирают встроенные инструменты, а объекты регистрируют пользовательские реализации. Порядок массива определяет порядок кнопок.
createEditor({
holder,
plugins,
inlineTools: ['bold', 'italic', 'link'],
})Названия встроенных типов чувствительны к регистру:
type | Поведение |
|---|---|
bold | полужирное начертание |
italic | курсивное начертание |
strikethrough | зачёркнутый текст |
link | создание или удаление проверенной ссылки |
code | внутристрочный код |
marker | выделение текста элементом mark |
bgcolor | цвет фона |
fontSize | целый размер от 1 до 200 пикселей; готовые значения: 12, 14, 16, 18, 20, 24, 28, 32, 40, 48 и 64 пикселя; значение 16 пикселей удаляет явно заданный размер |
script | верхний или нижний индекс |
align | выравнивание затронутых блоков |
caseTransform | переключение выделенных букв Юникода с регистром между верхним и нижним регистром; цифры, знаки препинания и письменности без регистра не меняются |
clearFormatting | удаление форматирования b, i, s, code, mark, span, em, strong, u, sup и sub с сохранением ссылок, внутристрочных виджетов и выравнивания блока |
align находится среди внутристрочных инструментов, потому что вызывается из панели выделения, но он не оборачивает выделенный текст разметкой. Инструмент меняет весь затронутый блок и сохраняет значение в block.tunes.textAlign. Плагины Paragraph и Heading по-прежнему поддерживают собственное поле data.align; если присутствуют оба значения, общим для всех плагинов переопределением служит настройка блока.
Неизвестная строка приводит к ошибке при вызове createEditor(). Если один type встречается в массиве несколько раз, более поздний объект заменяет предыдущую реализацию, сохраняя первую позицию кнопки. Это позволяет передать ['bold', customBold]; повторяйте тип только для намеренной замены.
Создание инструмента для одного HTML-элемента
Если инструмент только добавляет и удаляет один HTML-элемент, используйте готовую фабрику. Она согласованно обрабатывает частичные выделения, вложенное форматирование, восстановление выделения и очистку пустых элементов.
import { createEditor } from '@shelamkoff/rector'
import { createSimpleInlineTool } from '@shelamkoff/rector/inline-tools/utils'
const underline = createSimpleInlineTool(
'underline',
'Подчеркнуть',
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3v7a6 6 0 0 0 12 0V3"/><path d="M4 21h16"/></svg>',
'u',
'Mod+U',
)
const editor = createEditor({
holder,
plugins,
inlineTools: ['bold', 'italic', underline],
})Строка значка попадает в доверенный интерфейс редактора через innerHTML. Используйте только постоянный SVG из пакета. Не собирайте его из документа или пользовательских данных.
Полный контракт внутристрочного инструмента
Объектный контракт нужен, когда поведение нельзя выразить одним HTML-элементом.
interface InlineTool {
readonly type: string
readonly title?: string
readonly icon: string
readonly shortcut?: string
readonly tag?: string
isActive(selection: InlineSelection): boolean
toggle(selection: InlineSelection): void
renderActions?(ctx: InlineToolActionContext): HTMLElement | null
getIcon?(active: boolean): string
getTitle?(active: boolean): string
onMount?(button: HTMLElement, mutations?: InlineMutationContext): void
isDropdownOpen?(): boolean
destroy?(): void
}
interface InlineSelection {
blockId: string
range: Range
text: string
}Rector вызывает isActive() при изменении выделения. Метод должен только проверять состояние и не менять документ. Обычное нажатие кнопки уже выполняет toggle() внутри одной команды диапазона, поэтому toggle() непосредственно меняет переданный диапазон и не открывает ещё одну команду.
Методы getIcon() и getTitle() могут учитывать текущее активное состояние. Свойство tag описывает основной HTML-элемент и служит метаданными для совместимых инструментов. В сочетании клавиш используются нормализованные названия вроде Mod+U; Mod означает Command в macOS и Control в Windows или Linux.
Инструмент с панелью действий
Реализуйте renderActions(), если пользователь должен ввести или выбрать значение. Rector сохраняет исходный диапазон, пока фокус находится внутри панели.
const textColor = {
type: 'textColor',
title: 'Цвет текста',
icon: '<svg viewBox="0 0 24 24" aria-hidden="true">...</svg>',
isActive({ range }) {
return Boolean(range.startContainer.parentElement?.closest('[data-text-color]'))
},
toggle() {},
renderActions(ctx) {
const input = document.createElement('input')
input.type = 'color'
input.addEventListener('change', () => {
ctx.restoreSelection()
ctx.mutate(() => applyColor(ctx.range, input.value))
ctx.close()
})
return input
},
}Каждый завершённый выбор вызывает ctx.mutate() один раз. Открытие панели, перенос фокуса и предварительный просмотр значения не должны создавать шаг истории. Если renderActions() возвращает null, Rector вызывает toggle() для текущей активации.
Контекст InlineToolActionContext также предоставляет range, restoreSelection(), close(), showTooltip(anchor, label) и hideTooltip(). Свойство range содержит копию диапазона, активного при открытии панели. Непосредственно перед изменением DOM вызовите restoreSelection(), затем выполните завершённое изменение в одном вызове mutate(). Метод close() возвращает пользователя из панели действий к кнопкам инструментов. Методы showTooltip() и hideTooltip() используют доступную всплывающую подсказку Rector для элементов управления внутри панели; они не меняют выделение и историю.
Постоянные элементы управления и освобождение ресурсов
Метод onMount(button, mutations) предназначен для инструмента, который создаёт рядом с кнопкой раскрывающийся список или другой долгоживущий элемент DOM. Действие в таком списке выполняется за пределами обычной обработки кнопки, поэтому оно должно один раз вызвать mutations.mutate(range, operation) с сохранённым диапазоном. Пока дополнительный интерфейс открыт, isDropdownOpen() должен возвращать true, иначе Rector может скрыть панель.
В destroy() удаляйте обработчики document и window, отсоединённые всплывающие элементы, наблюдатели, таймеры и другие ресурсы инструмента. Один объект инструмента принадлежит одному экземпляру редактора и не используется повторно после editor.destroy().
Разрешение инструментов для типа блока
Плагин блока определяет, может ли его редактируемое содержимое участвовать во внутристрочном форматировании:
class CodeBlock {
inlineTools = false
}
class Paragraph {
inlineTools = true
}
class Title {
inlineTools = ['bold', 'italic']
}Значение false нужно блокам без совместимого редактируемого текста. true или отсутствие свойства включает набор, заданный в конфигурации редактора. Массив строк задаёт список разрешённых инструментов для типа блока: панель показывает только инструменты, которые одновременно зарегистрированы в редакторе и перечислены плагином блока. Пустой массив отключает панель для этого блока.
При выделении нескольких блоков Rector показывает пересечение их списков. Если хотя бы один блок отключает внутристрочные инструменты, панель скрывается. Название инструмента, который не зарегистрирован в редакторе, не создаёт кнопку.
Настройка внутристрочных плагинов
Постоянные виджеты регистрируются отдельно от инструментов форматирования:
import { createMentionPlugin } from '@shelamkoff/rector/inline-plugins/mention'
const editor = createEditor({
holder,
plugins,
inlinePlugins: [
createMentionPlugin({
searchFunction: query => searchPeople(query),
}),
],
})Плагин редактора и фабрика виджета рендерера должны использовать одинаковые type и форму данных. Регистрируйте все внутристрочные плагины, упомянутые в загружаемом документе. Иначе маркер нельзя восстановить как виджет.
Контракт внутристрочного плагина
interface InlinePlugin {
readonly type: string
readonly title: string
readonly icon: string
readonly styles?: readonly string[]
readonly trigger?: string
readonly pasteConfig?: { patterns: RegExp[] }
createWidget(data: Record<string, string>, id?: string): HTMLElement
mount?(rootElement: HTMLElement, ctx: InlinePluginContext): void
hydrate(element: HTMLElement, ctx: InlinePluginContext): void
getData(element: HTMLElement): Record<string, string>
isCommitted?(element: HTMLElement): boolean
onPatternMatch?(match: string): Record<string, string>
onEdit?(element: HTMLElement, text: string, ctx: InlinePluginContext): void
onCancel?(): void
onCommit?(element: HTMLElement, data: Record<string, string>): void
insertFresh?(ctx: InlinePluginContext): void
destroy?(): void
}styles объявляет URL таблиц стилей, но не загружает их самостоятельно. Rector объединяет их с базовыми стилями и стилями блочных плагинов при createEditor({ injectStyles: true }); приложение со сборщиком импортирует соответствующий CSS-subpath и устанавливает флаг в false. createWidget() создаёт виджет и обязан сохранить переданный идентификатор в data-id. mount() получает корневой элемент и контекст изменений после закрепления плагина за экземпляром редактора; здесь подключают общие обработчики и ресурсы экземпляра. getData() возвращает совместимые с JSON строки для сериализации. hydrate() подключает поведение к восстановленному DOM. Свойство trigger должно содержать ровно одну кодовую точку Юникода. onEdit() получает текст между этим символом и курсором; onCancel() закрывает временный интерфейс плагина, когда курсор покидает сеанс, пользователь нажимает клавишу Escape, удаляет символ активации или уничтожает редактор. Необязательные шаблоны вставки обеспечивают автоматическое преобразование, а insertFresh() заменяет стандартное программное добавление. destroy() освобождает ресурсы экземпляра и состояние виджетов при уничтожении редактора.
Реализуйте isCommitted(element), если у виджета есть временное состояние, которое видно во время поиска или редактирования, но ещё нельзя сохранять как данные документа. Возвращайте false только для такого состояния. При вызове save() Rector сохранит видимый текст элемента как обычный текст и не создаст для него запись в block.inline. Для завершённого виджета метод должен вернуть true; если временного состояния нет, метод можно не объявлять. Так автосохранение не создаёт незавершённую сущность и при этом не теряет введённый пользователем запрос.
Один объект плагина может обслуживать много элементов виджета. Состояние отдельного элемента храните в WeakMap. Для принадлежащих плагину всплывающих элементов используйте ctx.showPopup() и ctx.hidePopup(). Каждое завершённое постоянное изменение должно один раз вызвать ctx.mutate(target, operation). В качестве target передавайте виджет или его дочерний узел, чтобы Rector нашёл блок-владелец.
Свойство InlinePluginContext.readOnly сообщает текущий режим взаимодействия. При значении true не создавайте и не активируйте элементы, изменяющие документ. Метод ctx.notifyChanged(target?) сообщает Rector об изменении состояния плагина за пределами ctx.mutate(), чтобы наблюдатели могли повторно сохранить документ. Он не создаёт снимок истории и не заменяет ctx.mutate(). Для каждого пользовательского действия, меняющего сохраняемые данные, используйте ctx.mutate(). Вызывайте notifyChanged() только после уже завершённого внешнего обновления или в процессе, границей команды которого владеет другой компонент; по возможности передавайте элемент из блока-владельца.
Вставка виджета из приложения
const inserted = editor.insertInlinePlugin('mention', {
id: 'user-42',
name: 'Ада Лавлейс',
})Метод возвращает false, если плагин не зарегистрирован или курсор находится вне совместимого текстового блока. Плагин с insertFresh() может запустить собственный интерактивный процесс вместо немедленного создания виджета.
Отображение и безопасность
Rector очищает HTML форматирования во внутристрочном анализаторе. Виджет восстанавливается только зарегистрированной фабрикой рендерера; сериализованные данные не выполняются как разметка. Считайте недоверенными данные плагина, вставленные строки, URL, результаты поиска и файлы. Экранируйте текст, проверяйте URL и не смешивайте доверенный SVG значка с данными документа.
Перед реализацией интерактивных элементов прочитайте раздел Команды и история. Формат хранения виджетов описан в Формате документа, а регистрация вывода — в Отображении документов.