Создание надстроек
Что такое надстройка для редактора, какие есть ограничения среды исполнения надстроек, а также специфика управления внешними ресурсами описано в статье: Надстройки для редактора.
Полноценные надстройки представляют из себя проекты, код которых может быть реализован с использованием технологий Typescript, CoffeeScript, Dart, Elm и других. Главное, чтобы была возможность скомпилировать весь код в один JavaScript-файл надстройки на выходе.
Нередко надстройки импортируют и сторонние библиотеки для реализации своих возможностей.
В этой статье будет рассмотрен пример создания демонстрационного надстройки на TypeScript с подключением сторонней библиотеки.
В качестве сборщика (бандлера) будем использовать Vite.
Мы создадим проект для надстройки и шаг за шагом рассмотрим возможности EditorApi для реализации возможных задач.
Вы можете скачать и ознакомиться к кодом демонстрационной надстройки, перейдя по ссылке: Скачать архив demo-plugin.zip
Создание проекта для надстройки
Сперва создадим пустой проект для надстройки:
demo-plugin/
├── src/
│ └── index.ts
├── package.json
├── tsconfig.json
└── vite.config.ts
- src / index.ts
- package.json
- vite.config.ts
- tsconfig.json
export default {
onInit() {
console.log('Поздравляем! Это ваша первая надстройка');
},
};
{
"name": "demo-plugin",
"type": "module",
"description": "Демонстрационная надстройка",
"main": "./src/index.ts",
"scripts": {
"build": "vite build"
},
"devDependencies": {
"vite": "6.3.5"
}
}
import { defineConfig } from 'vite';
export default defineConfig({
build: {
minify: true,
target: 'esnext',
lib: {
formats: ['es'], // важно - надстройки поддерживают только ES6
entry: './src/index.ts',
name: 'DemoPlugin',
fileName: 'demo-plugin',
},
},
});
{
"compilerOptions": {
"allowJs": true,
"target": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
},
"exclude": ["node_modules", "**/dist"]
}
Теперь у нас есть песочница для создания надстройки.
Соберем проект и убедимся, что в выходной папке появится файл с надстройкой dist / demo-plugin.js
- npm
- yarn
- dist / demo-plugin.js
npm run build
yarn build
export default {
onInit() {
console.log('Поздравляем! Это ваш первая надстройка');
},
}
Чаще всего надстройка должен создать свои элементы управления для взаимодействия с пользователем и подписаться на события редактора.
Интеграция элементов управления надстройки
Для взаимодействия с пользователем привычным способом надстройка имеет возможность проинтегрировать свои элементы управления в системы редактора:
- панель инструментов
- контекстное меню
- боковая панель
Надстройка будет получать управление, когда пользователь взаимодействует с этими элементами посредством функций обратного вызова (callback).
Для любого взаимодействия с редактором надстройка должна использовать глобальную переменную editorApi. Ее интерфейс структурирован на модули для работы с разными подсистемами. В частности, для регистрации пользовательских элементов управления используется модуль editorApi.ui
Использование панели инструментов
Создадим собственную вкладку в панели инструментов, и поместим в нее кнопку для получения информации о надстройке. Когда пользователь нажмет на кнопку, мы покажем ему модальное окно с информацией о надстройке.
Реализуем это в новом файле src/ribbon.ts и обратимся к возможностям editorApi.ui.ribbon.
demo-plugin/
└── src/
└── ribbon.ts
- src / ribbon.ts
- src / index.ts
import type { Button } from '@nct/web-editor-api';
const callbacks = {
// Все функции обратного вызова должны создаваться с помощью editorApi.createCallback()
onAbout: editorApi.createCallback(showAbout),
};
export function registerRibbonElements() {
const aboutButton: Button = {
id: 'demo-plugin:ribbon:button:about',
type: 'button',
title: 'О надстройке',
icon: 'Question',
// Зарегистрируем обработчик нажатия кнопки
onClick: callbacks.onAbout,
};
// Зарегистрируем в панели инструментов отдельную вкладку с названием "Демо надстройка"
editorApi.ui.ribbon.addTab({
id: 'demo-plugin',
title: 'Демо надстройка',
groups: [{
id: 'demo-plugin:ribbon:group',
controls: [aboutButton]
}]
})
};
// Снова используем editorApi.ui для показа модального окна со справкой по надстройке
function showAbout() {
editorApi.ui.modals.showModal({
id: 'plugin-demo:modal:about',
title: 'О Демо надстройке',
content: 'Демонстрационная надстройка...'
})
}
import { registerRibbonElements } from './ribbon';
export default {
onInit() {
registerRibbonElements();
},
};
Как мы видим, создание кнопок в панели инструментов делается довольно легко. Мы декларативно описываем, что должно отображаться на панели редактора, а в качестве обработчиков событий передаем функции обратного вызова.
Подробнее с возможностями метода editorApi.ui.ribbon.addTab() можно познакомиться в статье
Настройка панели инструментов или в описании API editorApi.ui.ribbon: RibbonApi:
- вы можете создать собственные вкладки на панели инструментов для размещения элементов управления,
- вы можете зарегистрировать группы элементов управления на вкладке "Надстройки",
- можно регистрировать отдельные кнопки, а можно объединить действия в кнопки с выпадающими списками,
- любые обработчики нужно оборачивать в
editorApi.createCallback()
Другим удобным способом показать кнопки надстройки, которые зависят от контекста в редакторе, является контекстное меню.
Использование контекстного меню
Так же, как и панель инструментов, контекстное меню поддерживает простые кнопки или кнопки с выпадающими списками кнопок.
Но в отличие от кнопок панели инструментов, элементы контекстного меню можно настроить на отображение / активацию только для определенных контекстов редактора - позиции курсора и наличия выделенного содержимого документа под курсором.
Давайте зарегистрируем пункт контекстного меню, который будет показан в случае, если контекстное меню вызвано для выделенного текста в документе.
Создадим файл src/context-menu.ts и обратимся к возможностям editorApi.ui.contextMenu.
demo-plugin/
└── src/
└── context-menu.ts
- src / context-menu.ts
- src / index.ts
import type { ContextMenuItem } from '@nct/web-editor-api';
export function registerContextMenuItems() {
const processTextItem: ContextMenuItem = {
id: 'demo-plugin:context-menu:process-text',
title: 'Обработать текст',
onClick: editorApi.createCallback(() => console.log('Выбран пункт меню')),
// Ограничим список контекстов, в которых будет показан этот пункт: выделение внутри текста
shownInScopes: [{ mode: 'selection', scope: 'text' }],
};
// Зарегистрируем этот пункт
editorApi.ui.contextMenu.addItems([processTextItem]);
};
import { registerContextMenuItems } from './context-menu';
export default {
onInit() {
registerContextMenuItems();
},
};
Контекстное меню - один из привычных способов взаимодействия пользователя с содержимым документа, поэтому расширение, обрабатывающее текущее содержимое документа, будет активно применять его, регистрируя свои пункты или группы пунктов.
Подробнее о контекстах в редакторе, доступных пунктам меню, можно ознакомиться в статье: Настройка контекстного меню или в описании API ContextMenuApi.
Если функциональность надстройки требует более продвинутых способов взаимодействия с пользователем (например поля ввода), то для этих целей уже не подойдет панель инструментов и контекстное меню.
Для таких задач предусмотрена интеграция в боковую панель редактора для размещения собственных вкладок.
Использование боковой панели
Давайте расширим нашу надстройку и зарегистрируем вкладку боковой панели, которую мы будем развивать и наполнять логикой.
Создадим для этого файл src / sidebar.ts и обратимся к возможностям editorApi.ui.sidebar.
demo-plugin/
└── src/
└── sidebar.ts
- src / sidebar.ts
- src / index.ts
import type { CustomSidebarContentPart, Textblock, Button } from '@nct/web-editor-api';
export function registerSidebarElements() {
// Зарегистрируем отдельную вкладку в боковой панели с названием "Демо надстройка"
editorApi.ui.sidebar.addTab({
id: 'demo-plugin:sidebar',
title: 'Демо надстройка',
icon: 'Settings',
content: getSidebarContent()
})
};
// Определим содержимое вкладки
function getSidebarContent(): CustomSidebarContentPart[] {
// Создадим 3 элемента управления:
// текстовое поле для вывода текста
const infoText: Textblock = {
id: 'demo-plugin:sidebar:info',
type: 'textblock',
content: 'Демонстрационная надстройка'
};
// кнопка для вызова справки по надстройке
const aboutButton: Button = {
id: 'demo-plugin:sidebar:about-button',
type: 'button',
content: 'О надстройке',
// Просто используем повторно уже имеющийся колбэк
onClick: callbacks.showAbout,
};
// кнопка для выполнения действия
const doActionButton: Button = {
id: 'demo-plugin:sidebar:process-button',
type: 'button',
content: 'Обработать текст',
disabled: true, // Кнопка будет отключена по умолчанию
onClick: editorApi.createCallback(() =>
console.log('Пользователь нажал кнопку "Обработать текст"')
),
};
// Расположим элементы в стопку один под другим
return [{
type: 'controls',
orientation: 'vertical',
items: [infoText, aboutButton, doActionButton],
}];
}
import { registerSidebarElements } from './sidebar';
export default {
onInit() {
registerSidebarElements();
},
};
После старта надстройки в боковой панели появится кнопка с иконкой "Settings" (одна из стандартных иконок редактора), при наведении на которую покажется подсказка с текстом "Демо надстройка".
Когда пользователь активирует эту панель, он увидит созданные надстройкой элементы управления и сможет с ними взаимодействовать.
Подробнее о компоновке элементов боковой панели описано в статье: Ориентация блоков внутри вкладки. Работа с иконками описана в API интерфейса HasIcon.icon, который является базовым для многих элементов управления.
Мы научились создавать пользовательские элементы в редакторе, но чтобы их оживить - чтобы они реагировали на события в редакторе, нам нужно изучить механизмы подписки на события и возможности API по обновлению элементов управления в зависимости от внутренней логики и состояния надстройки.
Работа с событиями редактора
Давайте оживим элементы боковой панели, чтобы они зависели от состояния выделенного текста в редакторе.
Для организации подписок на события в редакторе создадим файл src / observe-editor-events.ts.
Доступ к событиям в редакторе предоставляется через API editorApi.events.
Для реализации логики обновления элементов боковой панели расширим файл src / sidebar.ts
demo-plugin/
└── src/
└── observe-editor-events.ts
- src / observe-editor-events.ts
- src / sidebar.ts
- src / index.ts
import { updateButtons } from './sidebar';
export function observeEditorEvents() {
// Подпишемся на событие смены выделения / позиции курсора в редакторе
editorApi.events.subscribe('selectionChange', (state: any) => {
// Обновим элементы управления боковой панели в зависимости от наличия выделенного текста
updateButtons(state.isSelection);
});
// Как пример, подпишемся на изменение в самом документе
editorApi.events.subscribe('documentChange', () => {
console.log('Что-то изменилось в документе');
});
}
const infoLabelId = 'demo-plugin:sidebar:info';
const actionButtonId = 'demo-plugin:sidebar:process-button';
export function registerSidebarElements() { ... }
function getSidebarContent(): CustomSidebarContentPart[] {
// У нас уже были созданы 3 элемента управления
// В отличие от предыдущего примера мы вынесли идентификаторы в константы,
// чтобы иметь возможность указать их для обновления состояния
}
export function updateButtons(hasSelection: boolean) {
const newLabelContent = hasSelection ? 'Вы выбрали содержимое в документе' : 'Ничего не выбрано';
const canProcess = hasSelection;
// Укажем редактору, что мы хотим обновить состояние двух элементов управления
// - поменять текст в текстовом поле infoLabelId
// - отключить кнопку actionButtonId, если ничего не выбрано
editorApi.ui.updateUiNodes([
{ id: infoLabelId, content: newLabelContent },
{ id: actionButtonId, disabled: !canProcess },
]);
}
import { observeEditorEvents } from './observe-editor-events';
export default {
onInit() {
observeEditorEvents();
},
};
Теперь текст и кнопка "Обработать текст" в боковой панели станут изменять свое состояние в зависимости от наличия выбранного текста в редакторе.
Больше о событиях читайте в статье Работа с событиями документа.
Использование метода editorApi.ui.updateUiNodes подробно описано в его документации.
Подключение сторонних библиотек
Код надстройки может использовать внешние библиотеки, но нужно иметь в виду ограничения среды исполнения надстроек.
Для каждой конкретной сторонней библиотеки нужно провести предварительную попытку ее интеграции в код надстройки, только тогда можно будет понять, заработает ли она в среде исполнения надстройки.
Заранее понять, какие сторонние библиотеки можно или нельзя подключать, определить сложно, но можно использовать чек-лист:
- Если библиотека содержит код, специфичный для исполнения в браузере (window, document, ...), то ее точно нельзя использовать в надстройке, т.к. в его окружении нет доступа к этим переменным.
- Если библиотека использует динамические импорты, то это не поддерживается в текущей реализации надстроек.
- Если библиотека предоставляет код, работающий в любом JavaScript-окружении, то она с большой вероятностью заработает и в надстройке.
Установка библиотеки
Давайте подключим стороннюю библиотеку, которая реализует преобразование чисел в их письменное представление в русском языке, и используем её для реализации этой логики в нашей надстройке.
Установим библиотеку number-to-words-ru@2.4.1
- npm
- yarn
npm install number-to-words-ru@2.4.1
yarn add number-to-words-ru@2.4.1
Подключение библиотеки в надстройку
Используем код библиотеки точно так же, как и собственный код. Создадим под это новый файл number-to-words.ts:
demo-plugin/
└── src/
└── number-to-words.ts
И расширим имеющийся код надстройки для использования возможностей библиотеки.
- src / number-to-words.ts
- src / ribbon.ts
- src / sidebar.ts
- src / actions.ts
import { convert } from 'number-to-words-ru';
type ConvertOptions = Parameters<typeof convert>[1];
const options: ConvertOptions = {
// настройки для библиотеки 'number-to-words-ru'
};
// Regex-паттерн для поиска чисел внутри строки
const numbersPattern = new Regex('...');
// Преобразует все найденные числа в строке в их строковое представление
export function transformNumbers(text: string): string {
return text.replaceAll(numbersPattern, (numberPart) => convert(numberPart, options));
};
import { processEditorSelection } from './actions';
// Добавим кнопку в нашу вкладу панели инструментов,
// по нажатию которой будем обрабатывать выделенный текст в документе редактора
export function registerRibbonElements() {
const onTransformClick = editorApi.createCallback(processEditorSelection);
// Зарегистрируем кнопку "Преобразовать числа"
editorApi.ui.ribbon.addTab({
...,
groups: [{
...,
controls: [
...,
{
id: 'demo-plugin:ribbon:buttons:transform-selection',
title: 'Преобразовать числа',
type: 'button',
onClick: onTransformClick,
}
],
}],
});
}
const state = {
text: ''
}
const infoLabelId = 'demo-plugin:sidebar:info';
const actionButtonId = 'demo-plugin:sidebar:process-button';
export function updateSidebarControls(text: string) {
state.text = text;
if (!text) {
return;
}
editorApi.ui.sidebar.openTab('demo-plugin:sidebar');
editorApi.ui.updateUiNodes([
// Отобразим обработанный текст в боковой панели
{ id: infoLabelId, content: text },
// Активируем кнопку "Вставить текст"
{ id: actionButtonId, disabled: !text },
]);
}
// Реализует вставку текста, обработанного внешней библиотекой
function insertText() {
editorApi.document.insertContent(state.text);
}
// Изменим кнопку "Обработать текст": переименуем в "Вставить текст" и реализуем эту логику
function getSidebarContent(): CustomSidebarContentPart[] {
return [
...,
{
id: actionButtonId,
type: 'button',
content: 'Вставить текст',
onClick: editorApi.createCallback(insertText),
}
]
}
import { transformNumbers } from './number-to-words';
import { updateSidebarControls } from './sidebar';
export function processEditorSelection() {
// Обработаем текущий выделенный текст в редакторе
const selection = await editorApi.document.selection.getSelectionAsText();
if (!selection) {
return;
}
// Обработаем текст, выделенный в редакторе
const processedText = transformNumbers(selection);
// Обновим состояние боковой панели с учетом преобразованного текста
updateSidebarControls(processedText);
};
Теперь надстройка по нажатию на кнопку в панели инструментов "Преобразовать числа":
- обрабатывает выделенный в редакторе текст с помощью внешней библиотеки,
- активирует боковую панель,
- показывает обработанный текст в поле для информации,
- активирует кнопку "Вставить текст", если текст не пустой.
А по нажатию на кнопку "Вставить текст" в боковой панели надстройка заменяет текущий выбранный текст в редакторе
на обработанный текст, в котором все числа заменены на их строковое представление с помощью метода
editorApi.document.insertContent().
В этом примере в файле src/actions.ts мы применили:
- метод editorApi.document.selection.getSelectionAsText для получения текущего выделенного текста в редакторе.
- метод editorApi.ui.sidebar.openTab для активации нужной вкладки боковой панели из кода.
- метод editorApi.ui.updateUiNodes для обновления элементов управления.
Скачать Демо надстройку
В демонстрационной надстройке сведены вместе все описанные в этой статье подходы и реализована функциональность
для замены чисел в тексте на их строковое представление, для этого подключена библиотека numbers-to-words-ru.
Код надстройки структурирован в удобном для изучения виде и снабжен комментариями, поясняющими особенности работы с API редактора.
Вы можете собрать надстройку из исходного кода и подключить в редактор для изучения его возможностей.
Вы можете скачать и ознакомиться к кодом демонстрационной надстройки, перейдя по ссылке: Скачать архив demo-plugin.zip