# Свой тип свойства в админке Битрикса: конструктор вместо разметки руками

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

8 сентября 2026 · Заря · 11 минут

Когда контент-менеджеры вставляют вёрстку руками, рано или поздно её ломают. Лечится это не инструкцией, а тем, что разметку им перестают давать: вместо неё — конструктор прямо на форме элемента. Разбираем, как такой конструктор устроен и на чём он ломается.

## Задача: убрать вёрстку из рук менеджера

Была статья базы знаний, в которой инструкции различаются по операционным системам и по средам разработки. Менеджеры собирали такие переключалки вручную: копировали в текст статьи блок стилей, скрипт и разметку. Работало до первой второй группы на странице — обработчик переключения искал элементы по всей странице, поэтому вторая группа ломала первую.

Очевидное решение — научить менеджеров писать разметку аккуратнее — не работает никогда. Поэтому разметку у них забрали совсем: структура вкладок хранится отдельным свойством, а в тексте статьи остаётся только короткий токен-плейсхолдер.

_Плейсхолдер вместо вёрстки в тексте_

```php
// В тексте статьи менеджер пишет только это:
[TABS:install]

// Резолвер при выводе меняет токен на готовую разметку.
// Вместе с обёрткой <p>, которую визуальный редактор добавляет вокруг
// одиночной строки — иначе в вёрстке останется пустой абзац.
$html = preg_replace_callback(
    '~(?:<p>\s*)?\[TABS:([A-Za-z0-9_-]+)\]\s*(?:</p>)?~u',
    static fn(array $m): string => renderGroup($groups[$m[1]] ?? null),
    $html,
);
```

Сознательная жертва: содержимое вкладок больше не попадает в поисковый индекс сайта — оно лежит в свойстве, а не в тексте. Это обсуждается с заказчиком до начала работ, а не после.

## Как встроить свой контрол в форму элемента

Соблазн — повесить обработчик на событие формы и дорисовать свою вкладку. Так можно, но есть путь чище: зарегистрировать собственный тип свойства. Тогда ядро само рисует контрол там, где ему положено, а форму мы не трогаем.

_Регистрация типа свойства_

```php
// init.php — регистрируем свой тип свойства инфоблока
$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler('iblock', 'OnIBlockPropertyBuildList', [
    KbTabsProperty::class, 'getUserTypeDescription',
]);

final class KbTabsProperty
{
    public static function getUserTypeDescription(): array
    {
        return [
            'PROPERTY_TYPE'        => 'S',
            'USER_TYPE'            => 'KbTabs',
            'DESCRIPTION'          => 'Вкладки статьи (конструктор)',
            // Множественное свойство рисуется одним контролом целиком,
            // а не N копиями одиночного — за это отвечает Multy-метод.
            'GetPropertyFieldHtmlMulty' => [self::class, 'getPropertyFieldHtmlMulty'],
            'ConvertToDB'          => [self::class, 'convertToDB'],
            'ConvertFromDB'        => [self::class, 'convertFromDB'],
        ];
    }
}
```

Контракт множественного контрола в ядре не документирован, но подсматривается: имена полей должны совпадать с тем, что ждёт форма. Проще всего открыть штатный тип из ядра и повторить.

_Имена полей множественного свойства_

```php
// Имена полей значений — строго в формате, который ждёт форма элемента.
// Подсмотрено в ядровом CIBlockPropertyElementList::GetPropertyFieldHtmlMulty:
//   <base>[n0][VALUE], <base>[n1][VALUE], ...
// Индексы с префиксом n — это новые значения; ядро само разберёт их в массив.
foreach ($groups as $i => $group) {
    printf(
        '<input type="hidden" name="%s[n%d][VALUE]" value="%s">',
        htmlspecialcharsbx($strHTMLControlName['VALUE']),
        $i,
        htmlspecialcharsbx(json_encode($group, JSON_UNESCAPED_UNICODE)),
    );
}
```

![Конструктор вкладок на форме элемента: слева редактор групп и вкладок, справа живое превью](https://it-zarya.ru/media/blog/admin-constructor.jpg)

_Готовый конструктор на форме элемента. Слева — группы, оси и вкладки с редактором тела, справа — живое превью «как увидит читатель». Данные заказчика на скриншоте замазаны._

## Ловушка первая: молчаливая потеря данных

Самая дорогая ошибка в этой задаче выглядела так: менеджер открывал статью, ничего не менял, нажимал «Применить» — и все вкладки исчезали.

Причина в том, что скрытые поля со значениями заполнялись только при правке в конструкторе. Если правок не было, форма отправляла пустой набор значений, а ядро честно понимало это как «значений больше нет» и очищало свойство.

_Одна строка, которая перестала терять данные_

```js
// Было: сохраняем модель в скрытые поля только при изменении.
// Открыли форму, ничего не тронули, нажали «Применить» — свойство очищено.
onChange(() => save());

// Стало: сразу при инициализации кладём текущее состояние в поля.
// Форма всегда отправляет то, что реально есть, даже если её не трогали.
function init() {
    render();
    save();          // ← вот эта строка и есть весь фикс
    bindEvents();
}
```

Правило общее и не только про Битрикс: если контрол пишет значение в скрытое поле, поле должно быть заполнено сразу, а не по событию. Иначе «ничего не делал» превращается в удаление.

## Ловушка вторая: компонент отдаёт пустое значение

Свойство сохраняется, в админке видно, а на странице пусто. Оказалось, что компонент вывода элемента отдаёт множественное свойство с собственным типом пустым массивом — не null, а именно пустым. Из-за этого проверка «если не задано, дотянем сами» не срабатывала: значение формально было.

_Значение множественного свойства берём напрямую_

```php
// Не работает: значение формально есть, но оно пустое
$groups = $arResult['PROPERTIES']['KB_TABS']['VALUE'] ?? null;

// Работает: тянем напрямую и только тогда, когда токен в тексте реально есть.
// Лишний запрос к базе на каждой статье нам не нужен.
$groups = [];
if (str_contains($html, '[TABS:')) {
    $res = CIBlockElement::GetProperty(
        $arResult['IBLOCK_ID'],
        $arResult['ID'],
        [],
        ['CODE' => 'KB_TABS'],
    );
    while ($row = $res->Fetch()) {
        if ($row['VALUE'] !== null && $row['VALUE'] !== '') {
            $groups[] = $row['VALUE'];
        }
    }
}
```

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

## Ловушка третья: встроенный редактор в модальном окне

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

- Экземпляр редактора третьей версии берётся через свой реестр, а не через свойство DOM-элемента: по привычному пути возвращается редактор второй версии, и попап открывается пустым
- Всплывающие меню редактора имеют встроенный приоритет наложения — модалка обязана быть ниже, иначе она накрывает выпадающий список стилей
- Редактор нельзя переносить по DOM: перенос перезагружает его внутренний фрейм и ломает кнопки. Модалка рисуется сразу вокруг редактора, а «открытие» — это класс на обёртке
- У предков модалки не должно быть трансформаций: трансформация делает предка точкой отсчёта для фиксированного позиционирования, и всплывашки уезжают в сторону

_Работа с визуальным редактором_

```js
// Экземпляр редактора: только через реестр редактора третьей версии.
// BX('bxed_KB_TABS_LHE').pMainObj вернёт редактор второй версии — попап будет пустым.
const editor = window.BXHtmlEditor.Get('KB_TABS_LHE');

editor.SetContent(html);

// Модалку не двигаем по DOM — только показываем классом.
// И следим за размером: редактор сам не растянется под увеличенное окно.
new ResizeObserver(() => {
    editor.SetConfigHeight(box.clientHeight - HEADER_H);
    editor.ResizeSceleton();
}).observe(box);
```

Отдельная тонкость: выпадающий список пользовательских стилей показывает стили того сайта, к которому привязан редактор. Для свойства инфоблока это первый сайт инфоблока, а не тот, на котором статья выводится. Не передашь его явно — менеджер увидит пустой список стилей и придёт с вопросом.

_Контекст сайта для пользовательских стилей_

```php
// Стили редактора берутся из .styles.php ПЕРВОГО сайта инфоблока.
// Передаём его явно, иначе список стилей окажется пустым.
$site = CIBlock::GetSite($iblockId)->Fetch()['LID'] ?? 's1';

CFileMan::AddHTMLEditorFrame(
    'KB_TABS_LHE',
    $content,
    'KB_TABS_LHE_TYPE',
    'html',
    ['height' => 400],
    'N', 0, '', '', $site,
);
```

## Что в итоге получилось

- Менеджер не пишет ни строчки разметки: группы, оси и вкладки собираются кнопками
- Справа живое превью — видно ровно то, что увидит читатель
- В текст статьи вставляется один короткий токен, который нельзя сломать опечаткой в вёрстке
- Несколько групп на странице не мешают друг другу: переключение изолировано по группам
- Печать в PDF отдаёт те вкладки, которые открыты на экране — активные передаются в адресе страницы

И главный вывод, ради которого всё затевалось: надёжность здесь дала не проверка ввода, а отсутствие самой возможности ввести что-то не то. Пока у менеджера есть поле, куда можно вставить разметку, он однажды вставит её неправильно.

## Нужна такая же форма

Кастомизация админки — обычная для нас работа: свои вкладки, типы свойств, конструкторы. Расскажите, что должны делать ваши контент-менеджеры, и мы предложим, как убрать у них возможность ошибиться.

[Посмотреть услугу: сайты на 1С-Битрикс](https://it-zarya.ru/services/bitrix-cms/) [Обсудить задачу](https://it-zarya.ru/contacts/)

---

Источник: https://it-zarya.ru/blog/bitrix-admin-property/ · IT-агентство «Заря» · itzarya@mail.ru
Markdown-версия любой страницы сайта — тот же адрес с `.md` на конце. Индекс для ассистентов: https://it-zarya.ru/llms.txt
