TL;DR
Стандартная «Привязка к разделам» в Bitrix — это мусор, который ломается уже на 50 разделах. Разработчики годами терпят плоский список без поиска, хотя для элементов инфоблоков есть нормальный модальный диалог. Общепринятый подход «смириться и листать» в корне неверен — мы просто заменим нативное поле на кастомный UserType с AJAX-поиском. Это займёт полдня.
-
Регистрируем собственный тип свойства через хук
OnIBlockPropertyBuildList. Шаг обязательный,без него Bitrix не узнает о новом типе. Кладём обработчик вinit.phpили в метод модуля.PHPAddEventHandler('iblock', 'OnIBlockPropertyBuildList', function() { return [ 'PROPERTY_TYPE' => 'S', 'USER_TYPE' => 'SectionSearch', 'DESCRIPTION' => 'Раздел с поиском (кастом)', 'GetPropertyFieldHtml' => ['CSectionSearchProperty', 'GetPropertyFieldHtml'], 'GetAdminListViewHTML' => ['CSectionSearchProperty', 'GetAdminListViewHTML'], 'GetPublicEditHTML' => ['CSectionSearchProperty', 'GetPublicEditHTML'], ]; });После этого заходим в список свойств инфоблока,видим новый тип «Раздел с поиском (кастом)» в выпадающем списке.
-
Реализуем класс
CSectionSearchPropertyс методом-рендером полей. МетодGetPropertyFieldHtmlвозвращает HTML с input и скрытым полем для SECTION_ID. AJAX-запросы будем отправлять на самописный компонентный action.PHPclass 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 . '" > '; } }В админке на странице редактирования элемента увидим текстовое поле вместо старого списка.
-
Пишем AJAX-эндпоинт для поиска разделов. Проще всего,добавить action в
local/components/studio/section.search/ajax.php. Не забываем про CSRF-токен и фильтрацию по%NAME%иIBLOCK_ID.PHPrequire_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, названиями разделов и отступами по уровню вложенности.
-
Подключаем JavaScript-виджет на страницу админки. Используем стандартный
BX.UI.Selectorили простой самописный скрипт сfetch. Главное,при выборе раздела заполнять скрытое поле SECTION_ID.JSBX.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+ символов появляется выпадающий список с разделами, соответствующими поиску.
-
Обеспечиваем совместимость с админ-фильтрами и списком элементов. Для этого реализуем метод
GetAdminListViewHTML, который отображает название раздела в таблице. Код минимальный,просто получаем имя раздела по ID.PHPpublic 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.
-
Регистрируем кастомный тип свойства через хук
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'], ]; });После этого в списке типов свойств появится пункт «Привязка к разделам с поиском».
-
Создаём класс-обработчик
CSectionSearchPropertyс методомGetPropertyFieldHtml. Он рендерит<input>с автокомплитом и скрытое поле для храненияSECTION_ID. Хранение как у стандартногоE:Section,совместимость с XML-импортом и фильтрами админки.PHPclass 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-запрос к эндпоинту.
-
Реализуем 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 с разделами. -
Обрабатываем выбор в 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.
-
Реализуем метод GetAdminListViewHTML для отображения значения в списке элементов. Возвращаем название раздела, а не ID,так удобнее.
PHPpublic 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 ''; }В списке элементов в колонке свойства должно отображаться название раздела.
-
Учитываем совместимость с фильтрами и экспортом. Стандартный
E:SectionхранитSECTION_ID,наш UserType делает то же самое. Фильтры админки по умолчанию не поддерживают кастомный тип, но можно добавить свой фильтр черезGetAdminFilterHTML.PHPpublic 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-поиском и совместимостью с фильтрами админки без риска сломаться при следующем обновлении. Да, это больше кода, но это работает предсказуемо и не требует правки файлов ядра.
-
Регистрация собственного 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)или перезапустите сессию админки. -
Реализация 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. -
Рендер поля в форме редактирования: 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. -
Совместимость с фильтрами админки: GetAdminListViewHTML
Этот метод вызывается при отображении значения свойства в таблице списка элементов. Он должен возвращать строку с названием раздела,простой текст или ссылку на редактирование раздела. Не используйте здесь сложный HTML, так как ядро экранирует вывод. Пример:
PHPpublic static function GetAdminListViewHTML($arProperty, $value, $strHTMLControlName) { if ($value > 0) { $rsSection = CIBlockSection::GetByID($value); if ($arSection = $rsSection->Fetch()) { return htmlspecialcharsbx($arSection['NAME']); } } return '—'; }Фильтр в админке (
GetAdminFilterHTML) должен рендерить такой же<select>с автокомплитом, но без привязки к конкретному элементу,передавайте пустое значение по умолчанию. -
Совместимость с экспортом/импортом XML и миграциями
Поскольку значение хранится как
SECTION_ID(число, а не сериализованный путь), стандартные механизмы экспорта/импорта Bitrix (например,CIBlockElement::ExportXml) обработают его корректно. При импорте черезCIBlockXMLFile::ImportElementсвойство будет заполнено как обычная привязка к разделу. Не используйте сериализацию дерева разделов,это сломает поиск по фильтрам и агенты обновления.Для миграций (
bitrix:sale.export.1cили кастомные скрипты) убедитесь, чтоPROPERTY_TYPEвашего UserType равенG. Иначе 1С-обмен может игнорировать поле или выдавать ошибкуProperty type mismatch. -
Известные грабли и их решения
- Кеш 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).
- Кеш OnIBlockPropertyBuildList: после добавления обработчика в
Альтернативные подходы (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.
-
Регистрация собственного 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— отображает значение в списке элементов. -
Реализация
GetPropertyFieldHtml— рендер кастомного поля с автокомплитомМетод выводит HTML с input и подключает JS-скрипт для автокомплита. Значение хранится как
SECTION_ID.PHPpublic 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 нужен только для отображения и поиска. -
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. -
Совместимость с
GetAdminListViewHTMLи фильтрами спискаМетод
GetAdminListViewHTMLвозвращает просто имя раздела. Этого хватает для отображения в списке элементов без поломок.PHPpublic 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-свойству ожидает число и корректно выбирает элементы. -
Совместимость с экспортом/импортом 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.0GetTreeListподдерживает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 минут, не отвлекаясь на остальную статью.
- Создать файл
/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+,предпочтительнее модуль. - Реализовать функцию-обработчик
customSectionPropertyHandler, возвращающую массивGetUserTypeDescription()с ключамиPROPERTY_TYPE='G',USER_TYPE='CustomSectionSearch',GetPublicEditHTML. Внутри массива обязательно укажите'PROPERTY_TYPE' => 'S'и'USER_TYPE' => 'custom_section_autocomplete',это гарантирует совместимость с фильтрами админки и экспортом XML. - В методе
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. - Создать 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. - В 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+. - На клиенте подключить
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: … }). - При выборе раздела в select2 обновить значение скрытого
inputчерезBX.adjustилиval(). Используйте$(hiddenInput).val(item.id).trigger('change'),это важно для inline-редактора, гдеBX.onCustomEventне срабатывает. - В методе
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. - Проверить совместимость с фильтром админки: убедиться, что
GetAdminListViewHTMLне ломаетBX.adminList. В файле/bitrix/admin/iblock_list_admin.phpфильтр по кастомному полю должен отображаться как<input type="text">,проверьте, чтоGetAdminFilterHTMLвозвращает корректный HTML без JS-ошибок. - Проверить экспорт/импорт 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,как заклеить трещину скотчем: быстро, но ненадёжно. Редакторы не простят лагающий интерфейс на тысяче разделов.
- • Стандартный &lt;select&gt; для разделов неудобен при сотнях элементов • Причина — отсутствие диалога для E:Section в ядре Bitrix • Решение — кастомный UserType через OnIBlockPropertyBuildList • Хранение SECTION_ID для совместимости с фильтрами и XML • AJAX-эндпоинт с фильтрацией по NAME и IBLOCK_ID