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

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

Обновлено 7 окт. 2026 г.Редактировать на GitHub
TypeScript
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);

Конфигурация

ПараметрТипПо умолчаниюОписание
holderstring | HTMLElement'blok'

Контейнер (ID или элемент DOM)

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

tunesstring[]undefined

Имена блочных тюнов, которые добавляются каждому блочному инструменту, не объявившему собственный набор tunes. Сами классы тюнов должны быть зарегистрированы в tools (класс со static isTune = true).

placeholderstring | falsefalse

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

Учтите: false (значение по умолчанию) не убирает плейсхолдер. Инструмент параграфа тогда берёт собственный встроенный локализованный текст («Write something or press / to select a tool»).

Чтобы очистить его, задайте инструменту по умолчанию собственный пустой плейсхолдер: tools: { paragraph: { class: Paragraph, placeholder: '' } }.

minHeightnumber300

Высота в px нижней кликабельной зоны редактора

captureClicksBelowEditorbooleanfalse

Опция по требованию: клики по странице ниже редактора добавляют блок и не занимают места в раскладке. Сочетайте её с minHeight: 0, чтобы полностью убрать нижнюю зону.

Срабатывают только клики по пустому фону элемента, который содержит редактор. Клики по вашему контенту, отрисованному ниже, игнорируются. Всплытие события не останавливается, поэтому обработчики кликов хост-страницы продолжают работать.

inlineEmojibooleantrue

Меню инлайн-эмодзи. Открывается при вводе : с последующим именем в текстовом блоке, например :fire. Выбор эмодзи заменяет введённое :запрос символом.

Двоеточие открывает меню только в начале слова. То есть в начале блока или сразу после пробела. 10:30, http:// и Note: this меню не открывают. После двоеточия должен идти хотя бы один символ.

Ввод закрывающего : может сразу вставить эмодзи. Это происходит, только если запрос является точным шорткодом. :fire: даёт 🔥, даже не открывая меню. Любой другой запрос остаётся обычным текстом.

Esc закрывает меню. Введённый текст при этом не меняется. Пока меню открыто, страница не прокручивается. Собственная сетка меню при этом прокручивается.

У меню нет поля поиска, кнопки случайного выбора и кнопки удаления. Раздела с подборкой тоже нет. Всё это остаётся в собственном пикере эмодзи инструмента «Выноска», на который эта опция не влияет.

false отключает меню. Отсутствие ключа оставляет его включённым.

findboolean | FindConfigtrue

Поиск по странице. Cmd/Ctrl+F внутри редактора открывает собственную панель поиска Blok вместо браузерной. Панель открывается в правом верхнем углу окна, там же, где браузеры показывают свою панель поиска, и остаётся поверх всего на странице. Если выделен короткий фрагмент текста в одну строку, в редакторе или в полях вашей страницы, например в заголовке, он попадает в поле поиска, и поиск начинается с него.

Передайте объект, чтобы выбрать место панели, например find: { placement: 'bottom-center', offset: { y: 24 } }. placement принимает top-end (по умолчанию), top-start, top-center, bottom-end, bottom-start или bottom-center. start и end следуют направлению текста редактора.

offset задаёт отступ в пикселях от этих краёв окна. С ним панель не закроет ваш собственный закреплённый интерфейс, например шапку. Панель закреплена на месте: пользователи редактора не могут её передвинуть.

По умолчанию сравнение нестрогое.

  • Регистр не учитывается. Кнопка Aa (Alt+C) включает учёт регистра.
  • Диакритика не учитывается, поэтому cafe находит café.
  • Фигурные и прямые кавычки совпадают друг с другом. Разные тире тоже.
  • Несколько пробелов подряд совпадают с одним пробелом.
  • Кнопка ab (Alt+W) оставляет только совпадения целым словом.

Совпадение может проходить через жирный текст, курсив и ссылки. Оно никогда не захватывает два блока или две ячейки таблицы. Поиск охватывает текст на всей странице и во всех редакторах Blok, включая контент только для чтения, блоки кода, ячейки таблиц и дочерние блоки свёрнутых сворачиваемых списков.

Enter и Shift+Enter переходят к следующему и предыдущему совпадению, как и стрелки вниз и вверх в поле поиска. Cmd/Ctrl+G и F3 делают то же самое, а с Shift идут назад. Счётчик показывает текущую позицию, например 3 из 12. Полоса под полем отмечает, где на странице находится каждое совпадение. Щелчок по ней переносит туда.

Ввод запроса никогда не разворачивает свёрнутый список, но совпадения внутри него учитываются. Переход к такому совпадению разворачивает его список. Это работает для клавиш, кнопок-стрелок и карты совпадений. В редактируемом редакторе это обычное изменение документа, как щелчок по стрелке списка. Оно сохраняется и отменяется.

Esc закрывает панель и выделяет текущее совпадение, только если это редактируемый текст в этом редакторе. Ввод затем заменяет его. Повторное нажатие Cmd/Ctrl+F, пока фокус в панели, выделяет текст поиска. Поиск браузера при этом не открывается.

Замена открывается кнопкой-шевроном, сочетанием Cmd+Option+F на macOS или Ctrl+H на других системах. В поле замены Enter заменяет текущее совпадение, если оно находится в этом редакторе, и переходит к следующему. Cmd/Ctrl+Enter заменяет все редактируемые совпадения в этом редакторе одним шагом отмены. Текст в других частях страницы никогда не заменяется. В режиме только для чтения замена недоступна.

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

Совпадения подсвечиваются через CSS Custom Highlight API. В ваш контент не добавляется никакая разметка, поэтому поиск никогда не меняет документ. В браузере без этого API у совпадений нет заливки. Кольцо вокруг текущего совпадения всё равно видно, и переход между совпадениями работает. Цвета заданы токенами темы, они перечислены в разделе «API стилей».

Cmd/Ctrl+F в полях вашей страницы, например в заголовке страницы или в поле поиска, тоже открывает поиск Blok, как и Cmd/Ctrl+F, когда ничего не в фокусе. Поле, которое само обрабатывает Cmd/Ctrl+F и вызывает preventDefault(), оставляет сочетание себе. Если редакторов несколько, поиск откроется в том, которым пользовались последним. false везде оставляет Cmd/Ctrl+F браузеру. Отсутствие ключа оставляет поиск включённым.

defaultBlockstring'paragraph'

Тип блока по умолчанию

dataOutputData | LooseOutputData | nullundefined

Начальные данные для отображения. Принимается нестрогий формат: значения null для data, id или time блока (частое дело в backend-DTO) нормализуются на границе. Документ целиком, равный null, тоже принимается и превращается в пустой документ.

Поэтому nullable-состояние контролируемого компонента можно передавать как есть, без обёртки вида value ?? { blocks: [] }.

dataModel'legacy' | 'hierarchical' | 'auto''auto'

Модель входных и выходных данных. 'auto' определяет формат данных, которые вы отображаете, и сохраняет их в том же формате. 'legacy' всегда использует вложенную структуру items[]. 'hierarchical' всегда использует плоские блоки со ссылками parent/content.

sanitizerSanitizerConfig{}

Общий для редактора allowlist санитайзера по умолчанию. Blok объединяет его с собственными правилами sanitize каждого инструмента. Он применяется при сохранении, при рендере, при вставке и при копировании выбранных блоков.

readOnlyboolean | { hideControls?: boolean }false

Включить режим только для чтения. Передайте { hideControls: true }, чтобы также скрыть hover-панель инструментов, настройки блока и строчную панель инструментов. Живое поле: меняйте его на лету через readOnly.set(state, { hideControls }). Тот же экземпляр переключает режим на месте и сохраняет курсор, историю отмен и прокрутку.

onChange(api: API, event: BlockMutationEvent | BlockMutationEvent[]) => voidundefined

Функция обратного вызова при изменении. Аргумент event несёт произошедшую мутацию или массив мутаций, если несколько срабатывают одновременно.

Задержка доставки ограничена, поэтому от неё можно питать интерфейс. Первое изменение простаивающего документа приходит уже на следующем микротаске, в том же кадре, в котором пользователь печатал. Последующие изменения объединяются в один дополнительный вызов в конце короткого окна пакетирования, и новые изменения это окно не продлевают.

detail.origin каждого события говорит, кто внёс изменение. Это 'local' для этой вкладки, включая её отмену и повтор, 'tab' для другой вкладки этого браузера и 'remote' для участника совместной работы.

Живое поле: обработчик ставится, заменяется или снимается на лету через handlers.set({ onChange }). Именно его наличие вместе с onSave включает конвейер отслеживания изменений.

onSave(data: OutputData, api: API) => voidundefined

Реактивный колбэк сохранения. Срабатывает автоматически с полным сериализованным содержимым на каждый пакет изменений, поэтому вызывать save() вручную не нужно.

Он срабатывает только по завершении окна пакетирования. В отличие от onChange он никогда не опережает окно: сериализовать весь документ слишком дорого, чтобы делать это заранее.

Живое поле: обработчик ставится, заменяется или снимается на лету через handlers.set({ onSave }). Само его наличие заставляет Blok сериализовать документ на каждую пачку изменений.

При синхронизации вкладок его вызывает только вкладка, в которой вы работаете. В фоновых вкладках читайте документ через editor.save().

onReady(blok?: Blok) => voidundefined

Срабатывает один раз, когда редактор становится готов, получая полностью инициализированный экземпляр Blok

onEnter(event: KeyboardEvent, api: API) => boolean | voidundefined

Срабатывает при нажатии Enter в блоке, прежде чем Blok разделит его или создаст новый. Верните true, чтобы пометить событие обработанным: тогда Blok отменит своё разделение или создание блока (нативный перенос строки всё равно предотвращается).

Не срабатывает для инструментов с enableLineBreaks, пока Enter принадлежит поповеру или панели инструментов, а также для Shift+Enter с мягким переносом строки. Исключение только на iOS: Safari сообщает Shift+Enter для завершающего предложение «. », и Blok создаёт блок, поэтому хук срабатывает и там.

Подходит для чат-полей («Enter отправляет»). Сочетайте его с настройкой preserveBlank параграфа вместо наследования от Paragraph.

Живое поле: обработчик ставится, заменяется или снимается на лету через handlers.set({ onEnter }).

onSubmit(data: OutputData, api: API) => voidundefined

Срабатывает с полными сериализованными OutputData на том Enter, который иначе создал бы или разделил блок. Это жест «Enter отправляет». Blok сериализует документ и подавляет разделение по умолчанию, поэтому вызывать save() внутри onEnter вручную не нужно.

Колбэк наследует все исключения onEnter. Если заданы оба, то onEnter, вернувший true, имеет приоритет и подавляет onSubmit.

Живое поле: обработчик ставится, заменяется или снимается на лету через handlers.set({ onSubmit }). Передайте undefined, чтобы вернуть стандартное поведение Enter (разбиение блока) без пересоздания редактора.

onError(error: Error, context: { source: 'save' }) => voidundefined

Срабатывает, когда операция редактора завершается ошибкой, которую Blok иначе только записал бы в лог. Сегодня такие ошибки даёт только сериализация. Через колбэк проходят и отложенное автосохранение, и явный save().

Неудавшийся save() отклоняет промис с исходной ошибкой и никогда не разрешается значением undefined, поэтому оборачивайте явные сохранения в try/catch. Кроме того, onError показывает сбои отложенного автосохранения, у которого собственного промиса нет.

onBeforePaste(html: string) => string | nullundefined

Преобразует исходный text/html из буфера обмена до какой-либо предобработки и санитизации в Blok, поэтому перехватчик вставки на фазе перехвата больше не нужен.

  • Верните HTML, который попадёт в остальной конвейер вставки.
  • Верните null, чтобы пропустить HTML-путь и вставить обычный текст.

Всё описанное ниже выполняется уже после вашего обработчика. Blok приводит в порядок разметку, которую другие приложения кладут в буфер обмена.

Скопируйте ответ из ChatGPT, Claude или Gemini или страницу из Notion или Google Docs. Содержимое станет настоящими блоками, а не одним сплошным абзацем. Это заголовки, списки, таблицы, цитаты и код.

Для ChatGPT и Gemini дополнительно выполняется отдельный предпроход, потому что каждый из них прячет смысл в разметке, которую санитайзер иначе удалил бы.

  • ChatGPT не отдаёт MathML. Поэтому LaTeX формулы восстанавливается из атрибута с исходником и снова собирается в формулу.
  • Блоки кода ChatGPT ещё и дедуплицируются. Каждый из них отрисован как вложенный редактор.
  • У Gemini язык блока кода считывается с подписи, которую он печатает над блоком.

Для Claude отдельный предпроход не нужен: его ответы и так приходят семантическим HTML и проходят по обычным путям HTML и markdown.

onBeforeRender(blocks: OutputBlockData[]) => OutputBlockData[]undefined

Преобразует массив блоков прямо перед отображением. Работает при первом рендере, при каждом вызове blocks.render() и при перерисовках, которые Blok выполняет сам. Таких перерисовок две: рантайм-вызов i18n.update() и повторный рендер как запасной путь для режима только для чтения.

Получает сохранённые блоки в исходном виде, до анализа формата и раскрытия иерархии, и возвращает блоки для отображения. Так миграции данных приложения выполняются внутри Blok, а не заранее.

Из-за этого функция обязана быть идемпотентной: перечисленные перерисовки подают ей блоки, которые она уже преобразовала. Живое поле: обработчик ставится, заменяется или снимается на лету через handlers.set({ onBeforeRender }).

onAfterRender(api: API) => voidundefined

Срабатывает после того, как очередная партия рендера попадает в DOM. Это первый рендер, каждый вызов blocks.render() и перерисовки, которые Blok выполняет сам.

К последним относится смена локали или сообщений через рантайм-вызов i18n.update(). Сюда же входит переключение readOnly.set(), которое откатывается к полному повторному рендеру. Так бывает, когда смонтированный инструмент не поддерживает смену режима только для чтения на месте.

Используйте колбэк для побочных эффектов после рендера: восстановления прокрутки, подключения наблюдателей. Помните об этих дополнительных срабатываниях, если считаете рендеры. Он отличается от onReady, который срабатывает один раз, когда редактор впервые становится готов.

Живое поле: обработчик ставится, заменяется или снимается на лету через handlers.set({ onAfterRender }).

autofocusbooleanfalse

Если true, устанавливает курсор в первый блок, как только редактор готов

scrollToBlock{ topOffset?: number }undefined

Blok всегда плавно прокручивает к блоку, чей id совпадает с хешем URL страницы (#<blockId>), как только блоки отрисованы. Это касается и блоков, отрисованных позже через blocks.render(). Опция лишь настраивает такое поведение: topOffset (по умолчанию 0) резервирует место над блоком под закреплённую шапку.

inlineToolbarstring[] | booleantrue

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

Массив выбирает, какие инструменты показать, но не их порядок. Панель всегда рисует встроенные инструменты в одном фиксированном порядке (преобразование, цвет, полужирный, курсив, подчёркнутый, очистка форматирования, ссылка, зачёркнутый, строчный код, формула, над/подстрочный). Любой пользовательский строчный инструмент добавляется после них, в порядке регистрации.

Живое поле: перенастраивается на лету через tools.setInlineToolbar(config).

hideToolbarbooleanfalse

Скрывает hover-панель инструментов блока (кнопку «плюс» и ручку перетаскивания) и убирает зарезервированный под неё отступ редактора. Меню по клавише "/" продолжает работать. Живое поле: переключается на лету через toolbar.setHidden(hidden).

toolbarPosition'left' | 'right''left'

С какой стороны от колонки контента находятся плавающие элементы управления блоком (кнопка «плюс» и ручка перетаскивания или меню). 'right' переносит и элементы управления, и зарезервированный под них отступ на inline-end сторону редактора. Начальный отступ схлопывается, а в конце открывается такой же, поэтому текст занимает место, которое раньше занимали элементы управления.

Значения называют физическую сторону для LTR и применяются через логические свойства, поэтому в RTL-редакторе они зеркалятся. Не действует, пока включён hideToolbar или пока идёт режим только для чтения без элементов управления: размещать нечего.

Живое поле: сторона меняется на лету через toolbar.setPosition(position).

i18nI18nConfigundefined

Конфигурация интернационализации (локаль и словарь сообщений). Живое поле: меняйте язык на лету через i18n.update({ locale, messages }).

Редактор меняет подписи на месте, поэтому курсор и история отмен переживают смену языка. Исключение это defaultLocale, он читается только при монтировании.

Заголовки пользовательских инструментов локализуются по имени регистрации, например инструмент fileLink через messages: { 'toolNames.fileLink': '…' }, либо через titleKey в записи toolbox инструмента.

uploaderBlokUploaderundefined

Загрузчик уровня редактора для всех медиаассетов. Маршрутизация идёт по ТИПУ ассета, а не по инструменту. uploadByFile(file, { kind, tool }) и uploadByUrl(url, { kind, tool }) получают kind: 'image' | 'video' | 'audio' | 'file'. Поэтому одна реализация обслуживает блоки изображения, видео, аудио и файла.

Сюда входят и ассеты, которыми инструмент владеет вне своего медиасемейства. Пример: обложка аудиоблока (kind: 'image', tool: 'audio'), у которой нет собственного загрузчика уровня инструмента.

Загрузчик уровня инструмента (tools.image.config.uploader) остаётся главным для своего типа и имеет приоритет. Этот же загрузчик запасной. Без того и другого ассеты превращаются в URL вида blob:, которые не переживают перезагрузку.

mediaMediaConfigundefined

Преобразует каждую загруженную фотографию в несколько форматов. Затем редактор показывает лучший формат, который поддерживает браузер. Пока вы не перечислите форматы, функция выключена.

  • formats.image принимает 'avif', 'webp', 'jpeg' и 'png'.
  • Порядок в списке не важен. Blok хранит и показывает форматы от лучшего к худшему.
  • Самый совместимый формат попадает в url, поэтому код, который не знает о variants, продолжает работать.
  • Формат, который браузер не умеет кодировать, пропускается. Если не получилось ни JPEG, ни PNG, запасным вариантом остаётся исходный файл.
  • Прозрачное изображение никогда не превращается в JPEG.

Каждый формат загружается отдельно. Ваш загрузчик получает в контексте variant: { mimeType, role }.

convert(file, formats, ctx) заменяет встроенный конвертер. Верните null, чтобы использовать конвертер Blok.

С видео всё иначе, потому что преобразование занимает время.

  • formats.video принимает 'av1', 'webm' и 'mp4'.
  • Исходный файл воспроизводится сразу. Каждый формат создаётся в фоне, начиная с самого совместимого, и добавляется, когда готов.
  • Как только появляется MP4, он становится url. До этого url остаётся исходным файлом.
  • Пока видео преобразуется, при закрытии страницы браузер показывает диалог «Покинуть сайт?».
  • Видео длиннее maxTranscodeDuration секунд (по умолчанию 600) только перепаковываются, без перекодирования.

Blok не включает конвертер видео в сборку, потому что у него лицензия MPL-2.0. Установите mediabunny и передайте mediabunny: () => import('mediabunny'). Без него форматы видео приходят только из convert.

serverstringundefined

Базовый URL сервиса, который говорит на контрактах загрузки и разворачивания ссылок Blok: например, https://blok.myapp.com или путь на том же origin вида /api/blok. Это только сокращение.

Он подставляет uploader, endpoint инструмента закладки и link.unfurl, если вы не задали их сами, а всё заданное вами явно побеждает. Именно это позволяет взять сервис ради предпросмотра ссылок и при этом загружать файлы в собственный S3, без связующего кода.

Хранение документов он не настраивает. Ваши документы остаются вашими. См. persistence.

ticketstringundefined

Эндпоинт в ВАШЕМ приложении, который выпускает краткоживущий пропуск для вошедшего пользователя и отвечает { "ticket": "<pass>" }. Он нужен всякий раз, когда маршруты проверяют пропуска: для отдельного сервиса с --auth ticket или для маршрутов внутри вашего приложения с Auth = "ticket". При Auth, равном none или proxy, пропуск не читается.

Редактор кеширует пропуск и заменяет его заранее, до истечения срока, а не в момент истечения, поэтому ни один запрос не приходит уже недействительным. Загрузки и предпросмотр ссылок пользуются одним и тем же пропуском. @bloklabs/server/ticket экспортирует blokTicket() для его выпуска, а бэкенд на .NET использует BlokTicket.Create из Blok.Server. Любой другой бэкенд может подписать пропуск своей библиотекой JWT. Заголовок при этом должен быть ровно {"alg":"HS256","typ":"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 }, которая фиксирует авторство правок.

Задайте обе, и участники увидят «Автор последнего изменения: <имя>» на блоках, которые вы правили, потому что id уходит в состояние присутствия рядом с этим именем. Имя публикуется в комнату. user.name не публикуется, поэтому хост, задавший только user.name, называет себя и никого больше.

Все в комнате также видят, когда вы последний раз проявляли активность, вместе с тем, где стоит ваш курсор. Если не задать ни то ни другое, вы присутствуете как анонимный аватар, и никакой id не публикуется.

color принимает только HEX (#rgb, #rrggbb, с альфа-каналом или без). Всё остальное заменяется цветом из встроенной палитры.

offline держит копию документа в этом браузере, чтобы правки, сделанные без подключения, пережили перезагрузку. По умолчанию опция выключена, потому что записывает содержимое документа в хранилище браузера, и эта копия сбрасывается всякий раз, когда сервис сбрасывает документ.

Она ТРЕБУЕТ offlineScope, непрозрачный стабильный id вошедшего аккаунта. Хранилище браузера принадлежит браузеру, а не человеку, поэтому без такого разделения следующий человек за общим профилем получит документ предыдущего.

Это никогда не заявка на права доступа, ведь сервер этот id вообще не видит, и это никогда не отображаемая личность. Не выводите его из чего-либо, что меняется со временем: новое разделение при каждом обновлении страницы оставляет брошенными все прежние копии.

Опция требует server и взаимно исключает persistence. Сервис синхронизации владеет всем циклом загрузки и сохранения документа, поэтому вторая пара load/save дала бы документу двух владельцев. И то и другое отклоняется при создании редактора.

Когда опции нет, она ничего не стоит: Blok не открывает сокет. Только при монтировании: её изменение означает пересоздание редактора.

  • React и Vue принимают её как проп collaboration.
  • У Angular отдельного входа нет, поэтому она идёт через [config].

Состояние подключения и присутствующие люди приходят в событии collaboration:status.

documentIdstringundefined

Id этого документа в вашем приложении. Вкладки одного браузера с одинаковым documentId синхронизируются вживую, без сервера.

Возьмите id, по которому приложение уже загружает и сохраняет документ, например параметр маршрута или ключ записи. Он должен быть уникальным на всём сайте. Пустая строка не принимается, и isReady отклоняется с ошибкой.

Без него Blok берёт id, который сам записывает в сохранённые данные, и добавляет путь страницы. Это работает только для документа, который загружает или сохраняет persistence.

Задаётся только при монтировании: чтобы показать другой документ, создайте редактор заново. Откуда взять id, рассказывает руководство «Синхронизация вкладок».

tabSyncboolean | { settings?: boolean }true

Синхронизация между вкладками одного браузера в реальном времени. Включена по умолчанию.

  • false выключает её.
  • { settings: false } оставляет синхронизацию документа, но перестаёт передавать язык, режим темы и ширину.

С collaboration она не работает, потому что сервер совместной работы сам синхронизирует все вкладки. Задаётся только при монтировании: чтобы изменить её, создайте редактор заново.

theme'auto' | 'light' | 'dark''auto'

Цветовая тема; 'auto' следует настройке ОС через prefers-color-scheme

onThemeChange(resolvedTheme: ResolvedTheme) => voidundefined

Срабатывает с ИТОГОВОЙ темой ('light' или 'dark') всякий раз, когда она меняется. Это и переключение настройки ОС, пока theme равна 'auto', и вызов theme.set(), меняющий итоговую тему. Колбэк не срабатывает при инициализации и не срабатывает, когда theme.set() оставляет итоговую тему прежней.

Это опция конфигурации ядра, а не проп только для адаптеров. Адаптеры фреймворков отдают тот же колбэк как проп onThemeChange, эмит theme-change и output themeChange.

linkPaste{ allowGenericEmbed?: boolean; allowedEmbedOrigins?: string[]; hostAliases?: string[] }undefined

Поведение при вставке ссылки в стиле Notion. Задайте allowGenericEmbed: true, чтобы предлагать «Встроить содержимое» (в iframe с песочницей) и для ссылок, не совпавших ни с одним зарегистрированным провайдером встраивания. По умолчанию сохраняется гарантия Blok встраивать только из реестра.

allowedEmbedOrigins это более тонкий промежуточный вариант: имена хостов (dashboards.example.com) или шаблоны поддоменов с подстановкой (*.internal.example.dev), которые разрешено встраивать как произвольные эмбеды. Сохранённый произвольный эмбед, не совпавший ни с тем ни с другим, отображается безопасной кликабельной карточкой ссылки вместо iframe, поэтому URL остаётся виден, но не встраивается.

Если вставить URL при выделенном тексте, выделенный текст станет ссылкой на него. Меню не открывается.

hostAliases перечисляет другие имена хостов (или шаблоны *.), которые считаются вашим собственным сайтом, вместе с хостом страницы. Для вставленной ссылки на ваш сайт пункт «Создать закладку» не предлагается. Если других вариантов не остаётся, меню не открывается.

user{ id: string; name?: string }undefined

Личность текущего редактирующего пользователя. Blok проставляет user.id в lastEditedBy каждого блока, который правит этот пользователь. Без него lastEditedBy остаётся null.

Добавьте name, и подвал настроек блока покажет «Автор последнего изменения: <имя>» без единого колбэка. Само это имя локальное, поэтому участникам вас называет collaboration.user.name. Задайте оба, чтобы имя было видно везде.

Идентификаторы других людей Blok спрашивает у resolveUser, а в совместной сессии ещё и узнаёт имена от самих участников.

В совместной сессии идентификатор не приватен: он попадает в общий документ как lastEditedBy, а если задано ещё и collaboration.user.name, то и в состояние присутствия рядом с этим именем. Так участники и узнают друг друга. Передавайте непрозрачный токен на документ, а не внутренний ключ аккаунта, если это важно.

resolveUser(id: string) => UserInfo | Promise<UserInfo | null> | nullundefined

Определяет пользователя по идентификатору lastEditedBy, который Blok показывает в подвале настроек блока. Может возвращать значение синхронно или асинхронно. Верните null для неизвестного пользователя: тогда Blok возьмёт имя, которое этот человек опубликовал в совместной сессии, а если такого нет, покажет только дату.

Blok спрашивает не больше одного раза на идентификатор: и ответ, и «такого не знаю» он держит всё время жизни редактора. Колбэк, который бросает исключение или отклоняет промис, поглощается и не ломает меню. Но ответом это не считается, поэтому про такой идентификатор спросят снова.

Подключённый колбэк также означает, что неаутентифицированное имя из совместной сессии не показывается, пока идёт запрос.

Идентификатор приходит обрезанным по краям и без NUL. Идентификатор длиннее 128 символов отклоняется и до колбэка не доходит.

notifierPositionNotifierPosition'bottom-center'

Где на экране закреплён контейнер встроенных тостов.

notifier(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => voidundefined

Полностью заменяет встроенный тост. Blok вызывает ваш обработчик с тем же объектом опций вместо отрисовки собственного DOM-уведомления.

logLevelLogLevelsLogLevels.VERBOSE

Сколько Blok пишет в консоль. Значения: VERBOSE, INFO, WARN и ERROR. Уровня «silent» нет, поэтому самый тихий из них это LogLevels.ERROR. LogLevels это именованный экспорт корня пакета.