Tools API: регистрация и обновление инструментов
Доступ и управление инструментами редактора.
Как получить экземпляр редактора
Методы ниже вызываются на редакторе, созданном через new Blok(). Они доступны после того, как разрешится editor.isReady.
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;
// Call any API method on it.
editor.caret.setToLastBlock('end');Методы
tools.getBlockTools()
BlockToolAdapter[]Получить все доступные адаптеры блочных инструментов. Каждый адаптер предоставляет `name` и метаданные инструмента, включая `assetKind` — он равен `'image' | 'video' | 'audio' | 'file'` у медиаинструментов, которые хранят URL загруженного ресурса в `data.url`, и `undefined` у остальных. Используйте это, чтобы во время выполнения определить набор инструментов, несущих медиа (вместо жёсткого прописывания формы данных каждого инструмента), и сверить `data.url` сохранённого документа с вашим CDN — например, чтобы подчистить осиротевшие загрузки.
Когда использовать
Перечисляет зарегистрированные блочные инструменты во время выполнения — удобно для своего выбора блоков или отладки конфигурации.
const blockTools = editor.tools.getBlockTools();
blockTools.forEach(tool => {
console.log('Available tool:', tool.name);
});
// Discover which block types hold uploaded media, then collect their URLs
const mediaTypes = new Set(
editor.tools.getBlockTools().filter(t => t.assetKind).map(t => t.name)
);
const referenced = (await editor.save()).blocks
.filter(b => mediaTypes.has(b.type))
.map(b => b.data.url);tools.getToolsConfig()
ToolsConfigВозвращает связанную с инструментами конфигурацию этого экземпляра — { tools, inlineToolbar?, tunes?, theme? } — для создания вложенных редакторов Blok с тем же набором инструментов.
const nested = new Blok({ holder, ...editor.tools.getToolsConfig() });tools.update(name, config)
voidПоверхностно объединить новую конфигурацию с установленным инструментом во время выполнения — без пересоздания редактора. Ключ `toolbox` трактуется как настройка уровня инструмента (та же, что `toolbox` в карте `tools`): передайте `toolbox: false`, чтобы скрыть инструмент со всех поверхностей вставки (существующие блоки продолжают отрисовываться), или объект toolbox, чтобы (снова) показать его — разграничение прав без пересборки редактора. В React-адаптере это происходит автоматически: измените значение `toolbox` в пропе `tools`, и `useBlok`/`BlokEditor` применит его на месте.
// Swap a config value (e.g. an uploader) at runtime
editor.tools.update('image', { uploader: { uploadByFile } });
// Permission flip: hide the tool from the + / slash / convert menus.
// Existing goodsList blocks still render; insertion is gated.
editor.tools.update('goodsList', { toolbox: false });
// Re-enable it later
editor.tools.update('goodsList', { toolbox: { title: 'Goods List' } });tools.setInlineToolbar(config)
voidRuntime-сеттер глобальной опции `inlineToolbar`. Заново назначает строчные инструменты каждому блочному инструменту и пересобирает мемоизированные конфигурации санитизации — санитизация при вставке следует новому набору сразу, а строчная панель отражает его при следующем выделении. Настройки `inlineToolbar` на уровне инструмента (массивы и отказы) остаются приоритетными. Передайте `true` для всех строчных инструментов, `false` — чтобы отключить, или упорядоченный список имён строчных инструментов. Если вы отображаете сохранённый контент через @bloklabs/core/view, учтите: viewSchema собирается из того значения inlineToolbar, с которым она была определена — после рантайм-вызова setInlineToolbar с пользовательскими строчными инструментами пересоберите её через defineBlokSchema, прежде чем вызывать blocksToHtml.
Когда использовать
Вступает в силу при следующем выделении; санитизация при вставке пересобирается сразу, так что вставляемый контент следует новому набору строчных инструментов немедленно. Если сохранённый контент отображается через @bloklabs/core/view, учтите: viewSchema составляется из того значения inlineToolbar, с которым была определена — после runtime-вызова setInlineToolbar с пользовательскими строчными инструментами пересоберите её через defineBlokSchema перед вызовом blocksToHtml.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
config | boolean | string[] | Обязательный | — | true включает все зарегистрированные строчные инструменты, false отключает строчную панель, массив ограничивает её перечисленными инструментами в этом порядке. |
// Restrict inline formatting to bold and italic at runtime
editor.tools.setInlineToolbar(['bold', 'italic']);
// Disable the inline toolbar entirely
editor.tools.setInlineToolbar(false);
// Back to every registered inline tool
editor.tools.setInlineToolbar(true);tools.isInstalled(name)
booleanВозвращает true, если инструмент с указанным именем установлен и доступен в этом экземпляре редактора — блочный, строчный или тюн. Публичная интроспекция набора установленных инструментов, например как проверка перед `tools.update(name, config)`, который бросает исключение для неизвестных имён.
Когда использовать
Защищает runtime-вызовы инструментов — tools.update() бросает исключение для неустановленных имён.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
name | string | Обязательный | — | Имя зарегистрированного инструмента для проверки. |
if (editor.tools.isInstalled('image')) {
editor.tools.update('image', { uploader: { uploadByFile } });
}defineTool(toolClass, settings?)
ExternalToolSettingsВспомогательная функция регистрации, экспортируемая из `@bloklabs/core/tools`, а не член пространства имён `tools`. Обычная карта `tools` типизирует каждую запись голым `ToolSettings`, у которого `Config` откатывается к `Record<string, unknown>`, поэтому опечатка в ключе конфигурации (`defaultLevle` вместо `defaultLevel`) компилируется молча. `defineTool` восстанавливает настоящий тип конфигурации инструмента из его конструктора и применяет его к `settings.config`, превращая такие опечатки в ошибки компиляции. Проверка типов происходит на аргументе `settings`; ВОЗВРАЩАЕМЫЙ тип остаётся стёртым `ExternalToolSettings`, поэтому результат ложится прямо в карту `tools`. `ExtractToolConfig<TClass>` — тип, который выполняет это восстановление, — экспортируется рядом с ней и откатывается к `Record<string, unknown>` для классов инструментов, чей конструктор не объявляет конкретной конфигурации.
import { Blok } from '@bloklabs/core';
import { Header, defineTool } from '@bloklabs/core/tools';
new Blok({
tools: {
header: defineTool(Header, { config: { levels: [1, 2, 3] } }),
// `defaultLevle: 2` here would now be a compile error
},
});mountChildBlocks(container, children)
voidСогласователь холдеров детей для блоков-контейнеров, экспортируемый из `@bloklabs/core/tools`. Вызывайте его из хука `rendered()` вашего инструмента — именно его используют встроенные инструменты тоггла, выноски и колонки, и именно его запускают React/Vue/Angular-адаптеры блоков на каждом коммите. Он идемпотентен и дёшев, поэтому запускайте его на каждой отрисовке. По каждому ребёнку он: оставляет как есть холдер, уже находящийся внутри `container`; ЗАБИРАЕТ холдер, застрявший во вложенном контейнере, который ОХВАТЫВАЕТ `container`, и вставляет его на позицию из модели, а не добавляет в конец; оставляет как есть холдеры, лежащие в любом ДРУГОМ вложенном контейнере, поэтому два контейнера никогда не смогут украсть блоки друг у друга; и монтирует всё остальное на позицию из модели. Именно этот возврат позволяет контейнеру пережить порядок вставки: ядро привязывает только что вставленного первого ребёнка как DOM-соседа блока-контейнера, поэтому без него ребёнок вложенного контейнера отрисовывается на один уровень выше — навсегда, если слот для детей вашего контейнера ещё не был создан к моменту вставки (портал фреймворка коммитит отрисовку после вставки в ядре). Пометьте `container` атрибутом `data-blok-nested-blocks`, чтобы остальной редактор распознавал его как контейнер.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
container | HTMLElement | Обязательный | — | Элемент, в котором должны находиться холдеры детей, — элемент, несущий data-blok-nested-blocks. |
children | { holder: HTMLElement }[] | Обязательный | — | Дети блока в порядке модели, обычно api.blocks.getChildren(blockId). |
import { mountChildBlocks } from '@bloklabs/core/tools';
class CardTool {
constructor({ api, block }) {
this.api = api;
this.blockId = block.id;
}
render() {
this.slot = document.createElement('div');
this.slot.setAttribute('data-blok-nested-blocks', '');
return this.slot;
}
// Runs after the holder is in the document, and on every re-render
rendered() {
mountChildBlocks(this.slot, this.api.blocks.getChildren(this.blockId));
}
}BlockToolConstructorOptions.origin
BlockOriginСигнал «создание или восстановление» в контракте инструмента, передаваемый в конструктор каждого блочного инструмента рядом с `data`, `block` и `readOnly`. Контейнерный инструмент, который засевает детей по умолчанию — строку из двух колонок, карточку, начинающуюся с заголовка, — может делать это только один раз, при создании. Во всех остальных случаях к моменту конструирования инструмента документ уже говорит, какие у него дети, а при восстановлении эти дети обычно приходят на тик ПОЗЖЕ вызова `rendered()`, поэтому пустой `api.blocks.getChildren()` там лишь временный: засев по такому чтению создаёт фантомных детей рядом с настоящими. Значения СОЗДАНИЯ — засевать можно: `'user'` (прямой жест редактирования: Enter, кнопка «плюс», тулбокс по «/», настройки блока, Markdown-сокращение), `'api'` (программные `blocks.insert` / `insertMany` / `insertInsideParent`), `'convert'` (преобразование блока). Значения ВОССТАНОВЛЕНИЯ — засевать нельзя: `'load'` (отрисовка документа), `'replay'` (воспроизведение отмены/повтора или удалённое совместное изменение), `'paste'` (вставленное содержимое, которое приносит своих детей), `'probe'` (экземпляр ВНЕ ДЕРЕВА, который `blocks.composeBlockData()` создаёт, чтобы прочитать данные инструмента по умолчанию — он никогда не вставляется, но всё равно выполняет `render()` и `rendered()`, поэтому он вообще не должен трогать дерево блоков). Blok всегда передаёт его; отсутствующее значение трактуйте как `'api'`, а проверку пишите как белый список значений создания, чтобы будущее значение origin по умолчанию не считалось созданием. Сочетайте его с `blocks.insert(..., origin)`, если вы запускаете вставку из собственного интерфейса. В React/Vue/Angular-адаптерах вы редко читаете его вручную: хук `onCreated` в спецификации блока уже кодирует этот белый список и срабатывает после первого коммита адаптера — в тот тик, когда DOM блока и принятые им холдеры детей действительно существуют.
class TwoColumnCard {
constructor({ api, block, origin }) {
this.api = api;
this.blockId = block.id;
// Allow-list, so a future origin never silently opts into seeding.
this.isCreation = ['user', 'api', 'convert', undefined].includes(origin);
}
render() {
this.slot = document.createElement('div');
this.slot.setAttribute('data-blok-nested-blocks', '');
return this.slot;
}
rendered() {
const children = this.api.blocks.getChildren(this.blockId);
if (children.length > 0) {
mountChildBlocks(this.slot, children);
return;
}
// Empty on a load / undo-redo replay / paste / probe means "my children
// have not arrived yet", NOT "I am brand new". Only a creation may seed.
if (!this.isCreation) {
return;
}
this.seedColumns();
}
}BlockToolConstructable.keepsChildrenOnEnter
booleanСтатическое поле на КЛАССЕ вашего инструмента, которое решает, куда уходит Enter на пустом ПОСЛЕДНЕМ ребёнке контейнера. По умолчанию Blok читает эту пустую последнюю строку как способ автора выйти наружу: при наличии соседей строка выносится к родителю самого контейнера, а если она единственный ребёнок — после всего контейнера вставляется новый блок, как ведёт себя выноска в Notion. Контейнеру для вёрстки, чьи дети И ЕСТЬ его содержимое (колонка, карточка, блок `steps`), нужно обратное, и без этого объявления выход оставляет новую строку рядом с контейнером. Задайте `true` — и новая строка остаётся внутри, по тому же правилу, которому следуют встроенные `column`, `column_list` и `toggle`. Вывести это из DOM нельзя: выноска отрисовывает ровно тот же слот `data-blok-nested-blocks`, что и колонка, и намеренно сохраняет выход, поэтому это политика уровня инструмента. Ядро читает его и для симметричного жеста «убрать один уровень отступа» (Enter/Backspace на блоке, вложенном в ОБЫЧНОГО родителя), поэтому объявивший инструмент и там считается контейнером, а его дети никогда не выходят из него по одному уровню отступа. В React/Vue/Angular-адаптерах объявляйте его в наборе `statics` спецификации блока, как любое другое статическое поле класса.
class StepsTool {
static keepsChildrenOnEnter = true;
render() {
this.slot = document.createElement('div');
this.slot.setAttribute('data-blok-nested-blocks', '');
return this.slot;
}
rendered() {
mountChildBlocks(this.slot, this.api.blocks.getChildren(this.blockId));
}
}
// Framework adapters forward it through `statics`:
export const StepsTool = createReactBlock({
type: 'steps',
statics: { ownsChildren: true, keepsChildrenOnEnter: true },
component: StepsCard,
});BlockToolConstructable.childTools
{ allow?: string[]; deny?: string[] }Статическое поле на КЛАССЕ вашего инструмента, объявляющее, какие блочные инструменты могут быть ПРЯМЫМИ детьми его блока, — и ядро само следит за этим везде. При ВСТАВКЕ запрещённый инструмент понижается (никогда не отклоняется, потому что Enter обязан всегда порождать блок): целью становится первая запись `allow`, поэтому с `allow: ['segment-item']` «Enter в конце сегмента» порождает ещё один сегмент, а не случайный параграф. При ПЕРЕМЕЩЕНИИ перетаскивание или переупорядочивание с клавиатуры, которое перенесло бы запрещённый блок через границу контейнера, отклоняется. В ТУЛБОКСЕ запрещённые инструменты скрыты, пока курсор находится в ребёнке. `deny` побеждает `allow` для инструмента, названного в обоих, а пустые списки читаются как «нет ограничений». Это избирательный, учитывающий вставку аналог `ownsChildren`, который работает по принципу «всё или ничего» и ограничивает только перемещения, — и обобщённая форма `restrictedTools` из инструмента таблицы, чьё применение жёстко привязано к ячейкам таблицы. Без него контейнерному инструменту приходится защищаться ниже по течению: фильтровать `child.name` при отрисовке, делать свой CSS устойчивым к чужому ребёнку и вычищать залётные блоки из сохранённых документов. В React/Vue/Angular-адаптерах объявляйте его в наборе `statics` спецификации блока, как любое другое статическое поле класса.
class Segments {
static get childTools() {
return { allow: ['segment-item'] };
}
render() {
this.slot = document.createElement('div');
this.slot.setAttribute('data-blok-nested-blocks', '');
return this.slot;
}
}
// Only forbid a few tools, accept everything else
class Callout {
static childTools = { deny: ['table', 'column_list'] };
}
// Framework adapters forward it through `statics`:
export const Segments = createReactBlock({
type: 'segments',
statics: { childTools: { allow: ['segment-item'] } },
component: SegmentsCard,
});setData(newData)
boolean | void | Promise<boolean | void>Необязательный метод вашего инструмента, который применяет новые данные к ЖИВОМУ экземпляру. Объявите его — и `blocks.update()`, отмена/повтор и удалённые совместные правки станут переиспользовать уже отрисованный блок вместо того, чтобы собирать его заново: ни нового экземпляра инструмента, ни нового холдера, поэтому эфемерное состояние (открытое меню, позиция прокрутки, локальное состояние компонента фреймворка), принятые холдеры детей и курсор сохраняются. Без него ядро уничтожает блок и строит замену — именно поэтому хост, вызывавший `blocks.update()` на каждое нажатие клавиши, раньше наблюдал, как блок на компоненте становится пустым. Возвращайте `false`, когда применить данные на месте нельзя — так делает инструмент списка при смене стиля, которой нужна другая форма DOM, — и ядро откатывается к полной пересборке; `true` или ничего означает, что данные применены. Исключение действует так же, как `false` (логируется, затем пересборка). React/Vue/Angular-фабрики блоков реализуют его за вас, поэтому блоки адаптеров автоматически идут по пути обновления на месте.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
newData | BlockToolData | Обязательный | — | Полные данные блока после обновления — существующие данные, объединённые с патчем вызывающей стороны, а не только сам патч. |
class CalloutTool {
setData(newData) {
if (newData.variant !== this.data.variant) {
// A different variant renders a different DOM shape — let core rebuild.
return false;
}
this.data = newData;
this.box.textContent = newData.text ?? '';
return true;
}
}