Skip to content
ФреймворкJavaScript

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

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

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

Методы ниже вызываются на редакторе, созданном через 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[]

Get all available block tool adapters. Each adapter exposes `name` plus tool metadata, including `assetKind` — set to `'image' | 'video' | 'audio' | 'file'` on media tools that store an uploaded asset URL at `data.url`, and `undefined` otherwise. Use it to discover the media-bearing tool set at runtime (instead of hardcoding each tool's data shape) and reconcile a saved document's `data.url`s against your CDN — e.g. to garbage-collect orphaned uploads.

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

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

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

Returns the tools-related configuration of this instance — { tools, inlineToolbar?, tunes?, theme? } — for creating nested Blok editors with the same tool set.

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

tools.update(name, config)

void

Shallow-merge new configuration into an installed tool at runtime — no editor recreation. A `toolbox` key is treated as the tool-level setting (same as `toolbox` in the `tools` map): pass `toolbox: false` to hide the tool from every insertion surface (existing blocks keep rendering) or a toolbox object to (re)show it — permission gating without rebuilding the editor. Under the React adapter this is automatic: change the `toolbox` value in the `tools` prop and `useBlok`/`BlokEditor` applies it in place.

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, с которым была определена — после runtime-вызова 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, если инструмент с указанным именем установлен и доступен в этом экземпляре редактора — блочный, строчный или tune. Публичная интроспекция набора установленных инструментов, например как проверка перед `tools.update(name, config)`, который бросает исключение для неизвестных имён.

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

Защищает runtime-вызовы инструментов — tools.update() бросает исключение для неустановленных имён.

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
namestringОбязательныйИмя зарегистрированного инструмента для проверки.
TypeScript
if (editor.tools.isInstalled('image')) {
  editor.tools.update('image', { uploader: { uploadByFile } });
}

defineTool(toolClass, settings?)

ExternalToolSettings

A registration helper exported from `@bloklabs/core/tools`, not a member of the `tools` namespace. The plain `tools` map types every entry with a bare `ToolSettings` whose `Config` falls back to `Record<string, unknown>`, so a misspelled config key (`defaultLevle` for `defaultLevel`) compiles silently. `defineTool` recovers the tool's real config type from its constructor and applies it to `settings.config`, turning those typos into compile errors. The type-checking happens on the `settings` argument; the RETURN type stays the erased `ExternalToolSettings`, so the result drops straight into the `tools` map. `ExtractToolConfig<TClass>` — the type that does the recovery — is exported alongside it, and falls back to `Record<string, unknown>` for tool classes whose constructor declares no concrete config.

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

The child-holder reconciler for container blocks, exported from `@bloklabs/core/tools`. Call it from your tool's `rendered()` hook — it is what the built-in toggle, callout and column tools use, and what the React/Vue/Angular block adapters run on every commit. It is idempotent and cheap, so run it on every render. Per child it: leaves a holder already inside `container` alone; RECLAIMS a holder stranded in a nested container that ENCLOSES `container`, inserting it at its model position rather than appending it last; leaves holders sitting in any OTHER nested container alone, so two containers can never steal each other's blocks; and mounts everything else at its model position. The reclaim is what makes a container survive the insert ordering: core anchors a newly inserted first child as the container block's DOM sibling, so without it a child of a nested container renders one level out — permanently, when your container's child slot had not been created yet at insert time (a framework portal commits a render after core inserts). Mark `container` with `data-blok-nested-blocks` so the rest of the editor recognises it as a container.

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
containerHTMLElementОбязательныйThe element child holders belong in — the element carrying data-blok-nested-blocks.
children{ holder: HTMLElement }[]ОбязательныйThe block's children in model order, normally 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

The create-vs-restore signal on the tool contract, handed to every block tool's constructor alongside `data`, `block` and `readOnly`. A container tool that seeds default children — a two-column row, a card that starts with a heading — may only do that once, at creation. Every other time the tool is constructed the document already says what its children are, and during a restore those children commonly land a tick AFTER `rendered()` runs, so an empty `api.blocks.getChildren()` there is only transient: seeding on that read fabricates phantom children beside the real ones. CREATION values — seed: `'user'` (a direct editing gesture: Enter, the plus button, the slash menu, block settings, a markdown shortcut), `'api'` (a programmatic `blocks.insert` / `insertMany` / `insertInsideParent`), `'convert'` (a turn-into). RESTORE values — never seed: `'load'` (a document render), `'replay'` (an undo/redo replay or a remote collaborative update), `'paste'` (pasted content that brings its own children), `'probe'` (the OFF-TREE instance `blocks.composeBlockData()` builds to read a tool's default data — it is never inserted, yet it still runs `render()` and `rendered()`, so it must not touch the block tree at all). Blok always supplies it; treat an absent value as `'api'`, and write the check as an allow-list of creation values so a future origin fails closed. Pair it with `blocks.insert(..., origin)` if you drive insertion from your own UI. On the React/Vue/Angular adapters you rarely read it by hand: the block spec's `onCreated` hook already encodes this allow-list, and it fires after the adapter's first commit — the tick at which the block's DOM and its adopted child holders actually exist.

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

BlockToolConstructable.keepsChildrenOnEnter

boolean

A static on your tool CLASS that decides where Enter goes on the container's empty LAST child. By default Blok reads that empty trailing line as the author's way out: with siblings present the line is outdented to the container's own parent, and as a sole child a fresh block is inserted after the whole container — Notion's callout behaviour. A layout container whose children ARE its content (a column, a card, a `steps` block) wants the opposite, and without the declaration the escape strands the new line beside the container. Set it to `true` and the new line stays inside, the same rule the built-in `column`, `column_list` and `toggle` follow. It cannot be inferred from the DOM: a callout renders the very same `data-blok-nested-blocks` slot as a column and deliberately keeps the escape, so this is per-tool policy. Core reads it for the symmetric "remove one indent level" gesture too (Enter/Backspace on a block nested under a PLAIN parent), so a declaring tool is treated as a container there as well and its children never stepwise-outdent out of it. On the React/Vue/Angular adapters, declare it in the block spec's `statics` bag like any other class static.

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[] }

A static on your tool CLASS declaring which block tools may be DIRECT children of its block — and core enforces it everywhere for you. On INSERT a disallowed tool is demoted (never refused, because Enter must always produce a block): the target is the first entry of `allow`, so `allow: ['segment-item']` makes "Enter at the end of a segment" produce another segment instead of a stray paragraph. On MOVE a drag or keyboard reorder that would carry a disallowed block across the container boundary is refused. In the TOOLBOX the disallowed tools are hidden while the caret sits in a child. `deny` wins over `allow` for a tool named in both, and empty lists read as "no restriction". This is the selective, insert-aware counterpart to `ownsChildren`, which is all-or-nothing and clamps moves only — and the generic form of the Table tool's `restrictedTools`, whose enforcement is hard-wired to table cells. Without it a container tool has to defend itself downstream: filtering `child.name` in render, keeping its CSS robust against a foreign child, and migrating strays out of stored documents. On the React/Vue/Angular adapters, declare it in the block spec's `statics` bag like any other class static.

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>

An optional method on your tool that applies new data to the LIVE instance. Declare it and `blocks.update()`, undo/redo and remote collaborative edits all reuse the block you already rendered instead of recomposing it — no new tool instance, no new holder, so ephemeral state (an open menu, a scroll position, a framework component's local state), the adopted child holders and the caret survive. Without it core destroys the block and builds a replacement, which is why a host that called `blocks.update()` per keystroke used to watch a component-backed block go blank. Return `false` when you cannot apply the data in place — the list tool does that for a style change, which needs a different DOM shape — and core falls back to the full recompose; returning `true` or nothing means it was applied. Throwing has the same effect as `false` (logged, then recomposed). The React/Vue/Angular block factories implement it for you, so adapter blocks are on the in-place path automatically.

Параметры

ПараметрТипОбязательныйПо умолчаниюОписание
newDataBlockToolDataОбязательныйThe block's full data after the update — the existing data merged with the caller's patch, not the patch alone.
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;
  }
}