Параметры конфигурации Blok
Объект конфигурации, передаваемый в конструктор Blok. Формально он разделён на два типа: `BlokMountOptions` — опции, фиксированные на всё время жизни экземпляра (holder, tools, i18n, …) — и `BlokState`, ЖИВЫЕ поля: `readOnly` (включая `hideControls`), `hideToolbar`, `toolbarPosition`, `inlineToolbar`, а также колбэки редактора `onChange`, `onSave`, `onEnter`, `onSubmit`, `onBeforeRender` и `onAfterRender`. Каждому полю `BlokState` соответствует документированный рантайм-сеттер (`readOnly.set`, `toolbar.setHidden`, `toolbar.setPosition`, `tools.setInlineToolbar`, `handlers.set`), поэтому его изменение никогда не требует пересоздания редактора — и адаптеры React, Vue и Angular реагируют на эти props/inputs на месте. Само НАЛИЧИЕ колбэка тоже значимо (наличие `onSubmit` превращает Enter в «сериализовать и отправить», наличие `onSave` включает конвейер отслеживания изменений), поэтому `handlers.set` принимает и `undefined`, чтобы снять обработчик. `BlokConfig = BlokMountOptions & BlokState`, так что существующий код компилируется без изменений.
import { Blok, type BlokConfig } from '@bloklabs/core';
const config: BlokConfig = {
holder: 'editor',
placeholder: 'Start writing...',
autofocus: true,
readOnly: false,
minHeight: 300,
};
const editor = new Blok(config);Конфигурация
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
holder | string | HTMLElement | 'blok' | Контейнер (ID или элемент DOM) |
tools | Record<string, ToolConstructable | ToolSettings> | {} | Доступные блочные и строчные инструменты. По умолчанию не регистрируется ничего: значение `{}` оставляет только внутренние инструменты Blok (`stub`, `delete`, `copyLink`, `convertTo`), поэтому голый `new Blok({ holder })` не сможет отрисовать даже параграф. Из `@bloklabs/core/full` экспортируются два готовых набора — `defaultTools` (параграф, заголовок и список, все с `inlineToolbar: true`) и `allTools` (`defaultTools` плюс цитата, выноска, код, тоггл и все строчные инструменты). `toolbox: false` у отдельного инструмента оставляет его зарегистрированным (существующие блоки продолжают отображаться, blocks.insert() продолжает работать), но убирает его со всех путей вставки пользователем — из тулбокса, из меню преобразования и из сочетания клавиш. Удобно для разграничения прав — и переключается на лету через `tools.update(name, { toolbox })` (React-адаптер применяет изменения значений `toolbox` в пропе `tools` автоматически), так что смена прав никогда не требует пересоздания редактора. |
tunes | string[] | undefined | Имена блочных тюнов, добавляемых каждому блочному инструменту, который не объявляет собственный набор `tunes`. Сами классы тюнов должны быть зарегистрированы в конфигурации `tools` (класс со `static isTune = true`). |
placeholder | string | false | false | Плейсхолдер, который получает каждый блок инструмента по умолчанию — не только первый блок и не только пока документ пуст. Со встроенным параграфом он виден всякий раз, когда блок пуст и находится в фокусе. Учтите, что `false` (значение по умолчанию) не убирает плейсхолдер: инструмент параграфа тогда использует собственный встроенный локализованный текст («Write something or press / to select a tool»). Чтобы очистить его, задайте инструменту по умолчанию собственный пустой placeholder — `tools: { paragraph: { class: Paragraph, placeholder: '' } }`. |
minHeight | number | 300 | Высота в px нижней кликабельной зоны редактора |
captureClicksBelowEditor | boolean | false | Опция по требованию: клики по странице ниже редактора добавляют блок, не занимая места в раскладке. Сочетайте с `minHeight: 0`, чтобы полностью убрать нижнюю зону. Срабатывают только клики по пустому фону элемента, содержащего редактор — клики по вашему контенту, отрисованному ниже, игнорируются, а всплытие события не останавливается, так что обработчики кликов хост-страницы продолжают работать. |
defaultBlock | string | 'paragraph' | Тип блока по умолчанию |
data | OutputData | LooseOutputData | null | undefined | Начальные данные для отображения. Принимается нестрогий формат: значения `null` для `data`, `id` или `time` блока (частые в backend-DTO) нормализуются на границе. Документ целиком, равный `null`, тоже допустим и превращается в пустой документ, поэтому nullable-состояние контролируемого компонента можно передавать как есть, без обёртки вида `value ?? { blocks: [] }`. |
dataModel | 'legacy' | 'hierarchical' | 'auto' | 'auto' | Модель входных/выходных данных. 'auto' определяет формат данных, которые вы отображаете, и сохраняет их в том же формате; 'legacy' всегда использует вложенную структуру `items[]`; 'hierarchical' всегда использует плоские блоки со ссылками `parent`/`content`. |
sanitizer | SanitizerConfig | {} | Общий для редактора allowlist санитайзера по умолчанию. Объединяется с собственными правилами `sanitize` каждого инструмента и применяется при сохранении, при рендере, при вставке и при копировании выбранных блоков. |
readOnly | boolean | { hideControls?: boolean } | false | Включить режим только для чтения. Передайте `{ hideControls: true }`, чтобы также скрыть hover-панель инструментов, настройки блока и строчную панель. Живое поле: меняется на лету через `readOnly.set(state, { hideControls })` — тот же экземпляр переключает режим на месте, сохраняя курсор, историю отмен и прокрутку. |
onChange | (api: API, event: BlockMutationEvent | BlockMutationEvent[]) => void | undefined | Функция обратного вызова при изменении; аргумент event несёт произошедшую мутацию (или массив мутаций, если несколько срабатывают одновременно) Задержка доставки ограничена, поэтому от него можно питать интерфейс: первое изменение простаивающего документа приходит уже на следующем микротаске — в том же кадре, в котором пользователь печатал, — а последующие изменения объединяются в один дополнительный вызов в конце короткого окна пакетирования, которое новые изменения не продлевают. Живое: устанавливается, заменяется или снимается в рантайме через `handlers.set({ onChange })` — именно его наличие (вместе с `onSave`) включает конвейер отслеживания изменений. |
onSave | (data: OutputData, api: API) => void | undefined | Реактивный колбэк сохранения — срабатывает автоматически с полным сериализованным содержимым при каждом пакете изменений, поэтому save() вручную вызывать не нужно. Он срабатывает только по завершении окна пакетирования и, в отличие от onChange, никогда не опережает его: сериализовать весь документ слишком дорого. Живое: устанавливается, заменяется или снимается в рантайме через `handlers.set({ onSave })` — само его наличие заставляет Blok сериализовать документ на каждую пачку изменений. |
onReady | (blok?: Blok) => void | undefined | Срабатывает один раз, когда редактор становится готов, получая полностью инициализированный экземпляр Blok |
onEnter | (event: KeyboardEvent, api: API) => boolean | void | undefined | Срабатывает при нажатии Enter в блоке, прежде чем Blok разделит его или создаст новый. Верните true, чтобы пометить событие обработанным — Blok отменит своё разделение/создание блока (нативный перенос строки всё равно предотвращается). Не срабатывает для инструментов с enableLineBreaks и пока Enter принадлежит поповеру или панели инструментов, а также для Shift+Enter с мягким переносом строки — кроме iOS, где Safari сообщает Shift+Enter для завершающего предложение «. », и Blok создаёт блок, поэтому хук срабатывает и там. Подходит для чат-полей («Enter отправляет») — сочетайте с настройкой preserveBlank параграфа вместо наследования от Paragraph. Живое: устанавливается, заменяется или снимается в рантайме через `handlers.set({ onEnter })`. |
onSubmit | (data: OutputData, api: API) => void | undefined | Срабатывает с полными сериализованными OutputData на том Enter, который иначе создал бы или разделил блок, — жест «Enter отправляет». Blok сериализует документ и подавляет разделение по умолчанию, поэтому вызывать save() внутри onEnter вручную не нужно. Наследует все исключения onEnter; если заданы оба, onEnter, вернувший true, имеет приоритет и подавляет onSubmit. Живое: устанавливается, заменяется или снимается в рантайме через `handlers.set({ onSubmit })` — передайте `undefined`, чтобы вернуть стандартное поведение Enter (разбиение блока) без пересоздания редактора. |
onError | (error: Error, context: { source: 'save' }) => void | undefined | Срабатывает, когда операция редактора завершается ошибкой, которую Blok иначе только записал бы в лог; сегодня единственный источник — сериализация. Через него проходят и отложенное автосохранение, и явный save(). Неудавшийся save() отклоняет промис с исходной ошибкой — он никогда не разрешается значением undefined, — поэтому оборачивайте явные сохранения в try/catch; onError дополнительно показывает сбои отложенного автосохранения, у которого собственного промиса нет. |
onBeforePaste | (html: string) => string | null | undefined | Преобразует исходный `text/html` из буфера обмена до какой-либо предобработки и санитизации в Blok, поэтому перехватчик вставки на фазе перехвата больше не нужен. Верните HTML, который попадёт в остальной конвейер вставки, или null, чтобы пропустить HTML-путь и вставить как обычный текст. Всё описанное ниже выполняется уже после вашего обработчика. Blok приводит в порядок разметку, которую другие приложения кладут в буфер обмена: ответ, скопированный из ChatGPT, Claude или Gemini, и страница, скопированная из Notion или Google Docs, становятся настоящими блоками (заголовки, списки, таблицы, цитаты, код), а не одним сплошным абзацем. Для ChatGPT и Gemini дополнительно выполняется отдельный предпроход, потому что каждый из них прячет смысл в разметке, которую санитайзер иначе удалил бы: ChatGPT не отдаёт MathML, поэтому LaTeX формулы восстанавливается из атрибута с исходником и снова собирается в формулу, а его блоки кода дедуплицируются (каждый отрисован как вложенный редактор); у Gemini язык блока кода считывается с подписи над блоком. Для Claude отдельный предпроход не нужен — его ответы и так приходят семантическим HTML и проходят по обычным путям HTML и markdown. |
onBeforeRender | (blocks: OutputBlockData[]) => OutputBlockData[] | undefined | Преобразует массив блоков прямо перед отображением — при первом рендере, при каждом вызове `blocks.render()` и при перерисовках, которые Blok выполняет сам (рантайм-вызов `i18n.update()` и повторный рендер как запасной путь переключения режима только для чтения). Получает сохранённые блоки в исходном виде (до анализа формата и раскрытия иерархии) и возвращает блоки для отображения, поэтому миграции данных приложения выполняются внутри Blok, а не заранее. Из-за этого функция обязана быть идемпотентной: перечисленные перерисовки подают ей блоки, которые она уже преобразовала. Живое: устанавливается, заменяется или снимается в рантайме через `handlers.set({ onBeforeRender })`. |
onAfterRender | (api: API) => void | undefined | Срабатывает после того, как очередная партия рендера попадает в DOM: при первом рендере, при каждом `blocks.render()` и при перерисовках, которые Blok выполняет сам, — смене локали/сообщений через рантайм-вызов `i18n.update()` и переключении `readOnly.set()`, которое откатывается к полному повторному рендеру, потому что смонтированный инструмент не поддерживает смену режима только для чтения на месте. Используйте для побочных эффектов после рендера (восстановление прокрутки, подключение наблюдателей), помня об этих дополнительных срабатываниях, если считаете рендеры. Отличается от onReady, который срабатывает один раз, когда редактор впервые становится готов. Живое: устанавливается, заменяется или снимается в рантайме через `handlers.set({ onAfterRender })`. |
autofocus | boolean | false | Если true, устанавливает курсор в первый блок, как только редактор готов |
scrollToBlock | { topOffset?: number } | undefined | Blok всегда плавно прокручивает к блоку, чей id совпадает с хешем URL страницы (`#<blockId>`), как только блоки отрисованы, — включая блоки, отрисованные позже через `blocks.render()`. Эта опция лишь настраивает такое поведение: `topOffset` (по умолчанию 0) резервирует место над блоком под закреплённую шапку. |
inlineToolbar | string[] | boolean | true | Строчная панель по умолчанию для всех инструментов; массив ограничивает её перечисленными строчными инструментами, false отключает её. Живое поле: перенастраивается на лету через `tools.setInlineToolbar(config)`. |
hideToolbar | boolean | false | Скрыть hover-панель инструментов блока (кнопку «плюс» / ручку перетаскивания) и убрать зарезервированный под неё отступ редактора; меню по клавише "/" продолжает работать. Живое поле: переключается на лету через `toolbar.setHidden(hidden)`. |
toolbarPosition | 'left' | 'right' | 'left' | С какой стороны от колонки контента находятся плавающие элементы управления блоком (кнопка «плюс» и ручка перетаскивания/меню). `'right'` переносит и элементы управления, и зарезервированный под них отступ на inline-end сторону редактора: начальный отступ схлопывается, а в конце открывается такой же — текст занимает место, которое раньше занимали элементы управления. Значения называют физическую сторону для LTR и применяются через логические свойства, поэтому в RTL-редакторе они зеркалятся. Не действует, пока включён `hideToolbar` или read-only без элементов управления — размещать нечего. Живое поле: переключается на лету через `toolbar.setPosition(position)`. |
i18n | I18nConfig | undefined | Конфигурация интернационализации (локаль + словарь сообщений). Живое поле: меняйте язык на лету через `i18n.update({ locale, messages })` — редактор меняет подписи на месте, поэтому курсор и история отмен переживают смену языка (`defaultLocale` — исключение и читается только при монтировании). Заголовки пользовательских инструментов локализуются по имени регистрации — например, инструмент `fileLink` через `messages: { 'toolNames.fileLink': '…' }` — либо через `titleKey` в записи toolbox инструмента. |
uploader | BlokUploader | undefined | Загрузчик уровня редактора для всех медиаассетов, маршрутизируемый по ТИПУ ассета, а не по инструменту. `uploadByFile(file, { kind, tool })` и `uploadByUrl(url, { kind, tool })` получают `kind: 'image' | 'video' | 'audio' | 'file'`, поэтому одна реализация обслуживает блоки изображения, видео, аудио и файла — включая ассеты, которыми инструмент владеет вне своего медиасемейства, например обложку аудиоблока (`kind: 'image'`, `tool: 'audio'`), у которой нет собственного загрузчика уровня инструмента. Загрузчик уровня инструмента (`tools.image.config.uploader`) остаётся главным для своего типа и имеет приоритет; этот же — запасной вариант. Без того и другого ассеты превращаются в URL вида `blob:`, которые не переживают перезагрузку. |
server | string | undefined | Базовый URL сервиса, говорящего на контрактах загрузки и разворачивания ссылок Blok, — `https://blok.myapp.com` или путь на том же origin, например `/api/blok`. Только сокращение: он подставляет `uploader` и `endpoint` инструмента закладки, если вы не задали их сами, а всё заданное вами явно побеждает. Именно это позволяет взять сервис ради предпросмотра ссылок, загружая при этом файлы в собственный S3, без связующего кода. Он не настраивает хранение документов — ваши документы остаются вашими; см. `persistence`. |
ticket | string | undefined | Эндпоинт в ВАШЕМ приложении, который выпускает краткоживущий пропуск для вошедшего пользователя и отвечает `{ "ticket": "<pass>" }`. Нужен, только когда `server` указывает на отдельный сервис — маршруты, работающие внутри вашего собственного приложения, и так знают, кто их вызывает. Редактор кеширует пропуск и заменяет его заранее, до истечения срока, а не в момент истечения, поэтому ни один запрос не приходит уже недействительным, а загрузки и предпросмотр ссылок пользуются одним и тем же пропуском. `@bloklabs/server/ticket` экспортирует `blokTicket()` для его выпуска; любой бэкенд может делать то же самое своей библиотекой JWT. |
persistence | { load(): Promise<OutputData | PersistedDocument | null>; save(data: OutputData, ctx: SaveContext): Promise<SaveResult | void>; onError?(error: unknown): void } | undefined | Загружать документ при монтировании и сохранять его по мере изменений — на ваш собственный эндпоинт; сервис Blok не хранит документов. Два колбэка, а не URL, потому что форма эндпоинта, его авторизация и id документа — ваши. Сохранения никогда не идут параллельно, и за тем, что уже в полёте, следует только самый свежий ожидающий документ, поэтому медленное сохранение, завершившееся после быстрого, не может вернуть устаревшее содержимое. Загрузка происходит, только если вы не передали `data`, а самостоятельно заданный `onSave` побеждает. `load` может ответить версией вместе с документом, а каждому `save` сообщается версия, которую он перезаписывает, и он может сообщить записанную — Blok только переносит эту версию между двумя вызовами, поэтому ваш эндпоинт остаётся единственным местом, где обнаруживается устаревшая запись. |
collaboration | { doc: string; user?: { name: string; color?: string }; offline?: boolean; offlineScope?: string } | undefined | Совместное редактирование в реальном времени через сервис синхронизации, на который указывает `server`: два редактора, открытые на одном `doc`, видят правки друг друга вживую. `doc` — это id общего документа, и он становится одним сегментом пути в URL синхронизации, поэтому обязан быть ровно одним сегментом пути (без `/`, без закодированного слеша, без `.`/`..`), а всё остальное отклоняется при создании редактора, а не падает уже на подключении. `user` — это ОТОБРАЖАЕМАЯ личность, которую видят остальные люди: имя на аватаре, цвет курсора и цвет маленького лица, расположенного на полях рядом с блоком, в котором человек находится, — и она не зависит от опции `user: { id }`, которая фиксирует авторство правок: одна отвечает на вопрос «чей это курсор», другая — «кому засчитать эту правку»; задавайте любую, обе или ни одной. `color` — только HEX (`#rgb`, `#rrggbb`, с альфа-каналом или без); всё остальное заменяется цветом из встроенной палитры. `offline` держит копию документа в этом браузере, чтобы правки, сделанные без подключения, пережили перезагрузку, — по умолчанию выключено, потому что записывает содержимое документа в хранилище браузера, и эта копия сбрасывается всякий раз, когда сервис сбрасывает документ. Опция ТРЕБУЕТ `offlineScope` — непрозрачный стабильный id вошедшего аккаунта: хранилище браузера принадлежит браузеру, а не человеку, поэтому без такого разделения следующий человек за общим профилем получит документ предыдущего. Это никогда не заявка на права доступа — сервер его никогда не видит — и никогда не отображаемая личность; не выводите его из чего-либо, что меняется со временем, потому что новое разделение при каждом обновлении страницы оставляет брошенными все прежние копии. Требует `server` и взаимно исключает `persistence` — сервис синхронизации владеет всем циклом загрузки и сохранения документа, поэтому вторая пара load/save дала бы документу двух владельцев, — и то и другое отклоняется при создании редактора. Когда опции нет, она ничего не стоит: Blok не открывает сокет. Только при монтировании; изменить её означает пересоздать редактор. React и Vue принимают её как проп `collaboration`, у Angular отдельного входа нет, поэтому она идёт через `[config]`. Состояние подключения и присутствующие люди приходят в событии `collaboration:status`. |
theme | 'auto' | 'light' | 'dark' | 'auto' | Цветовая тема; 'auto' следует настройке ОС через prefers-color-scheme |
onThemeChange | (resolvedTheme: ResolvedTheme) => void | undefined | Срабатывает с ИТОГОВОЙ темой ('light' или 'dark') всякий раз, когда она меняется, — и когда настройка ОС переключается, пока `theme` равна 'auto', и когда `theme.set()` меняет то, какой становится итоговая тема. Не срабатывает при инициализации и не срабатывает, когда `theme.set()` оставляет итоговую тему прежней. Это опция конфигурации ядра, а не проп только для адаптеров; адаптеры фреймворков отдают тот же колбэк как проп `onThemeChange` / эмит `theme-change` / output `themeChange`. |
link | { target?: string; rel?: string; transformHref?: (href: string) => string; transform?: (context: LinkTransformContext) => LinkTransformResult | void } | undefined | Управляет ссылками, которые создаёт Blok, вместо постобработки готового DOM. Действует на всех путях, порождающих `<a>`: строчный инструмент Link, `blocks.render()` (ссылки из сохранённого HTML блока) и вставка. `target` по умолчанию '_blank', `rel` — 'nofollow'. `transform` — расширенный вариант и заменяет `transformHref` (который тогда игнорируется): он получает href, текст и элемент и может вернуть `href`, `target`, `rel` и дополнительные `attributes`; опущенные поля берут значения из сокращённой формы, включая правило `_self` для ссылок на ту же страницу. Обе функции обязаны быть идемпотентными: на путях рендера и вставки они повторно выполняются над уже преобразованными ссылками при каждом рендере. |
linkPaste | { allowGenericEmbed?: boolean; allowedEmbedOrigins?: string[] } | undefined | Поведение при вставке ссылки в стиле Notion. Задайте `allowGenericEmbed: true`, чтобы предлагать «Встроить содержимое» (в iframe с песочницей) и для ссылок, не совпавших ни с одним зарегистрированным провайдером встраивания; по умолчанию сохраняется гарантия Blok встраивать только из реестра. `allowedEmbedOrigins` — более тонкий промежуточный вариант: имена хостов (`dashboards.example.com`) или шаблоны поддоменов с подстановкой (`*.internal.example.dev`), которые разрешено встраивать как произвольные эмбеды. Сохранённый произвольный эмбед, не совпавший ни с тем ни с другим, отображается безопасной кликабельной карточкой ссылки вместо iframe, поэтому URL остаётся виден, но не встраивается. |
user | { id: string } | undefined | Личность текущего редактирующего пользователя. Blok проставляет `user.id` в `lastEditedBy` каждого блока, который правит этот пользователь, — без него `lastEditedBy` остаётся null. Сочетайте её с `resolveUser`, чтобы отобразить имя в подвале настроек блока. |
resolveUser | (id: string) => UserInfo | Promise<UserInfo | null> | null | undefined | Определяет пользователя по идентификатору `lastEditedBy`, который Blok показывает в подвале настроек блока. Может возвращать значение синхронно или асинхронно; верните null для неизвестного пользователя — тогда Blok покажет только дату. |
notifierPosition | NotifierPosition | 'bottom-center' | Где на экране закреплён контейнер встроенных тостов. |
notifier | (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => void | undefined | Полностью заменяет встроенный тост — Blok вызывает ваш обработчик с тем же объектом опций вместо отрисовки собственного DOM-уведомления. |
logLevel | LogLevels | LogLevels.VERBOSE | Сколько Blok пишет в консоль. Значения: `VERBOSE`, `INFO`, `WARN` и `ERROR` — уровня «silent» нет, поэтому самый тихий из них — `LogLevels.ERROR`. `LogLevels` — именованный экспорт корня пакета. |