Skip to content

Внутристрочные инструменты и плагины

В Rector есть два механизма расширения, которые работают внутри текста, но решают разные задачи. Внутристрочный инструмент меняет форматирование, сохраняемое в текстовом поле блока. Внутристрочный плагин вставляет постоянный структурированный виджет с собственными сериализуемыми данными.

Выбор механизма

ТребованиеВнутристрочный инструментВнутристрочный плагин
Полужирное начертание, подчёркивание, ссылка, размер шрифта, выравниваниеданет
Упоминание, товар, формула или ссылка на сущностьнетда
Собственная запись в документенетда, в block.inline
Работа с выделенным текстомобычноне обязательна
Всплывающий список подсказоквозможен, но используется редкода
Восстановление отдельной фабрикой виджета в рендереренетда

Результат инструмента становится частью обычных текстовых данных плагина блока, например { text: 'Привет, <strong>мир</strong>' }. Виджет сохраняется как маркер в тексте и соответствующая запись в карте inline блока.

Интерактивное расширение вызывает предоставленную границу изменения один раз для каждого завершённого действия пользователя. Два отдельных нажатия являются двумя действиями и создают две команды, а одно нажатие, меняющее несколько связанных значений, остаётся одним действием.

Настройка внутристрочных инструментов

Если inlineTools не задан, Rector создаёт все встроенные инструменты. Пустой массив отключает общий набор. Строки выбирают встроенные инструменты, а объекты регистрируют пользовательские реализации. Порядок массива определяет порядок кнопок.

js
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-элемент, используйте готовую фабрику. Она согласованно обрабатывает частичные выделения, вложенное форматирование, восстановление выделения и очистку пустых элементов.

js
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-элементом.

ts
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 сохраняет исходный диапазон, пока фокус находится внутри панели.

js
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().

Разрешение инструментов для типа блока

Плагин блока определяет, может ли его редактируемое содержимое участвовать во внутристрочном форматировании:

js
class CodeBlock {
  inlineTools = false
}

class Paragraph {
  inlineTools = true
}

class Title {
  inlineTools = ['bold', 'italic']
}

Значение false нужно блокам без совместимого редактируемого текста. true или отсутствие свойства включает набор, заданный в конфигурации редактора. Массив строк задаёт список разрешённых инструментов для типа блока: панель показывает только инструменты, которые одновременно зарегистрированы в редакторе и перечислены плагином блока. Пустой массив отключает панель для этого блока.

При выделении нескольких блоков Rector показывает пересечение их списков. Если хотя бы один блок отключает внутристрочные инструменты, панель скрывается. Название инструмента, который не зарегистрирован в редакторе, не создаёт кнопку.

Настройка внутристрочных плагинов

Постоянные виджеты регистрируются отдельно от инструментов форматирования:

js
import { createMentionPlugin } from '@shelamkoff/rector/inline-plugins/mention'

const editor = createEditor({
  holder,
  plugins,
  inlinePlugins: [
    createMentionPlugin({
      searchFunction: query => searchPeople(query),
    }),
  ],
})

Плагин редактора и фабрика виджета рендерера должны использовать одинаковые type и форму данных. Регистрируйте все внутристрочные плагины, упомянутые в загружаемом документе. Иначе маркер нельзя восстановить как виджет.

Контракт внутристрочного плагина

ts
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() только после уже завершённого внешнего обновления или в процессе, границей команды которого владеет другой компонент; по возможности передавайте элемент из блока-владельца.

Вставка виджета из приложения

js
const inserted = editor.insertInlinePlugin('mention', {
  id: 'user-42',
  name: 'Ада Лавлейс',
})

Метод возвращает false, если плагин не зарегистрирован или курсор находится вне совместимого текстового блока. Плагин с insertFresh() может запустить собственный интерактивный процесс вместо немедленного создания виджета.

Отображение и безопасность

Rector очищает HTML форматирования во внутристрочном анализаторе. Виджет восстанавливается только зарегистрированной фабрикой рендерера; сериализованные данные не выполняются как разметка. Считайте недоверенными данные плагина, вставленные строки, URL, результаты поиска и файлы. Экранируйте текст, проверяйте URL и не смешивайте доверенный SVG значка с данными документа.

Перед реализацией интерактивных элементов прочитайте раздел Команды и история. Формат хранения виджетов описан в Формате документа, а регистрация вывода — в Отображении документов.

Rector is released under the MIT License.