Play
online · 30 мин
← все статьи
Постмортемы 26 мин чтения 12.05.2026

Кастомный UserType для поиска разделов инфоблоков в Bitrix админке | Разбор

Стандартный селектор разделов в Bitrix превращается в ад уже на 50 элементах. Вместо того чтобы листать бесконечный список, замените его на кастомный UserType с поиском. Всего полдня работы — и вы получите удобный AJAX-фильтр в админке.
TL;DR
Стандартный <select> для разделов неудобен при сотнях элементов Причина — отсутствие диалога для E:Section в ядре Bitrix Решение — кастомный UserType через OnIBlockPropertyBuildList Хранение SECTION_ID для совместимости с фильтрами и XML AJAX-эндпоинт с фильтрацией по NAME и IBLOCK_ID

TL;DR

Стандартная «Привязка к разделам» в Bitrix — это мусор, который ломается уже на 50 разделах. Разработчики годами терпят плоский список без поиска, хотя для элементов инфоблоков есть нормальный модальный диалог. Общепринятый подход «смириться и листать» в корне неверен — мы просто заменим нативное поле на кастомный UserType с AJAX-поиском. Это займёт полдня.

  1. Регистрируем собственный тип свойства через хук OnIBlockPropertyBuildList. Шаг обязательный,без него Bitrix не узнает о новом типе. Кладём обработчик в init.php или в метод модуля.

    PHP
    AddEventHandler('iblock', 'OnIBlockPropertyBuildList', function() {
            return [
                'PROPERTY_TYPE' => 'S',
                'USER_TYPE' => 'SectionSearch',
                'DESCRIPTION' => 'Раздел с поиском (кастом)',
                'GetPropertyFieldHtml' => ['CSectionSearchProperty', 'GetPropertyFieldHtml'],
                'GetAdminListViewHTML' => ['CSectionSearchProperty', 'GetAdminListViewHTML'],
                'GetPublicEditHTML' => ['CSectionSearchProperty', 'GetPublicEditHTML'],
            ];
    });

    После этого заходим в список свойств инфоблока,видим новый тип «Раздел с поиском (кастом)» в выпадающем списке.

  2. Реализуем класс CSectionSearchProperty с методом-рендером полей. Метод GetPropertyFieldHtml возвращает HTML с input и скрытым полем для SECTION_ID. AJAX-запросы будем отправлять на самописный компонентный action.

    PHP
    class CSectionSearchProperty {
        public static function GetPropertyFieldHtml($arProperty, $value, $strHTMLControlName) {
            $sectionId = (int) $value['VALUE'];
            $sectionName = '';
            if ($sectionId > 0) {
                $rsSection = CIBlockSection::GetList([], ['ID' => $sectionId], false, ['NAME']);
                if ($arSection = $rsSection->Fetch()) {
                    $sectionName = htmlspecialcharsbx($arSection['NAME']);
                }
            }
            return '<input type="text" id="section-search-input" value="'.$sectionName.
            '" placeholder="Начните вводить название раздела..."> <
            input type = "hidden"
            name = "' . $strHTMLControlName['VALUE'] . '"
            value = "' . $sectionId . '" > ';
        }
    }

    В админке на странице редактирования элемента увидим текстовое поле вместо старого списка.

  3. Пишем AJAX-эндпоинт для поиска разделов. Проще всего,добавить action в local/components/studio/section.search/ajax.php. Не забываем про CSRF-токен и фильтрацию по %NAME% и IBLOCK_ID.

    PHP
    require_once($_SERVER['DOCUMENT_ROOT'].
        '/bitrix/modules/main/include/prolog_before.php');
    if (!check_bitrix_sessid()) {
        die(json_encode(['error' => 'Session expired']));
    }
    $iblockId = (int) $_REQUEST['IBLOCK_ID'];
    $search = trim($_REQUEST['q']);
    $rsSections = CIBlockSection::GetList(
        ['NAME' => 'ASC'],
        ['IBLOCK_ID' => $iblockId, '%NAME' => $search],
        false,
        ['ID', 'NAME', 'DEPTH_LEVEL', 'IBLOCK_SECTION_ID']
    );
    $result = [];
    while ($arSection = $rsSections->Fetch()) {
        $result[] = ['id' => $arSection['ID'], 'text' => str_repeat('— ', $arSection['DEPTH_LEVEL'] - 1).$arSection['NAME']];
    }
    echo json_encode($result);

    В ответе получаем JSON-массив с ID, названиями разделов и отступами по уровню вложенности.

  4. Подключаем JavaScript-виджет на страницу админки. Используем стандартный BX.UI.Selector или простой самописный скрипт с fetch. Главное,при выборе раздела заполнять скрытое поле SECTION_ID.

    JS
    BX.ready(function() {
        var input = document.getElementById('section-search-input');
        if (!input) return;
        input.addEventListener('input', function() {
            var q = this.value;
            if (q.length < 2) return;
            fetch('/local/components/studio/section.search/ajax.php', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    },
                    body: 'sessid=' + BX.bitrix_sessid() + '&IBLOCK_ID=' + iblockId + '&q=' + encodeURIComponent(q)
                })
                .then(r => r.json())
                .then(data => {
                    // отрисовка выпадающего списка
                });
        });
        input.addEventListener('blur', function() {
            // если поле пустое — сбросить SECTION_ID
        });
    });

    После ввода 2+ символов появляется выпадающий список с разделами, соответствующими поиску.

  5. Обеспечиваем совместимость с админ-фильтрами и списком элементов. Для этого реализуем метод GetAdminListViewHTML, который отображает название раздела в таблице. Код минимальный,просто получаем имя раздела по ID.

    PHP
    public static function GetAdminListViewHTML($arProperty, $value, $strHTMLControlName) {
        $sectionId = (int) $value['VALUE'];
        if ($sectionId <= 0) return '';
        $rsSection = CIBlockSection::GetList([], ['ID' => $sectionId], false, ['NAME']);
        if ($arSection = $rsSection->Fetch()) {
            return htmlspecialcharsbx($arSection['NAME']);
        }
        return '—';
    }

    В списке элементов инфоблока теперь отображается название раздела, а не пустая строка или ID.

Альтернатива (если не хотите писать свой UserType)

Можно переопределить рендер существующего <select> через JS-инъекцию в админке. Подключаем скрипт на странице iblock_element_edit.php, заменяем нативный <select> на select2 с AJAX-подгрузкой разделов. Минусы,хрупкость при обновлениях Bitrix и привязка к URL админки. Наш подход с UserType работает независимо от версии и не требует костылей с глобальными хуками.

<select>

Симптом

Между нами: стандартный <select> для свойства «Привязка к разделам» — это наследие, которое ломается уже на 50 разделах. Разработчики Bitrix это знают, но фича не появится, так как приоритеты ядра другие. Выход — сделать свой UserType, который не трогает ядро и даёт нормальный UX.

  1. Регистрируем кастомный тип свойства через хук OnIBlockPropertyBuildList. Это единственный способ внедрить новое поведение без правок в /bitrix/admin/. Хук вешаем в init.php или в своём модуле.

    PHP
    // init.php
    AddEventHandler('iblock', 'OnIBlockPropertyBuildList', function() {
            return [
                'PROPERTY_TYPE' => 'G',
                'USER_TYPE' => 'G:SectionSearch',
                'DESCRIPTION' => 'Привязка к разделам с поиском',
                'GetPropertyFieldHtml' => ['CSectionSearchProperty', 'GetPropertyFieldHtml'],
                'GetAdminListViewHTML' => ['CSectionSearchProperty', 'GetAdminListViewHTML'],
                'GetPublicViewHTML' => ['CSectionSearchProperty', 'GetPublicViewHTML'],
            ];
    });

    После этого в списке типов свойств появится пункт «Привязка к разделам с поиском».

  2. Создаём класс-обработчик CSectionSearchProperty с методом GetPropertyFieldHtml. Он рендерит <input> с автокомплитом и скрытое поле для хранения SECTION_ID. Хранение как у стандартного E:Section,совместимость с XML-импортом и фильтрами админки.

    PHP
    class CSectionSearchProperty {
        public static function GetPropertyFieldHtml($arProperty, $value, $strHTMLControlName) {
            $sectionId = (int) $value['VALUE'];
            $sectionName = '';
            if ($sectionId > 0) {
                $res = CIBlockSection::GetByID($sectionId);
                if ($section = $res->Fetch()) {
                    $sectionName = htmlspecialcharsbx($section['NAME']);
                }
            }
            return '<input type="text" id="section-search-'.$arProperty['ID'].
            '" value="'.$sectionName.
            '" 
            data - property - id = "' . $arProperty['ID'] . '"
            data - iblock - id = "' . $arProperty['IBLOCK_ID'] . '"
            style = "width: 80%;" >
            <
            input type = "hidden"
            name = "' . $strHTMLControlName['VALUE'] . '"
            value = "' . $sectionId . '" >
            <
            script > ... < /script>'; / / JS для AJAX - поиска
        }
    }

    Должны увидеть текстовое поле вместо выпадающего списка. В скрипте подключаем AJAX-запрос к эндпоинту.

  3. Реализуем AJAX-эндпоинт для поиска разделов. Проще всего через bitrix:main.ajax или самописный action в local/components/. Эндпоинт принимает q (строка поиска), IBLOCK_ID, возвращает JSON с id и text.

    PHP
    // local/ajax/section_search.php
    require_once($_SERVER['DOCUMENT_ROOT'].
        '/bitrix/modules/main/include/prolog_before.php');
    $APPLICATION->RestartBuffer();
    $q = trim($_REQUEST['q']);
    $iblockId = (int) $_REQUEST['IBLOCK_ID'];
    $result = [];
    $rsSections = CIBlockSection::GetList(
        ['NAME' => 'ASC'],
        ['IBLOCK_ID' => $iblockId, '%NAME' => $q],
        false,
        ['ID', 'NAME', 'IBLOCK_SECTION_ID']
    );
    while ($section = $rsSections->Fetch()) {
        $result[] = ['id' => $section['ID'], 'text' => htmlspecialcharsbx($section['NAME'])];
    }
    echo json_encode($result);\CMain::FinalActions();

    Проверить: /local/ajax/section_search.php?q=test&IBLOCK_ID=1&sessid=... должен вернуть JSON с разделами.

  4. Обрабатываем выбор в JS на странице редактирования элемента. Используем Awesomplete или Select2 для красивого dropdown’а. При выборе,записываем SECTION_ID в скрытое поле.

    JS
    // Внутри GetPropertyFieldHtml
    BX.ready(function() {
        var input = BX('section-search-' + propertyId);
        new Awesomplete(input, {
            list: [],
            minChars: 2,
            maxItems: 20,
            autoFirst: true,
            data: function(item) {
                return {
                    label: item.text,
                    value: item.id
                };
            },
            filter: function() {
                return true;
            },
            replace: function() {
                this.input.value = this.selected.label;
            },
            sort: false,
            item: function(item) {
                return BX.create('div', {
                    text: item.label
                });
            }
        });
        input.addEventListener('awesomplete-selectcomplete', function(e) {
            BX('hidden-section-' + propertyId).value = e.text.value;
        });
    });

    После выбора раздела,скрытое поле должно получить ID.

  5. Реализуем метод GetAdminListViewHTML для отображения значения в списке элементов. Возвращаем название раздела, а не ID,так удобнее.

    PHP
    public static function GetAdminListViewHTML($arProperty, $value, $strHTMLControlName) {
        $sectionId = (int) $value['VALUE'];
        if ($sectionId <= 0) return '';
        $res = CIBlockSection::GetByID($sectionId);
        if ($section = $res->Fetch()) {
            return htmlspecialcharsbx($section['NAME']);
        }
        return '';
    }

    В списке элементов в колонке свойства должно отображаться название раздела.

  6. Учитываем совместимость с фильтрами и экспортом. Стандартный E:Section хранит SECTION_ID,наш UserType делает то же самое. Фильтры админки по умолчанию не поддерживают кастомный тип, но можно добавить свой фильтр через GetAdminFilterHTML.

    PHP
    public static function GetAdminFilterHTML($arProperty, $strHTMLControlName) {
        return '<input type="text" name="'.$strHTMLControlName['VALUE'].
        '" value="'.
        htmlspecialcharsbx($_REQUEST[$strHTMLControlName['VALUE']]).
        '">';
    }

    Фильтр будет работать как текстовый поиск по названию раздела.

Последнее и самое важное: кеш хука OnIBlockPropertyBuildList,он кешируется в bx_user_type. Если меняете код, сбрасывайте кеш через «Настройки → Производительность → Сброс кеша» или удаляйте /bitrix/cache/. Иначе новое свойство не появится. Это грабля, на которую попадаются все.

Причина

Я видел кучу проектов, где редакторы проклинали всё на свете, листая выпадающий список из 500 разделов ради одного выбора. Скажу прямо: стандартный <select> для E:Section в админке Bitrix — это просто преступление против UX, которое мы обязаны исправить.

Почему так происходит? Потому что Bitrix дал нам два разных мира: для привязки к элементам (тип E) есть красивый модальный диалог CIBlockElement::ShowSearchDialog с поиском, фильтрами и пагинацией, а для разделов — просто тупой <select>. Внутренности платформы не предусматривают для E:Section никакого альтернативного рендера. Хук OnIBlockPropertyBuildList существует, но его используют для кастомных типов свойств, а не для модификации существующих. Это фундаментальная архитектурная дыра: UserType для привязки к разделам не имеет метода ShowSearchDialog в принципе. На одном проекте мы пытались просто навесить jQuery-плагин через JS-инъекцию в /bitrix/admin/iblock_element_edit.php,это сработало ровно до первого обновления ядра, когда поменялись ID полей. Другой вариант,переопределить рендер через GetAdminListViewHTML,тоже не панацея, потому что он отвечает только за отображение в списке элементов, а не за форму редактирования. Корень проблемы в том, что E:Section,это legacy-тип, который не рефакторили годами, и его внутренняя логика привязана к старому CAdminList.

Мой совет: не пытайтесь чинить стандартный <select> костылями. Регистрируйте собственный UserType через AddEventHandler('iblock', 'OnIBlockPropertyBuildList', ...),это единственный путь, который даст вам контроль над рендером, AJAX-поиском и совместимостью с фильтрами админки без риска сломаться при следующем обновлении. Да, это больше кода, но это работает предсказуемо и не требует правки файлов ядра.

  1. Регистрация собственного UserType через OnIBlockPropertyBuildList

    Создайте класс-обработчик в /local/php_interface/classes/CustomSectionProperty.php или в модуле. Подписка на событие выполняется в init.php:

    КОД
    AddEventHandler('iblock', 'OnIBlockPropertyBuildList', ['CustomSectionProperty', 'GetUserTypeDescription']);

    Метод GetUserTypeDescription возвращает массив с ключами PROPERTY_TYPE (должен быть 'G' для совместимости с фильтрами), USER_TYPE (уникальный идентификатор, например 'CustomSection'), CLASS_NAME и массивом методов: GetPropertyFieldHtml, GetAdminListViewHTML, GetPublicEditHTML. Храните значение как SECTION_ID,это гарантирует, что экспорт/импорт XML и стандартные агенты продолжат работать без модификаций.

    Важно: кеш обработчиков событий в Bitrix 24.x сбрасывается только при очистке системного кеша. Если после регистрации UserType не появляется в списке типов свойств, выполните BXClearCache(true) или перезапустите сессию админки.

  2. Реализация AJAX-эндпоинта для поиска разделов

    Эндпоинт должен принимать GET-параметры: q (поисковый запрос), IBLOCK_ID (обязателен), IBLOCK_SECTION_ID (опционально для фильтра по родителю). Разместите его как компонентный action в /local/components/yournamespace/section.search/ajax.php или как отдельный скрипт в /bitrix/admin/ с проверкой check_bitrix_sessid().

    PHP
    $arFilter = [
        'IBLOCK_ID' => intval($_GET['IBLOCK_ID']),
        '?NAME' => $_GET['q'],
        'CHECK_PERMISSIONS' => 'N'
    ];
    if ($_GET['IBLOCK_SECTION_ID']) {
        $arFilter['SECTION_ID'] = intval($_GET['IBLOCK_SECTION_ID']);
        $arFilter['INCLUDE_SUBSECTIONS'] = 'Y';
    }
    $rsSections = CIBlockSection::GetList(['LEFT_MARGIN' => 'ASC'], $arFilter, false, ['ID', 'NAME', 'DEPTH_LEVEL']);
    $result = [];
    while ($section = $rsSections->Fetch()) {
        $prefix = str_repeat('— ', $section['DEPTH_LEVEL'] - 1);
        $result[] = ['id' => $section['ID'], 'text' => $prefix.$section['NAME']];
    }
    echo json_encode($result);

    Ожидаемый stdout: [{"id":12,"text":"— Категория А"},{"id":15,"text":"—— Подкатегория Б"}]. Фильтрация по ?NAME использует SQL-оператор LIKE с подстановкой %,это стандартный механизм Bitrix, работающий начиная с версии Main 9.0.

  3. Рендер поля в форме редактирования: GetPropertyFieldHtml

    Метод должен возвращать HTML с <input type="hidden"> для хранения SECTION_ID и <input type="text"> с автокомплитом. Для инициализации используйте JS-библиотеку BX.UI.Selector (родная для Bitrix 24.x) или лёгкую альтернативу вроде Select2 (версия 4.0+). Пример для Select2:

    PHP
    $html = '<input type="hidden" name="'.$arProperty['FIELD_NAME'].
    '" value="'.$value.
    '" id="section_id_'.$propertyId.
    '">';
    $html. = '<input type="text" class="section-autocomplete" data-property-id="'.$propertyId.
    '" data-iblock-id="'.$iblockId.
    '" value="'.$sectionName.
    '">';
    $html. = '<script>
    BX.ready(function() {
            BX.loadCSS("/local/js/select2/dist/css/select2.min.css");
            BX.loadScript("/local/js/select2/dist/js/select2.min.js", function() {
                    BX("section_autocomplete_' . $propertyId . '").select2({
                            ajax: {
                                url: "/ajax/section_search.php",
                                dataType: "json",
                                delay: 250,
                                data: function(params) {
                                    return {
                                        q: params.term,
                                        IBLOCK_ID: ' . $iblockId . '
                                    };
                                }
                            }
                    });
            });
    }); <
    /script>';

    Edge-case: в режиме инлайн-редактирования списка элементов (iblock_list_admin.php) z-index модалки Select2 может перекрываться другими элементами. Решение,принудительно выставить z-index: 9999 через CSS-класс .select2-container--open.

  4. Совместимость с фильтрами админки: GetAdminListViewHTML

    Этот метод вызывается при отображении значения свойства в таблице списка элементов. Он должен возвращать строку с названием раздела,простой текст или ссылку на редактирование раздела. Не используйте здесь сложный HTML, так как ядро экранирует вывод. Пример:

    PHP
    public static function GetAdminListViewHTML($arProperty, $value, $strHTMLControlName) {
        if ($value > 0) {
            $rsSection = CIBlockSection::GetByID($value);
            if ($arSection = $rsSection->Fetch()) {
                return htmlspecialcharsbx($arSection['NAME']);
            }
        }
        return '—';
    }

    Фильтр в админке (GetAdminFilterHTML) должен рендерить такой же <select> с автокомплитом, но без привязки к конкретному элементу,передавайте пустое значение по умолчанию.

  5. Совместимость с экспортом/импортом XML и миграциями

    Поскольку значение хранится как SECTION_ID (число, а не сериализованный путь), стандартные механизмы экспорта/импорта Bitrix (например, CIBlockElement::ExportXml) обработают его корректно. При импорте через CIBlockXMLFile::ImportElement свойство будет заполнено как обычная привязка к разделу. Не используйте сериализацию дерева разделов,это сломает поиск по фильтрам и агенты обновления.

    Для миграций (bitrix:sale.export.1c или кастомные скрипты) убедитесь, что PROPERTY_TYPE вашего UserType равен G. Иначе 1С-обмен может игнорировать поле или выдавать ошибку Property type mismatch.

  6. Известные грабли и их решения
    • Кеш OnIBlockPropertyBuildList: после добавления обработчика в init.php может потребоваться очистка кеша через админку (Настройки > Инструменты > Очистка кеша) или вызов BXClearCache(true). Если не очистить, событие не сработает до следующего хита.
    • Регистрация только в init.php или модуле: размещение кода в header.php или component.php приведёт к дублированию подписок и ошибкам Cannot redeclare class. Единственно верный путь,/bitrix/php_interface/init.php или установленный модуль.
    • ENUM_ID конфликты: если ваш UserType возвращает PROPERTY_TYPE = 'L' (список) вместо G, Bitrix попытается создать ENUM-значения для каждого раздела,это приведёт к дублированию данных и падению производительности. Всегда используйте PROPERTY_TYPE = 'G'.
    • Проблема z-index при модалках в инлайн-редакторе: при редактировании свойства прямо в таблице списка элементов (режим inline_edit) выпадающий список автокомплита может оказаться под модальным окном. Решение,добавить data-zindex="1050" в инициализацию Select2 или принудительно задать dropdownParent: BX('popup-window-content').
    • Совместимость с PHP 8.1+: избегайте использования each(), ereg() и других устаревших функций. Все методы класса должны быть статическими, а типы параметров,явно объявлены (int $value, array $arProperty).

Альтернативные подходы (JS-инъекция, main.ui.selector, самописный диалог с деревом) имеют право на жизнь, но каждый несёт свои риски. JS-инъекция в iblock_element_edit.php ломается при каждом обновлении ядра,ID полей меняются, скрипты перестают находить <select>. main.ui.selector требует подключения целого набора модулей (ui.selector, ui.dialog, ui.entity-selector), что увеличивает время загрузки страницы на 200-400 мс. Самописное дерево через catalog.section.tree не имеет встроенного поиска,его придётся реализовывать отдельно, что удваивает объём кода.

Регистрация собственного UserType остаётся единственным предсказуемым решением для Bitrix 24.x. Оно не требует правки файлов ядра, поддерживает все стандартные механизмы (фильтры, экспорт, импорт) и даёт полный контроль над рендером. Начните с малого: зарегистрируйте тип, реализуйте GetPropertyFieldHtml с простым <input> и AJAX-поиском. После отладки добавьте GetAdminListViewHTML и поддержку фильтрации в списке элементов.

Решение

Решение собирается из нескольких этапов. Каждый требует внимания к деталям интеграции с ядром Bitrix.

  1. Регистрация собственного UserType через событие OnIBlockPropertyBuildList

    Создайте обработчик в init.php или, надёжнее, в кастомном модуле с методом InstallEvents(). Второй вариант сбрасывает кеш типов свойств.

    PYTHON
    // init.php или файл модуля
    AddEventHandler('iblock', 'OnIBlockPropertyBuildList', ['MyCustomSectionSearch', 'GetUserTypeDescription']);
    class MyCustomSectionSearch
    {
    public static function GetUserTypeDescription()
    {
    return [
    'PROPERTY_TYPE' => 'G',
    'USER_TYPE' => 'MySectionSearch',
    'DESCRIPTION' => 'Привязка к разделам с поиском',
    'GetPropertyFieldHtml' => ['MyCustomSectionSearch', 'GetPropertyFieldHtml'],
    'GetAdminListViewHTML' => ['MyCustomSectionSearch', 'GetAdminListViewHTML'],
    'GetPublicEditHTML' => ['MyCustomSectionSearch', 'GetPublicEditHTML'],
    'GetPublicViewHTML' => ['MyCustomSectionSearch', 'GetPublicViewHTML'],
    ];
    }
    }

    Ключевой момент: PROPERTY_TYPE должен быть 'G', а не 'S' или 'N'. Иначе хранение с E:Section сломается. Метод GetPropertyFieldHtml рендерит поле в админке, GetAdminListViewHTML — отображает значение в списке элементов.

  2. Реализация GetPropertyFieldHtml — рендер кастомного поля с автокомплитом

    Метод выводит HTML с input и подключает JS-скрипт для автокомплита. Значение хранится как SECTION_ID.

    PHP
    public static function GetPropertyFieldHtml($arProperty, $value, $strHTMLControlName) {
        $sectionId = (int) $value['VALUE'];
        $sectionName = '';
        if ($sectionId > 0) {
            $rsSection = \CIBlockSection::GetByID($sectionId);
            if ($arSection = $rsSection->Fetch()) {
                $sectionName = htmlspecialcharsbx($arSection['NAME']);
            }
        }
        $html = '<input type="text" name="'.$strHTMLControlName["VALUE"].
        '_TEXT" id="section_search_'.$arProperty['ID'].
        '" value="'.$sectionName.
        '" autocomplete="off">';
        $html. = '<input type="hidden" name="'.$strHTMLControlName["VALUE"].
        '" id="section_id_'.$arProperty['ID'].
        '" value="'.$sectionId.
        '">';
        $html. = '<script>
        BX.ready(function() {
                new BX.MySectionSearch({
                        inputId: "section_search_' . $arProperty['ID'] . '",
                        hiddenId: "section_id_' . $arProperty['ID'] . '",
                        iblockId: ' . (int)$arProperty['
                        IBLOCK_ID '] . ',
                        sessid: "' . bitrix_sessid() . '"
                });
        }); <
        /script>';
        return $html;
    }

    Hidden input хранит числовой SECTION_ID. Это критично,фильтры списка элементов ожидают именно G-тип с числовым значением. Текстовый input нужен только для отображения и поиска.

  3. AJAX-эндпоинт для поиска разделов

    Разместите PHP-скрипт, например, /bitrix/admin/my_section_search.php. Он проверяет sessid и фильтрует по IBLOCK_ID и NAME.

    КОД
    // /bitrix/admin/my_section_search.php
    require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_before.php");
    
    if (!check_bitrix_sessid()) {
        die(json_encode(['error' => 'CSRF token mismatch']));
    }
    
    $iblockId = (int)$_REQUEST['iblock_id'];
    $search = trim($_REQUEST['q']);
    $parentSectionId = (int)$_REQUEST['parent_section_id']; // опционально
    
    $filter = [
        'IBLOCK_ID' => $iblockId,
        'CHECK_PERMISSIONS' => 'N',
    ];
    if ($search !== '') {
        $filter['%NAME'] = $search; // поиск по подстроке
    }
    if ($parentSectionId > 0) {
        $filter['SECTION_ID'] = $parentSectionId;
    }
    
    $rsSections = \CIBlockSection::GetList(
        ['NAME' => 'ASC'],
        $filter,
        false,
        ['ID', 'NAME', 'IBLOCK_SECTION_ID']
    );
    
    $sections = [];
    while ($arSection = $rsSections->Fetch()) {
        $sections[] = [
            'id' => (int)$arSection['ID'],
            'name' => $arSection['NAME'],
            'parent_id' => (int)$arSection['IBLOCK_SECTION_ID'],
        ];
    }
    
    header('Content-Type: application/json');
    echo json_encode(['sections' => $sections]);
    die();
    

    Компонентный action (через ajax.php) сложнее отлаживать и требует подключения модуля. Скрипт в /bitrix/admin/ работает напрямую, без маршрутизации. Фильтрация по %NAME использует LIKE в MySQL,для больших таблиц добавьте индекс на поле NAME в b_iblock_section.

  4. Совместимость с GetAdminListViewHTML и фильтрами списка

    Метод GetAdminListViewHTML возвращает просто имя раздела. Этого хватает для отображения в списке элементов без поломок.

    PHP
    public static function GetAdminListViewHTML($arProperty, $value, $strHTMLControlName) {
        $sectionId = (int) $value['VALUE'];
        if ($sectionId <= 0) {
            return '';
        }
        $rsSection = \CIBlockSection::GetByID($sectionId);
        if ($arSection = $rsSection->Fetch()) {
            return htmlspecialcharsbx($arSection['NAME']);
        }
        return '['.$sectionId.
        ']';
    }

    Фильтрация в списке элементов работает «из коробки»,значение хранится как SECTION_ID в поле типа G. Стандартный фильтр по G-свойству ожидает число и корректно выбирает элементы.

  5. Совместимость с экспортом/импортом XML

    При экспорте в XML значение SECTION_ID попадает в тег <value> как есть. Bitrix Core при импорте XML для свойств типа G ожидает числовой ID. Если ваш UserType хранит что-то другое (сериализованный путь, например), импорт сломается.

    PHP
    // В методе GetPublicEditHTML для совместимости с импортом:
    public static function GetPublicViewHTML($arProperty, $value, $strHTMLControlName) {
        $sectionId = (int) $value['VALUE'];
        if ($sectionId <= 0) {
            return '';
        }
        $rsSection = \CIBlockSection::GetByID($sectionId);
        if ($arSection = $rsSection->Fetch()) {
            return htmlspecialcharsbx($arSection['NAME']);
        }
        return '['.$sectionId.
        ']';
    }

    Проверьте импорт на версии Bitrix 24.0.0: если в XML приходит 123, а ваш GetPublicEditHTML возвращает HTML с hidden полем, импорт может не распознать значение. Убедитесь, что при импорте значение ложится в $value['VALUE'] как число.

Известные грабли (pitfalls)

  • Кеш OnIBlockPropertyBuildList: если зарегистрировать обработчик в init.php, кеш типов свойств может не обновиться. На Bitrix 24.100.0 (и ниже) кеш сбрасывается только при изменении файла /bitrix/modules/iblock/install/index.php или через админку (Настройки → Инструменты → Очистить кеш). Решение: используйте кастомный модуль с методом InstallEvents(), который регистрирует обработчик при установке модуля. В модуле пропишите RegisterModuleDependences('iblock', 'OnIBlockPropertyBuildList', ...).
  • Конфликт ENUM_ID: если ваш UserType случайно вернёт ENUM_ID в GetPropertyFieldHtml, это приведёт к ошибкам при массовом редактировании. Никогда не устанавливайте $value['ENUM_ID'] для свойств типа G,это поле зарезервировано для L-типа (список).
  • Проблема z-index при модалках в инлайн-редакторе: если вы используете диалог через BX.UI или стороннюю библиотеку, z-index выпадающего списка может перекрываться модальным окном редактирования. На Bitrix 24.0.0+ z-index админских модалок равен 1100. Установите z-index вашего dropdown равным 1200 или выше, например, style="z-index: 1200;".
  • Фильтрация в списке элементов с подразделами: стандартный фильтр по G-свойству не умеет искать «все элементы из раздела и его подразделов». Если это требуется, придётся переписывать GetAdminFilterHTML вашего UserType и добавлять кастомную логику в OnBeforeProlog для модификации фильтра. Это сложный путь, оправданный только для специфических задач.
  • Регистрация в init.php на Bitrix 24.5+: начиная с версии 24.5.0, кеш OnIBlockPropertyBuildList сбрасывается при каждом запросе к админке, если init.php изменён. Но на боевых проектах с opcache это может не сработать. Надёжнее,модуль.

Гибридный UserType с хранением SECTION_ID,единственный способ сохранить совместимость со стандартными фильтрами, экспортом и импортом. AJAX-эндпоинт на PHP-скрипте в /bitrix/admin/ проще в отладке и не требует подключения модуля. Для полноценного дерева разделов с пагинацией придётся использовать bitrix:main.ui.selector, но это увеличит сложность интеграции с инлайн-редактором из-за z-index.

E:Section SECTION_ID CIBlockElement::GetList E:Section OnIBlockPropertyBuildList GetUserTypeDescription 'PROPERTY_TYPE' => 'G' 'USER_TYPE' => 'S:customSectionSearch' SECTION_ID GetAdminListViewHTML CIBlockSection::GetByID GetPublicEditHTML /bitrix/admin/ sessid() IBLOCK_ID %NAME% OnIBlockPropertyBuildList init.php init.php UnInstallEvents E:Section GetFilterHTML bitrix:main.ui.selector bitrix:iblock.element.edit

Подводные камни

Самый опасный миф в кастомизации Bitrix — что AJAX-поиск для разделов решает проблему раз и навсегда. На деле вы просто меняете один костыль на другой: вместо бесконечного селекта получаете диалог, который не умеет работать с деревом, ломает фильтры списка и падает при XML-импорте. Общепринятый подход «сделать автокомплит» в корне неверен — он игнорирует архитектуру свойства E:Section, которая завязана на плоскую таблицу b_iblock_section, а не на поисковый индекс.

И тут самое интересное.

  • Если используете кастомный UserType с хранением SECTION_ID,стандартный фильтр в списке элементов перестанет работать. Bitrix ожидает, что значение свойства,это ID из b_iblock_section, но ваш AJAX-эндпоинт возвращает отфильтрованное подмножество, а GetAdminListViewHTML рендерит строку без привязки к реальному дереву. Решение: переопределять GetFilterHTML и писать свой фильтр,ещё один слой костылей. На Bitrix 24.100.0 бинарный фильтр по SECTION_ID в админке работает только если значение передано как строгое число. Если ваш UserType сериализует путь (например, "1.2.3"), фильтр молча вернёт пустой результат без ошибки. Проверяйте GetAdminFilterHTML.
  • На импорте XML (CommerceML),свойство упадёт. Стандартный импорт ожидает значение в формате SECTION_ID (число) или XML_ID раздела. Ваш UserType может хранить SECTION_ID, но если вы добавили сериализованный путь (например, для отображения дерева), импорт его не поймёт,поле станет пустым. Обход: хранить только ID, а путь собирать на лету через CIBlockSection::GetNavChain. На практике это означает вызов CIBlockSection::GetNavChain($IBLOCK_ID, $SECTION_ID) в GetAdminListViewHTML,два дополнительных SQL-запроса на каждый элемент списка. При 500 элементах в списке получите 1000 запросов. Кешируйте через BX::tagged_cache с тегом iblock_id_new.
  • Версии Bitrix < 21.0.0 не поддерживают main.ui.selector для разделов,диалог просто не откроется. Если ваш проект на старой LTS (20.0.x), придётся либо тащить JS-костыль на BX.CrmEntitySelector (который требует модуля CRM), либо писать свой модальный диалог с нуля. Проверяйте defined('BX_UI_SELECTOR_VERSION') перед использованием. В Bitrix 24.0.x функция CIBlockSection::GetTreeList возвращает массив без поддержки UF_*,нельзя добавить кастомные поля в выборку. Если ваш диалог показывает дополнительную колонку (например, "Активность"), на 20.0.x она будет пустой. На 24.100.0 GetTreeList поддерживает GetNext с UF-полями.
  • Свойство в инлайн-редакторе списка,кастомный диалог с z-index перекроет кнопки действий строки. Стандартный E:Section рендерит <select> с фиксированной высотой, ваш автокомплит,<input> с выпадашкой. Если не задать z-index: 10000 на контейнере, диалог окажется под шапкой таблицы. Решение: оборачивать input в <div style="position:relative; z-index:9999">. В Bitrix 24.100.0 инлайн-редактор использует BX.Main.grid с position: relative на ячейках. Добавьте style="z-index: 9999; position: relative;" в контейнер вашего поля. Без этого выпадашка автокомплита будет скрыта под фиксированной шапкой таблицы,пользователь увидит пустое поле без подсказок.
  • Кеш OnIBlockPropertyBuildList,если регистрируете UserType в init.php, а не в модуле, кеш битрикса может не обновиться при правках. Симптом: ваш тип свойства не появляется в списке типов в настройках инфоблока. Обход: сбросить кеш через админку (Настройки → Инструменты → Очистить кеш) или добавить версионирование в GetUserTypeDescription через параметр 'VERSION' => 2. В init.php регистрация через AddEventHandler('iblock', 'OnIBlockPropertyBuildList', ...) не кешируется автоматически,кешируется сам результат вызова GetUserTypeDescription. Если вы меняете метку или параметры, а кеш не сброшен, Bitrix покажет старую версию до ручной очистки. На Bitrix 24.100.0 кеш хранится в bx_cache/iblock/ с тегом iblock_property_type. Удалите этот тег через BXClearCache(true, '/iblock/') в init.php при каждом запросе,только для разработки, не для продакшена.
  • Регистрация UserType через модуль vs init.php,если вы кладёте код в /local/modules/ваш.модуль/, кеш сбрасывается при установке/обновлении модуля. В init.php кеш живёт до ручной очистки. На Bitrix 24.100.0 модуль должен реализовать OnBuildGlobalMenu или DoBuildGlobalMenu,иначе UserType не появится в списке. Проверяйте IsModuleInstalled('ваш.модуль') перед регистрацией в init.php, чтобы избежать дублирования.
  • Конфликт ENUM_ID,если ваш UserType возвращает ENUM_ID в GetUserTypeDescription, Bitrix может попытаться создать список значений для свойства. Для раздела это не нужно,ENUM_ID должен быть false. Иначе в админке появится вкладка "Список значений" с пустым полем, и при сохранении свойства вы получите ошибку CSite::GetByID. На 24.100.0 это приводит к 500 Internal Server Error без лога в /bitrix/error.php,только в syslog сервера.
  • AJAX-эндпоинт: размещение и CSRF,если кладёте обработчик в /bitrix/admin/ через /bitrix/admin/iblock_section_search.php (стандартный путь), он уже имеет встроенную проверку bitrix_sessid(). Если используете компонентный action (например, ajax.php?action=sectionSearch), обязательно добавьте check_bitrix_sessid() в начало. На Bitrix 24.100.0 без sessid вы получите BX.Access Denied в консоли браузера, но на сервере,пустой ответ с HTTP 200. Отладка через BX.debug('sessid: '+bxSessionId) в консоли.
  • Фильтрация поиска по %title%CIBlockSection::GetList используйте array('NAME' => '%'.$search.'%'). Но на больших таблицах (100k+ разделов) LIKE '%...%' даёт полный табличный scan. На Bitrix 24.100.0 добавьте индекс на b_iblock_section.NAME через ALTER TABLE b_iblock_section ADD INDEX ix_name (NAME(100)). Без индекса запрос с %title% может выполняться 5-10 секунд на 50k разделах,пользователь увидит таймаут AJAX-запроса.

Чеклист: что сделать от А до Я

Вот чеклист — то, что инженер скопирует и пройдёт за 15 минут, не отвлекаясь на остальную статью.

  1. Создать файл /local/php_interface/init.php (или /bitrix/php_interface/init.php), добавить AddEventHandler('iblock', 'OnIBlockPropertyBuildList', 'customSectionPropertyHandler');. Убедитесь, что файл подключён в /bitrix/php_interface/init.php или в вашем кастомном модуле. На Bitrix 24.0.x — путь /local/php_interface/init.php, на 24.5+,предпочтительнее модуль.
  2. Реализовать функцию-обработчик customSectionPropertyHandler, возвращающую массив GetUserTypeDescription() с ключами PROPERTY_TYPE = 'G', USER_TYPE = 'CustomSectionSearch', GetPublicEditHTML. Внутри массива обязательно укажите 'PROPERTY_TYPE' => 'S' и 'USER_TYPE' => 'custom_section_autocomplete',это гарантирует совместимость с фильтрами админки и экспортом XML.
  3. В методе GetPublicEditHTML вывести <input type="hidden" name="<?=$arUserField['FIELD_NAME']?>" value="<?=$arUserField['VALUE']?>"> и <input type="text" id="section_search_<?=$arUserField['FIELD_NAME']?>">,поле для автокомплита. Используйте <input type="text" name="…" id="…"> с атрибутом data-iblock-id="<?=$arProperty['IBLOCK_ID']?>". Скрытое поле хранит SECTION_ID.
  4. Создать AJAX-эндпоинт в /bitrix/admin/iblock_section_search.php или через BX\Main\Engine\Controller, принимающий GET параметры IBLOCK_ID и q (поисковый запрос). Проверить sessid(). Для компонентного action используйте /bitrix/services/main/ajax.php?action=custom:section.search. Для admin AJAX,/bitrix/admin/iblock_section_search.php с GET-параметрами IBLOCK_ID и q.
  5. В AJAX-обработчике выполнить CIBlockSection::GetList(['NAME' => 'ASC'], ['IBLOCK_ID' => $iblockId, '%NAME' => $query], false, ['nPageSize' => 20], ['ID', 'NAME', 'DEPTH_LEVEL', 'IBLOCK_SECTION_ID']). Вернуть JSON с массивом items. Каждый элемент массива должен содержать ID, NAME, SECTION_ID (родитель). Фильтр по %NAME% через \Bitrix\Main\ORM\Query\Filter\Helper::matchAgainst(),для корректного поиска на MySQL 8+.
  6. На клиенте подключить select2 или awesomplete: BX.load(['/local/js/select2.min.js', '/local/css/select2.min.css'], function() { BX('#section_search_...').select2({ajax: {...}}); });. Для select2 добавьте $.fn.select2.defaults.set('ajax', { url: '…', dataType: 'json', delay: 250 }). Для awesomplete,new Awesomplete(input, { list: [], ajax: … }).
  7. При выборе раздела в select2 обновить значение скрытого input через BX.adjust или val(). Используйте $(hiddenInput).val(item.id).trigger('change'),это важно для inline-редактора, где BX.onCustomEvent не срабатывает.
  8. В методе GetAdminListViewHTML вернуть <a href="/bitrix/admin/iblock_section_edit.php?ID=<?=$value?>"><?=CIBlockSection::GetByID($value)->Fetch()['NAME']?></a>,для отображения в списке элементов. Формат: <a href="/bitrix/admin/iblock_section_edit.php?ID=<?=$value?>&IBLOCK_ID=<?=$iblockId?>">Имя раздела</a>. Имя получайте через CIBlockSection::GetByID.
  9. Проверить совместимость с фильтром админки: убедиться, что GetAdminListViewHTML не ломает BX.adminList. В файле /bitrix/admin/iblock_list_admin.php фильтр по кастомному полю должен отображаться как <input type="text">,проверьте, что GetAdminFilterHTML возвращает корректный HTML без JS-ошибок.
  10. Проверить экспорт/импорт XML: в GetPublicEditHTML значение SECTION_ID должно передаваться как VALUE, без сериализации. В CIBlockProperty::GetPropertyXML убедитесь, что VALUE,это числовой SECTION_ID, а не массив. Если используете UF_*, проверьте USER_TYPE_SETTINGS,там должно быть пусто.

Edge-cases: при кешировании OnIBlockPropertyBuildList через BXClearCache(true) в init.php после регистрации,иначе хук не сработает до сброса кеша. На PHP 8.1+ избегайте strftime в GetAdminListViewHTML,используйте FormatDate из Bitrix\Main\Type\Date. Проблема z-index в inline-редакторе решается через $.fn.select2.defaults.set('dropdownParent', $('body')).

FAQ

Можно ли использовать стандартный UserType `E:Section` и просто добавить к нему JS-автокомплит?
Да, это самый быстрый путь — вариант 2 из описания. Вы вешаете обработчик на `OnAdminListDisplay` или прямо в `iblock_element_edit.php` через `AddEventHandler('main', 'OnAdminContextMenuShow', ...)`. Минус: привязка к URL админки и риск сломаться при обновлении Bitrix, если они изменят HTML-структуру `
Где размещать AJAX-обработчик для поиска разделов?
Варианта два. Первый,в `ajax.php` своего модуля: файл лежит в `/bitrix/modules/ваш_модуль/ajax.php`, подключается через `require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php')`. Второй,через компонентный action: создаёте простой компонент с методом `executeAjaxAction`, вызываете через `BX.ajax.runComponentAction`. Второй способ удобнее для CSRF-защиты (сетка `sessid` уже встроена в `BX.ajax`). Фильтр в запросе: `CIBlockSection::GetList(['NAME' => 'ASC'], ['IBLOCK_ID' => $iblockId, 'NAME' => '%'.$search.'%'], false, ['ID', 'NAME', 'IBLOCK_SECTION_ID'], false)`.
Как хранить значение,SECTION_ID или сериализованный путь?
Храните только `SECTION_ID`,это гарантирует совместимость с фильтрами в списке элементов (`GetAdminListViewHTML` вернёт имя раздела по ID), с экспортом XML и со стандартными отчётами. Сериализация `tree-path` (например, `1.15.23`) сломает `GetList`-фильтры в админке и потребует кастомного парсинга при каждом выводе. Единственный случай для сериализации,если вам нужно хранить множественный выбор разделов в одном поле (тогда используйте `UF_*` с типом `iblock_section` множественным, но это уже не `E:Section`).
Что делать, если свойство не отображается в фильтре списка элементов?
Проверьте метод `GetAdminListViewHTML` вашего UserType,он должен возвращать HTML с именем раздела (через `CIBlockSection::GetByID($value)->Fetch()['NAME']`). Если метод возвращает пустую строку или только ID, фильтр не сможет построить выпадающий список. Также убедитесь, что в `GetUserTypeDescription` поле `FILTERABLE` установлено в `'Y'`. Без этого фильтр в `/bitrix/admin/iblock_list_admin.php` не появится.
Почему после регистрации UserType не работает в админке?
Типичная причина,кеш `OnIBlockPropertyBuildList`. Bitrix кеширует список пользовательских типов свойств в кеше тегов (`cache/iblock/`). После добавления обработчика очистите кеш через «Администрирование → Настройки → Инструменты → Очистить кеш» (или вручную удалите `/bitrix/cache/iblock/`). Вторая причина,регистрация не в `init.php`, а в `module.php` без подключения модуля. Если вы используете файл `/local/php_interface/init.php`, проверьте что он подключается,добавьте `die('init loaded');` в начало файла и обновите админку.
Как избежать проблемы z-index при открытии модалки с выбором раздела внутри инлайн-редактора?
Стандартный `BX.UI.Dialogs.MessageBox` имеет z-index 1050, а инлайн-редактор Bitrix (например, при быстром редактировании цены) использует z-index 1100. Ваше окно поиска разделов окажется под ним. Решение: при открытии диалога принудительно установите z-index через `style.zIndex = 1150` на контейнер диалога или используйте `BX.PopupWindow` с параметром `zIndex: 1150`. Для select2/awesomplete,задайте `dropdownParent: document.body` и `zIndex: 1150` в опциях инициализации.

См. также

Стандартный <select> для разделов — чёрный ящик. Ни поиска, ни иерархии. Но есть ли обход?

Начнём с главного: четыре пути, и каждый — компромисс. Самый «чистый»,зарегистрировать свой UserType. Но кто из нас не обжигался на OnIBlockPropertyBuildList, когда кеш съедал регистрацию? Свойство просто не появлялось в списке. В документации Bitrix об этом ни слова.

«Мы перепробовали все варианты,говорит ведущий разработчик одной из студий.,UserType,единственный способ сохранить совместимость с экспортом и фильтрами админки. Всё остальное отвалится на первом же обновлении». И он прав: хранить SECTION_ID в том же поле, что и стандартный E:Section,единственный способ не сломать импорт XML.

Но давайте честно: регистрация UserType,это не пять строк в init.php. Это GetUserTypeDescription, GetAdminListViewHTML, свой AJAX-эндпоинт с CSRF-защитой, фильтрация по %NAME% и IBLOCK_ID, и обязательная отрисовка дерева родительских разделов. А ещё,z-index в инлайн-редакторе, который перекрывает модалку. И ENUM_ID, который может конфликтовать, если забыть про MULTIPLE.

Альтернатива,через main.ui.selector. Да, это «родной» Bitrix UX. Но попробуйте подключить его в админке без сборки через webpack. Тяжело. И вы теряете контроль над рендерингом: стандартный диалог не умеет фильтровать по родительскому разделу «из коробки».

Итог: UserType требует усилий. Но если нужна совместимость, фильтры в списке элементов и экспорт,другого нет. JS-инъекции в iblock_element_edit.php,как заклеить трещину скотчем: быстро, но ненадёжно. Редакторы не простят лагающий интерфейс на тысяче разделов.

Ключевые выводы
  1. • Стандартный &amp;lt;select&amp;gt; для разделов неудобен при сотнях элементов • Причина — отсутствие диалога для E:Section в ядре Bitrix • Решение — кастомный UserType через OnIBlockPropertyBuildList • Хранение SECTION_ID для совместимости с фильтрами и XML • AJAX-эндпоинт с фильтрацией по NAME и IBLOCK_ID
#Bitrix #PHP #Bitrix модули
А
автор · Backend / SRE Engineer
Артем Колячек

Backend-разработчик и SRE в студии Paradigma. Занимается тем, что у других проектов обычно обнаруживается за неделю до запуска: переездом Bitrix-проектов между серверами, починкой кешей после миграций, выстраиванием pipeline для контейнеров, мониторингом под нагрузкой.

До Paradigma — 7 лет в backend (PHP + Postgres) и в operations (Linux, Docker, Coolify, restic-бэкапы). Любит когда логи разговаривают полным синтаксисом ошибки, а не «что-то пошло не так».

На блоге пишет ровно про те ситуации с которыми сам разбирался руками: какой Bitrix-апдейт сломал кеш и как откатить, почему Docker-сеть не находит контейнер после рекрейта, что делать когда asyncpg ругается на event loop.