Blocks 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');Методы
blocks.clear()
Promise<void>Удалить все блоки из редактора.
Когда использовать
То же, что clear() верхнего уровня, но на модуле blocks. Используйте, когда уже есть ссылка на editor.blocks.
await editor.blocks.clear();
// All content removed; editor keeps one empty paragraphblocks.render(data)
Promise<void>Отрисовать переданные JSON-данные как блоки, заменив текущий документ. Устойчив к эху: когда входящий документ структурно равен текущему сохранённому содержимому (`time`/`version` игнорируются), вызов оказывается no-op с сохранением курсора — круговому пути `data → render → onSave → setState → data` не нужна дедупликация на стороне потребителя. Принимает нестрогий формат (`LooseOutputData`); редактор делает глубокую копию данных, поэтому переданный объект никогда не мутируется и не удерживается — замороженное состояние хранилища (Redux, Immer) можно передавать напрямую.
Когда использовать
Заменяет весь документ. Повторная передача собственного вывода редактора — no-op с сохранением курсора, дедупликация на стороне потребителя не нужна. Чтобы добавить блоки без очистки, используйте insert() или insertMany().
const data = {
blocks: [
{ id: '1', type: 'paragraph', data: { text: 'Hello World' } },
{ id: '2', type: 'header', data: { text: 'Title', level: 1 } }
]
};
await editor.blocks.render(data);blocks.renderFromHTML(data)
Promise<void>Отрисовать HTML-строку как блоки, преобразовав её в формат блоков.
Когда использовать
Удобно для импорта старого HTML, но результат зависит от обработчиков вставки инструментов — JSON через render() сохраняет больше точности, когда источник под контролем.
const html = '<h1>Title</h1><p>Hello World</p>';
await editor.blocks.renderFromHTML(html);
// HTML is converted to appropriate blocksblocks.importMarkdown(md, options?)
Promise<OutputData>Преобразовать Markdown-строку в блоки и отрисовать их, ЗАМЕНИВ текущий документ, — внутри вызывается `blocks.render()`. Конвертер загружается лениво при первом вызове, а OutputData, которым резолвится промис, — это отрисованный документ. `options` — это `MarkdownImportConfig` (сопоставление инструментов, переключатель GFM, расширения micromark/mdast). Для аддитивной вставки используйте `markdownToBlocks()` из отдельного подпути `@bloklabs/core/markdown` вместе с `blocks.insertMany()`.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
md | string | Обязательный | — | Исходная Markdown-строка. |
options | MarkdownImportConfig | — | undefined | Сопоставление инструментов, переключатель GFM и расширения micromark/mdast. |
const data = await editor.blocks.importMarkdown('# Title\n\n- one\n- two');
// The whole document is replaced; data is the rendered OutputDatablocks.exportMarkdown()
Promise<string>Сериализовать текущий документ в Markdown — исходящий близнец `importMarkdown`. Блоки читаются через Saver, поэтому вывод отражает сохранённый (провалидированный) документ, а не сырой DOM, и промис резолвится в '', когда сохранять нечего. Блоки, принадлежащие ячейке таблицы, сериализуются внутри pipe-таблицы, а не повторяются как отдельные строки. То, что Markdown выразить не может, деградирует: `colspan`/`rowspan` таблицы и столбцы заголовков отбрасываются, а таблица без строки заголовков получает пустую строку заголовка, поскольку GFM её требует.
const md = await editor.blocks.exportMarkdown();
// → '# Title\n\n- one\n- two'blocks.delete(index?, setCaret?)
Promise<void>Удалить блок по указанному индексу или текущий блок, если индекс не передан.
Когда использовать
Без индекса удаляет блок в фокусе. Удаление последнего оставшегося блока автоматически вставляет свежий пустой параграф, поэтому редактор никогда не остаётся без цели для курсора.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
index | number | — | current block index | Индекс блока для удаления. |
setCaret | boolean | — | true | Перемещать ли курсор на уцелевший блок после удаления; передайте false, чтобы не отбирать курсор у пользователя при программном удалении. |
// Delete current block
await editor.blocks.delete();
// Delete block at index 0
await editor.blocks.delete(0);
// Delete without moving the user's caret (programmatic deletion)
await editor.blocks.delete(0, false);blocks.move(toIndex, fromIndex?)
voidПеремещает блок на новую позицию. Если fromIndex не передан, перемещает текущий блок.
Когда использовать
toIndex — это индекс после удаления: при перемещении вперёд вычтите 1, иначе блок встанет на позицию дальше нужной.
// Move current block to top
editor.blocks.move(0);
// Move block from index 2 to index 0
editor.blocks.move(0, 2);blocks.getBlockByIndex(index)
BlockAPI | undefinedПолучить объект BlockAPI для блока по указанному индексу.
Когда использовать
Возвращает undefined для индексов вне диапазона — вызов метода на undefined приведёт к ошибке, поэтому сначала проверьте результат.
const block = editor.blocks.getBlockByIndex(0);
if (block) {
console.log(block.id, block.name);
}blocks.getById(id)
BlockAPI | nullПолучить объект BlockAPI для блока с указанным ID.
Когда использовать
Надёжный способ ссылаться на блок между правками, так как индексы смещаются. Возвращает null, если id больше не существует.
const block = editor.blocks.getById('block-123');
if (block) {
await editor.blocks.update(block.id, { text: 'New content' });
}blocks.getCurrentBlockIndex()
numberПолучить индекс блока, который сейчас в фокусе.
Когда использовать
Отражает блок с курсором. Возвращает -1, когда ничего не в фокусе, — проверяйте перед использованием как индекс.
const index = editor.blocks.getCurrentBlockIndex();
console.log('Current block index:', index);blocks.getBlockIndex(blockId)
number | undefinedПолучить индекс блока по его ID.
Когда использовать
Узнаёт текущую позицию известного блока; сочетайте с getById(), когда есть id, а нужна позиция.
const index = editor.blocks.getBlockIndex('block-123');
if (index !== undefined) {
console.log('Block is at index:', index);
}blocks.getBlockByElement(element)
BlockAPI | undefinedПолучить объект BlockAPI для блока, который содержит переданный HTML-элемент.
Когда использовать
Мост от DOM-узла (например, цели клика) обратно к его блоку — полезно в собственных обработчиках.
document.addEventListener('click', (e) => {
const block = editor.blocks.getBlockByElement(e.target);
if (block) {
console.log('Clicked on block:', block.id);
}
});blocks.scrollToBlock(id)
voidПрокрутить страницу к блоку, выбрать его, мигнуть подсветкой прибытия и объявить переход вспомогательным технологиям — публичный аналог прокрутки по хешу URL при загрузке. No-op, когда в документе нет блока с таким id. Адаптеры фреймворков, монтирующиеся в отсоединённый холдер (React/Vue/Angular), отрисовывают засеянный контент до того, как он попадёт на страницу, поэтому загрузочная прокрутка по хешу откладывается; @bloklabs/react выполняет её автоматически, как только холдер подключается, — для глубоких ссылок вызывайте этот метод сами после готовности редактора.
editor.blocks.scrollToBlock(nodeId);blocks.getChildren(parentId)
BlockAPI[]Получить все дочерние блоки родительского блока-контейнера.
Когда использовать
Возвращает прямых потомков блока-контейнера (колонки, тоггла или базы данных). Пустой массив, если потомков нет.
const children = editor.blocks.getChildren('parent-block-id');
children.forEach(child => {
console.log('Child:', child.id);
});blocks.setBlockParent(blockId, parentId)
voidСменить родителя блока: обновляет `parentId` блока и `contentIds` родителя через единственную в ядре точку смены родителя. Передайте `null`, чтобы вернуть блок на корневой уровень. Неизвестный `blockId` — это no-op с записью предупреждения в лог; `parentId`, который сделал бы блок потомком самого себя, выбрасывает ошибку.
// Move a block into a container block
editor.blocks.setBlockParent('child-block-id', 'parent-block-id');
// Move it back to the root level
editor.blocks.setBlockParent('child-block-id', null);blocks.insertInsideParent(parentId, insertIndex, childData?, toolName?)
BlockAPIВставить блок как дочерний для `parentId` атомарно: создание и назначение родителя объединяются в ОДИН шаг отмены, поэтому одно нажатие Cmd+Z убирает его полностью (предпочтительнее, чем `insert()` с последующей сменой родителя, где шага два). `insertIndex` — это ПЛОСКИЙ индекс документа, по которому должен появиться дочерний блок. `toolName` выбирает блочный инструмент дочернего блока и по умолчанию равен `config.defaultBlock`; инструмент, запрещённый внутри ячеек таблицы, понижается до блока по умолчанию, когда новый дочерний блок оказался бы внутри такой ячейки, а незарегистрированное имя выбрасывает ошибку до того, как что-либо будет записано. `childData` по умолчанию равен `{ text: '' }` для блока по умолчанию и `{}` — позволяя инструменту применить собственные значения по умолчанию — всякий раз, когда `toolName` называет другой инструмент.
const parentIndex = editor.blocks.getBlockIndex('parent-block-id') ?? 0;
const child = editor.blocks.insertInsideParent(
'parent-block-id',
parentIndex + 1,
{ text: 'Nested content' },
);
// A TYPED child in the same single undo entry — no insert-then-reparent
editor.blocks.insertInsideParent(
'parent-block-id',
parentIndex + 1,
{ text: 'Nested heading', level: 2 },
'header',
);blocks.getBlocksCount()
numberПолучить общее количество блоков в редакторе.
Когда использовать
Считает все блоки, включая вложенные дочерние (колонки, тогглы, ячейки таблиц), а не только блоки верхнего уровня. Используйте для границ перед insert() или move() по индексу.
const count = editor.blocks.getBlocksCount();
console.log('Total blocks:', count);blocks.insert(type?, data?, config?, index?, needToFocus?, replace?, id?, tunes?, origin?)
BlockAPIВставить новый блок с полным контролем над его свойствами и позицией.
Когда использовать
Все параметры необязательны — insert() даёт пустой параграф, либо передайте type/data/index для контроля. replace: true заменяет блок по индексу index.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
type | string | — | config.defaultBlock | Имя инструмента для создания блока. |
data | BlockToolData | — | {} | Начальные данные инструмента для нового блока. |
config | ToolConfig | — | {} | Игнорируется — принимается только ради сохранения позиционной сигнатуры. Редактор связывает его с _config и никогда не читает, а опции вставки в block-manager не содержат поля config, поэтому созданный блок использует конфигурацию инструмента уровня редактора из config.tools. Передавайте undefined. |
index | number | — | current block index + 1 | Позиция для вставки блока. |
needToFocus | boolean | — | true | Переводить ли фокус на вставленный блок. Передайте false, чтобы вставить блок, не перемещая курсор. |
replace | boolean | — | false | Заменить существующий блок по индексу вместо вставки рядом с ним. |
id | string | — | auto-generated | Собственный id для нового блока. |
tunes | { [name: string]: BlockTuneData } | — | undefined | Необязательные данные тюнов, применяемые при создании, с ключами по имени тюна. |
origin | BlockOrigin | — | 'api' | Почему блок создаётся. Передаётся в конструктор инструмента как origin — именно так контейнерный инструмент отличает настоящее создание (можно засеять детей по умолчанию) от повторной материализации: загрузки документа, воспроизведения undo/redo или вставки (засевать нельзя). Передавайте 'user', когда вставка идёт из вашего собственного интерфейса — своей панели, тулбокса или горячей клавиши, — чтобы встроенные контейнеры Blok (column_list, column) и ваши собственные вели себя так же, как при вставке из штатного меню. Для программных вставок и перезагрузки данных параметр можно не передавать: значение по умолчанию 'api' тоже считается созданием, но никогда не выдаётся за жест пользователя. |
Ошибки
Не указан
type, и в конфигурации не заданdefaultBlock.Could not insert Block. Tool name is not specified.
Передайте явный
typeили задайтеdefaultBlockв конфигурации редактора.Определённое имя инструмента не зарегистрировано в редакторе.
Could not compose Block. Tool «<type>» not found.
Зарегистрируйте инструмент в конфигурации
toolsредактора перед вставкой блока этого типа.Передан
replace: true, но поindexнет блока.Could not replace Block at index <index>. Block not found.
Проверьте
indexчерезblocks.getBlocksCount()перед вызовом сreplace: true.
// Insert after the current block with the default type
const block = editor.blocks.insert();
// → BlockAPI { id: 'kP3xQ...', name: 'paragraph', ... }
// Insert paragraph with data at index 0
const block = editor.blocks.insert('paragraph', { text: 'Hello' }, undefined, 0);
// Insert with custom ID
const block = editor.blocks.insert('header', { text: 'Title' }, undefined, undefined, undefined, undefined, 'custom-id');
// From your own slash menu / toolbar: declare the user gesture so a container
// tool seeds its default children (see BlockOrigin on the tool contract)
const block = editor.blocks.insert('column_list', undefined, undefined, index, undefined, undefined, undefined, undefined, 'user');blocks.insertMany(blocks, index?)
BlockAPI[]Вставить несколько блоков за раз. Когда индекс не передан, блоки добавляются в конец документа.
Когда использовать
Массовая вставка за одну операцию (и один шаг отмены) — гораздо дешевле цикла из insert() для больших вставок или импорта.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
blocks | OutputBlockData[] | LooseOutputBlockData[] | Обязательный | — | Блоки для вставки. Принимаются нестрогие блоки — null в data становится {}, null/пустой id заменяется сгенерированным. |
index | number | — | end of document | Позиция для вставки. Когда не передана, по умолчанию блоки добавляются в конец документа. |
Ошибки
Переданный
indexотрицательный.Index should be greater than or equal to 0
Передайте
indexне меньше 0 либо не передавайте его, чтобы добавить в конец.
const blocksToInsert = [
{ id: '1', type: 'paragraph', data: { text: 'First' } },
{ id: '2', type: 'paragraph', data: { text: 'Second' } }
];
const inserted = editor.blocks.insertMany(blocksToInsert, 0);
console.log('Inserted:', inserted.length, 'blocks');blocks.composeBlockData(toolName)
Promise<BlockToolData>Создать пустые данные блока для указанного типа инструмента.
Когда использовать
Создаёт данные инструмента по умолчанию, ничего не вставляя — удобно, когда нужен корректный пустой payload для insert() или update().
Ошибки
toolNameне является зарегистрированным инструментом.Block Tool with type "<toolName>" not found
Сначала зарегистрируйте инструмент в конфигурации
toolsредактора.
const emptyData = await editor.blocks.composeBlockData('paragraph');
// Returns: { text: '' } or appropriate empty state for the toolblocks.update(id, data?, tunes?)
Promise<BlockAPI>Обновить данные и/или тюны блока. Когда инструмент блока реализует `setData(newData)` в своём прототипе — так делает каждый React/Vue/Angular-блок, а также встроенные инструменты заголовка, списка, кода, тоггла и таблицы, — обновление данных применяется НА МЕСТЕ: тот же экземпляр блока, тот же DOM-холдер, тот же смонтированный компонент продолжают жить, поэтому эфемерное состояние инструмента, принятые дочерние блоки и курсор сохраняются. Именно поэтому `update()` безопасно вызывать на каждое нажатие клавиши (переименование карточки, пока пользователь печатает). Инструменты без `setData`, инструмент, чей `setData` возвращает `false` (ему нужна другая форма DOM), и любой вызов, передающий `tunes`, откатываются к пересборке блока: новый экземпляр инструмента заменяет старый, а старый уничтожается.
Когда использовать
Изменяет data/tunes на месте, сохраняя id и тип блока. Чтобы сменить тип, используйте convert().
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
id | string | Обязательный | — | Id блока для обновления. |
data | Partial<BlockToolData> | — | undefined | Частичные данные, объединяемые с существующими данными блока. |
tunes | Record<string, BlockTuneData> | — | undefined | Данные тюнов, объединяемые с существующими тюнами блока. |
Ошибки
Блок с указанным
idне существует.Block with id "<id>" not found
Проверьте id через
blocks.getById()перед вызовомupdate().
// Update block data
const block = await editor.blocks.update('block-123', { text: 'New text' });
// Update with tunes
const block = await editor.blocks.update('block-123', undefined, { alignment: 'center' });
// Guard against an id that no longer exists (e.g. the block was deleted
// concurrently) instead of letting the rejection surface unhandled
try {
await editor.blocks.update(staleId, { text: 'New text' });
} catch {
console.warn('Block was already removed, skipping update');
}blocks.convert(id, newType, dataOverrides?)
Promise<BlockAPI>Преобразовать блок в другой тип. Оба инструмента должны поддерживать конфигурацию преобразования.
Когда использовать
И исходный, и целевой инструменты должны объявлять conversionConfig, иначе вызов выбросит ошибку. По возможности сохраняет текст.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
id | string | Обязательный | — | Id блока для преобразования. Его инструмент должен объявлять conversionConfig.export. |
newType | string | Обязательный | — | Имя зарегистрированного инструмента для преобразования. Его инструмент должен объявлять conversionConfig.import. |
dataOverrides | BlockToolData | — | undefined | Поля данных для перезаписи в результирующем блоке после преобразования. |
Ошибки
Блок с указанным
idне существует.Block with id "<id>" not found
Проверьте id через
blocks.getById()перед вызовомconvert().newTypeне является зарегистрированным инструментом.Block Tool with type "<newType>" not found
Зарегистрируйте целевой инструмент в конфигурации
toolsредактора.У исходного инструмента нет
conversionConfig.export, у целевого нетconversionConfig.import, либо нет ни того, ни другого.Conversion from "<sourceType>" to "<newType>" is not possible. <ToolName(s)> tool(s) should provide a "conversionConfig"
Добавьте
conversionConfigтому инструменту, у которого его нет, либо конвертируйте через промежуточный инструмент, поддерживающий оба направления.
// Convert paragraph to header
const headerBlock = await editor.blocks.convert('block-123', 'header', { level: 2 });
// Convert with data overrides
const headerBlock = await editor.blocks.convert('block-123', 'header', { text: 'New Title', level: 1 });
// Not every pair of tools supports conversion — guard it rather than
// assuming the target type is always convertible
try {
await editor.blocks.convert('block-123', 'table');
} catch (error) {
console.warn('Conversion not supported between these tools:', error);
}blocks.splitBlock(currentBlockId, currentBlockData, newBlockType, newBlockData, insertIndex)
BlockAPIАтомарно разделить блок, обновив текущий блок и вставив новый. Обе операции объединяются в один шаг отмены.
Когда использовать
Используйте для разбиения с учётом курсора (например, Enter посреди текста); update и insert образуют один шаг отмены.
// Split a paragraph at cursor position
const newBlock = editor.blocks.splitBlock(
'current-block-id',
{ text: 'First part' },
'paragraph',
{ text: 'Second part' },
1
);blocks.stopBlockMutationWatching(index)
voidОстановить наблюдение за мутациями блока по указанному индексу. Используйте это, чтобы предотвратить ложные события block-changed при операциях замены блока.
Когда использовать
Нишевое: вызывайте перед программной заменой DOM блока, чтобы подавить ложные события block-changed; следующий рендер включит наблюдение снова.
// Replace a block without triggering change events
editor.blocks.stopBlockMutationWatching(0);
// Perform block replacement...
// Mutation observer will not fire for this blockblocks.startBlockMutationWatching(blockId)
voidСнова включить наблюдение за мутациями блока, ранее заглушённого через `stopBlockMutationWatching`. Метод принимает **id**, а не индекс, потому что вставки и замены между двумя вызовами сдвигают индексы; несуществующий id молча пропускается — блок, заменённый на месте, был создан с собственным наблюдателем.
const blockId = editor.blocks.getBlockByIndex(0)?.id;
editor.blocks.stopBlockMutationWatching(0);
// Perform block replacement...
if (blockId) {
editor.blocks.startBlockMutationWatching(blockId);
}blocks.transact(fn)
voidОбъединить каждую операцию с блоками, выполненную внутри `fn`, в один шаг отмены. `fn` должна быть СИНХРОННОЙ — операции, попадающие туда после await, в группу уже не входят. Используйте это, чтобы структурные правки нельзя было отменить частично.
editor.blocks.transact(() => {
editor.blocks.insert('paragraph', { text: 'One' });
editor.blocks.insert('paragraph', { text: 'Two' });
});
// A single Cmd+Z removes both blocksblocks.beginTransaction()
voidОткрыть группу отмены, которая остаётся открытой через асинхронные границы. Каждая операция с блоками до `endTransaction()` попадает в один шаг отмены. Используйте это для жестов указателя, непрерывно меняющих документ (перетаскивание угла таблицы для добавления строк), где `transact()` не помогает, потому что оборачивает только синхронную функцию. Каждый вызов должен быть сопряжён с `endTransaction()`.
editor.blocks.beginTransaction();
// ... continuous mutations across async boundaries ...
editor.blocks.endTransaction();blocks.endTransaction()
voidЗакрыть группу отмены, открытую через `beginTransaction()`.
editor.blocks.beginTransaction();
// ... continuous mutations across async boundaries ...
editor.blocks.endTransaction();blocks.transactWithoutCapture(fn)
voidВыполнить операции с блоками, ничего не записывая в историю отмен. Используйте это для автопочинки и нормализации (например, чтобы в пустой ячейке всегда был блок), через которые Cmd+Z никогда не должен проходить.
editor.blocks.transactWithoutCapture(() => {
editor.blocks.insert('paragraph', {}, undefined, 0);
});
// Nothing was added to the undo historyblocks.setPointerDragActive(active)
voidСообщить ядру, что перетаскивание указателем началось или закончилось. Пока оно активно, синхронизации Yjs, запускаемые мутациями DOM, подавляются, чтобы непрерывные изменения DOM в браузере во время перетаскивания не могли испортить состояние Yjs.
editor.blocks.setPointerDragActive(true);
// ... run the drag gesture ...
editor.blocks.setPointerDragActive(false);Свойства
| Свойство | Тип | Описание |
|---|---|---|
isSyncingFromYjs | boolean | Геттер только для чтения — true, пока идёт операция синхронизации Yjs (отмена/повтор). Инструменты читают его, чтобы пропустить очистку, которая конфликтовала бы с состоянием отмены. Обратите внимание: это СВОЙСТВО на `editor.blocks`, в отличие от метода `isSyncingFromYjs()` React-хука. |
isPointerDragActive | boolean | Геттер только для чтения — true, пока активно перетаскивание указателем. Адаптеры фреймворков читают его, чтобы отложить программный `dispatchChange` посреди перетаскивания (ядро молча отбрасывает такое изменение) и отправить его повторно, когда перетаскивание закончится. |