Перейти к содержимому
ФреймворкJavaScript

Tools API: регистрация и обновление инструментов

Доступ и управление инструментами редактора.

Обновлено 24 сент. 2026 г.Редактировать на GitHub

Как получить экземпляр редактора

Методы ниже вызываются на редакторе, созданном через new Blok(). Они доступны после того, как разрешится editor.isReady.

TypeScript
// 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.

  • У медиаинструментов, которые хранят URL загруженного ресурса в data.url, assetKind равен 'image' | 'video' | 'audio' | 'file'.
  • У остальных инструментов он undefined.

Используйте это, чтобы во время выполнения найти инструменты с медиа, а не прописывать форму данных каждого инструмента вручную. Затем можно сверить data.url сохранённого документа с вашим CDN, например чтобы удалить осиротевшие загрузки.

Когда использовать

Перечисляет зарегистрированные блочные инструменты во время выполнения. Удобно для своего выбора блоков или для отладки конфигурации инструментов.

TypeScript
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 с тем же набором инструментов.

TypeScript
const nested = new Blok({ holder, ...editor.tools.getToolsConfig() });

tools.update(name, config)

void

Поверхностно объединяет новую конфигурацию с установленным инструментом во время выполнения, без пересоздания редактора. Ключ toolbox трактуется как настройка уровня инструмента, та же, что toolbox в карте tools.

  • Передайте toolbox: false, чтобы скрыть инструмент со всех поверхностей вставки; существующие блоки продолжают отрисовываться.
  • Передайте объект toolbox, чтобы снова его показать.

Так вы разграничиваете права без пересборки редактора. В React-адаптере это происходит автоматически: измените значение toolbox в пропе tools, и useBlok/BlokEditor применит его на месте.

TypeScript
// 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)

void

Runtime-сеттер глобальной опции inlineToolbar. Он заново назначает строчные инструменты каждому блочному инструменту и пересобирает мемоизированные конфигурации санитизации.

  • Санитизация при вставке следует новому набору сразу.
  • Строчная панель инструментов отражает его при следующем выделении.
  • Настройки inlineToolbar на уровне инструмента, и массивы, и отказы, остаются приоритетными.

Передайте true для всех строчных инструментов, false, чтобы отключить панель, или список имён строчных инструментов. Список выбирает, какие инструменты показывать, а их порядок слева направо остаётся фиксированным.

Если вы отображаете сохранённый контент через @bloklabs/core/view, учтите: viewSchema собирается из того значения inlineToolbar, с которым она была определена. После рантайм-вызова setInlineToolbar с пользовательскими строчными инструментами пересоберите её через defineBlokSchema, прежде чем вызывать blocksToHtml.

Когда использовать

Вступает в силу при следующем выделении. Санитизация при вставке пересобирается сразу, поэтому вставленный контент следует новому набору строчных инструментов немедленно. Если вы отображаете сохранённый контент через @bloklabs/core/view, учтите: viewSchema собирается из того значения inlineToolbar, с которым она была определена. После рантайм-вызова setInlineToolbar с пользовательскими строчными инструментами пересоберите её через defineBlokSchema, прежде чем вызывать blocksToHtml.

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
configboolean | string[]Обязательный—

true включает все зарегистрированные строчные инструменты. false отключает строчную панель инструментов. Массив ограничивает её перечисленными строчными инструментами в этом порядке.

TypeScript
// 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() бросает исключение для неустановленных имён.

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
namestringОбязательный—

Имя зарегистрированного инструмента для проверки.

TypeScript
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>. Он экспортируется рядом с defineTool и откатывается к Record<string, unknown> для классов инструментов, чей конструктор не объявляет конкретной конфигурации.

TypeScript
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, чтобы остальной редактор распознавал его как контейнер.

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
containerHTMLElementОбязательный—

Элемент, в котором должны находиться холдеры детей. Это элемент, несущий data-blok-nested-blocks.

children{ holder: HTMLElement }[]Обязательный—

Дети блока в порядке модели, обычно api.blocks.getChildren(blockId).

TypeScript
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

Сигнал «создание или восстановление» в контракте инструмента. Blok передаёт его в конструктор каждого блочного инструмента рядом с data, block и readOnly.

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

При восстановлении эти дети обычно приходят на тик ПОЗЖЕ вызова rendered(). Поэтому пустой api.blocks.getChildren() там лишь временный. Засев по такому чтению создаёт фантомных детей рядом с настоящими.

Значения СОЗДАНИЯ, при которых засевать можно: 'user', 'api' и 'convert'.

  • 'user' это прямой жест редактирования: Enter, кнопка «плюс», тулбокс по «/», настройки блока, Markdown-сокращение.
  • 'api' это программные blocks.insert / insertMany / insertInsideParent.
  • 'convert' это преобразование блока.

Значения ВОССТАНОВЛЕНИЯ, при которых засевать нельзя: 'load', 'replay', 'paste' и 'probe'.

  • 'load' это отрисовка документа.
  • 'replay' это воспроизведение отмены или повтора либо удалённое совместное изменение.
  • 'paste' это вставленное содержимое, которое приносит своих детей.
  • 'probe' это экземпляр ВНЕ ДЕРЕВА, который blocks.composeBlockData() создаёт, чтобы прочитать данные инструмента по умолчанию.

Экземпляр probe никогда не вставляется, но всё равно выполняет render() и rendered(). Поэтому он вообще не должен трогать дерево блоков.

Blok всегда передаёт это значение. Отсутствующее значение трактуйте как 'api'. Проверку пишите как белый список значений создания, чтобы будущее значение origin не прошло по ошибке. Сочетайте его с blocks.insert(..., origin), если вы запускаете вставку из своего интерфейса.

В React/Vue/Angular-адаптерах вы редко читаете его вручную. Хук onCreated в спецификации блока уже кодирует этот белый список. Он срабатывает после первого коммита адаптера, в тот тик, когда DOM блока и принятые им холдеры детей действительно существуют.

TypeScript
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();
  }
}

BlockToolConstructorOptions.replaySource

'history' | 'remote' | undefined

Кто пересобрал блок. Задаётся только при origin: 'replay'.

  • 'history': undo или redo этого клиента.
  • 'remote': пришло изменение другого участника совместного редактирования.

Изменение другого участника пересобирает блок у каждого клиента. Поэтому побочный эффект, который должен выполнить только действующий клиент (сетевой запрос или производная запись), относится к 'history'. Для остальных origin значение undefined.

TypeScript
class Bookmark {
  constructor({ data, origin, replaySource }) {
    // Only the client that redid the paste fetches the preview again.
    this.refetch = origin === 'replay' && replaySource !== 'remote' && data.title === undefined;
  }
}

BlockToolConstructable.keepsChildrenOnEnter

boolean

Статическое поле на КЛАССЕ вашего инструмента, которое решает, куда уходит Enter на пустом ПОСЛЕДНЕМ ребёнке контейнера. По умолчанию Blok читает эту пустую последнюю строку как способ автора выйти наружу.

  • Если рядом есть соседи, строка выносится к родителю самого контейнера.
  • Если она единственный ребёнок, Blok вставляет новый блок после всего контейнера.

Так ведёт себя выноска в Notion. Контейнеру для вёрстки, чьи дети И ЕСТЬ его содержимое (колонка, карточка, блок steps), нужно обратное. Такому инструменту нужно это объявление: без него выход оставляет новую строку рядом с контейнером.

Задайте true, и новая строка остаётся внутри. Это то же правило, которому следуют встроенные column, column_list и toggle. Вывести его из DOM нельзя: выноска отрисовывает ровно тот же слот data-blok-nested-blocks, что и колонка, и намеренно сохраняет выход, поэтому это политика уровня инструмента.

Ядро читает его и для симметричного жеста «убрать один уровень отступа» (Enter/Backspace на блоке, вложенном в ОБЫЧНОГО родителя). Объявивший инструмент и там считается контейнером, а его дети никогда не выходят из него по одному уровню отступа.

В React/Vue/Angular-адаптерах объявляйте его в наборе statics спецификации блока, как любое другое статическое поле класса.

TypeScript
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 спецификации блока, как любое другое статическое поле класса.

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

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
newDataBlockToolDataОбязательный—

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

TypeScript
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;
  }
}