Выпадающий список BX.Main.ui.select
Выпадающий список BX.Main.ui.select — контрол главного модуля для выбора одного или нескольких пунктов из заранее заданного набора. Список поддерживает группировку пунктов под заголовками и поиск по пунктам.
На этом контроле построены поля-списки фильтра main.ui.filter, поля множественного выбора во встроенном редакторе main.ui.grid и поле пользовательского свойства типа «Список» в форме редактирования.
В дизайн-системе есть отдельный компонент ui.select с классом BX.Ui.Select. Он поддерживает только одиночный выбор, а BX.Main.ui.select — одиночный и множественный. Для выбора объектов в дизайн-системе есть диалог ui.entity-selector. Он по умолчанию разрешает выбрать несколько элементов.
Подключить расширение
Загрузите ui из PHP-кода страницы или шаблона компонента.
\CJSCore::Init(['ui']);
Расширение ui подключает три расширения главного модуля:
-
ui_select— класс спискаBX.Main.ui.selectи всплывающее окно main.popup, в котором открываются пункты, -
ui_factory— фабрикуBX.Main.ui.Factory, которая создает экземпляры списков, и функциюBX.decl()для сборки разметки, -
ui_date— поле даты, которое использует та же фабрика.
Подключайте именно ui, а не только ui_select. Без фабрики из ui_factory клик по списку ничего не открывает, потому что экземпляр класса создает фабрика.
Расширение не экспортирует ES-модуль. Обращайтесь к классу и фабрике через глобальные имена BX.Main.ui.select и BX.Main.ui.Factory.
Вывести список
Чтобы показать поле на странице, выполните два действия:
-
Соберите разметку функцией
BX.decl(). Для одиночного выбора передайте блокmain-ui-select, для множественного —main-ui-multi-select. -
Вставьте полученный DOM-узел в страницу.
Экземпляр BX.Main.ui.select создает фабрика BX.Main.ui.Factory, когда пользователь первый раз кликает по полю или ставит в него фокус. Вызывать конструктор вручную не нужно.
Пример. Страница выводит одиночный список типов документов. Выбранным при загрузке показывается пункт «Счет».
<?php
\CJSCore::Init(['ui']);
?>
<div id="document-type"></div>
<script>
BX.ready(() => {
const items = [
{ NAME: 'Договор', VALUE: 'contract' },
{ NAME: 'Счет', VALUE: 'invoice' },
{ NAME: 'Акт', VALUE: 'act' },
{ NAME: 'Накладная', VALUE: 'waybill' },
];
const node = BX.decl({
block: 'main-ui-select',
name: 'DOCUMENT_TYPE',
items: items,
value: items[1],
params: { isMulti: false },
});
BX('document-type').appendChild(node);
});
</script>
Поле показывает подпись выбранного пункта.

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

Ключи декларации
Функция BX.decl() записывает пункты, значение и параметры в атрибуты data-items, data-value и data-params корневого узла.
Обязательные ключи обозначены *.
| Ключ | Описание |
|---|---|
block*string |
Вид списка. main-ui-select — одиночный выбор, main-ui-multi-select — множественный. Без ключа функция BX.decl() возвращает null |
items*array |
Пункты списка. Состав полей пункта описан в разделе Пункты списка. Без ключа создание экземпляра завершается ошибкой разбора JSON |
valueobject, array |
Начальное значение. Для одиночного списка — объект пункта, для множественного — массив пунктов. Список сопоставляет значение с пунктом по совпадению обоих полей NAME и VALUE |
paramsobject |
Параметры экземпляра — режим выбора, поиск и класс окна |
namestring |
Значение атрибута data-name корневого узла. Поле формы с этим именем список не создает |
valueDeleteboolean |
Добавляет в поле кнопку очистки. Во множественном списке кнопка снимает все выбранные пункты и скрыта, пока ничего не выбрано. В одиночном списке выбор не сбрасывает. Кнопка становится видна при наведении на поле. До первого клика по полю кнопка скрыта, даже если пункты выбраны |
placeholderstring |
Подсказка в строке ввода. Действует только для блока main-ui-multi-select |
tabindexnumber |
Значение атрибута tabindex строки ввода |
attrsobject |
Дополнительные атрибуты корневого узла |
Список не создает поле <input name="..."> и не отправляет значение с формой. Как передать выбор на сервер, показано в разделе Получить выбранное значение.
Пункты списка
Каждый элемент массива items — объект с полями в верхнем регистре.
-
NAME— подпись пункта. Список экранирует HTML в подписи. -
VALUE— значение пункта, которое получает код. -
HTML— разметка подписи вместоNAME. Действует только во множественном списке. Разметка выводится и в пункте, и в выбранном элементе поля. -
LEGEND— при значенииtrueпункт становится заголовком группы. Заголовок нельзя выбрать. -
DEPTH— уровень вложенности. Список сдвигает пункт вправо на десять пикселей за каждый уровень.
Задавайте пунктам уникальные VALUE и NAME. При снятии пункта во множественном списке из значения уходят все пункты, у которых совпадает VALUE или NAME.
Пример. Пункты единиц измерения сгруппированы под двумя заголовками, а пункты внутри групп сдвинуты на один уровень.
const units = [
{ NAME: 'Масса', VALUE: 'mass', LEGEND: true },
{ NAME: 'Килограмм', VALUE: 'kg', DEPTH: 1 },
{ NAME: 'Грамм', VALUE: 'g', DEPTH: 1 },
{ NAME: 'Длина', VALUE: 'length', LEGEND: true },
{ NAME: 'Метр', VALUE: 'm', DEPTH: 1 },
{ NAME: 'Сантиметр', VALUE: 'cm', DEPTH: 1 },
];
Заголовки групп выводятся жирным шрифтом, пункты групп сдвинуты вправо. Высота окна ограничена 177 пикселями, пункты сверх этой высоты прокручиваются.

Передать параметры экземпляра
Режим выбора, поиск и класс окна задают в ключе params декларации. Все параметры необязательны.
| Параметр | Тип | Описание |
|---|---|---|
isMulti |
boolean |
Включает множественный выбор. Клик по пункту отмечает его или снимает отметку, а список остается открытым. По умолчанию false |
enableSearch |
boolean |
Включает поиск по пунктам. Условия работы описаны в разделе Включить поиск по пунктам. По умолчанию false |
classPopup |
string |
Добавляет CSS-класс окну со списком. По умолчанию не задан |
Параметр isMulti задает поведение экземпляра, а ключ block — разметку поля. Для множественного списка передайте и блок main-ui-multi-select, и параметр isMulti: true.
Во множественном списке выбранные пункты выводятся в поле отдельными элементами с крестиком, а пункты в окне — с флажками.

Остальные ключи params список сохраняет. Метод getParams() экземпляра возвращает объект params целиком, поэтому в нем можно передать свои данные для обработчика, например имя поля формы.
Включить поиск по пунктам
Передайте enableSearch: true в params, чтобы пользователь отбирал варианты по введенному тексту.
Параметр enableSearch появился в главном модуле версии 26.900.0. Во множественном списке с isMulti: true поиск работает с версии 26.1000.0, в более ранних версиях параметр для такого списка не действует.
Поиск включается, только если в списке не меньше десяти пунктов. Заголовки с LEGEND: true в это число не входят. Порог задан в коде класса, параметром его не изменить.
Если пунктов меньше или поиск не включен, строка ввода работает в режиме быстрого перехода. Список подсвечивает первый пункт, подпись которого начинается с введенных символов, без учета регистра. Через секунду после последнего ввода список очищает строку ввода. Пункты при этом не скрываются.
Пример. Множественный список единиц измерения с поиском выводится в контейнер units. В нем 12 пунктов, поэтому поиск работает.
const node = BX.decl({
block: 'main-ui-multi-select',
name: 'UNITS',
items: [
{ NAME: 'Штука', VALUE: 'pcs' },
{ NAME: 'Упаковка', VALUE: 'pack' },
{ NAME: 'Килограмм', VALUE: 'kg' },
{ NAME: 'Грамм', VALUE: 'g' },
{ NAME: 'Тонна', VALUE: 't' },
{ NAME: 'Литр', VALUE: 'l' },
{ NAME: 'Миллилитр', VALUE: 'ml' },
{ NAME: 'Метр', VALUE: 'm' },
{ NAME: 'Сантиметр', VALUE: 'cm' },
{ NAME: 'Миллиметр', VALUE: 'mm' },
{ NAME: 'Квадратный метр', VALUE: 'm2' },
{ NAME: 'Кубический метр', VALUE: 'm3' },
],
value: [],
placeholder: 'Начните вводить единицу',
valueDelete: true,
params: {
isMulti: true,
enableSearch: true,
},
});
BX('units').appendChild(node);
Поиск работает по пяти правилам.
-
При открытии списка фокус переходит в строку поиска. В одиночном списке строка поиска на время открытия заменяет подпись выбранного пункта.
-
Список оставляет пункты, подпись которых содержит запрос в любом месте, без учета регистра. Запрос
метрнайдет пункты «Метр», «Сантиметр», «Миллиметр», «Квадратный метр» и «Кубический метр». -
Поиск не скрывает заголовки с
LEGEND: true. -
Список подсвечивает первый найденный пункт, клавиша
Enterвыбирает его. Если не найден ни один пункт,Enterничего не выбирает. -
При закрытии список сбрасывает запрос и в следующий раз открывается со всеми пунктами.
Во множественном списке после выбора клавишей Enter список очищает строку поиска, снова показывает все пункты и остается открытым. Пользователь может сразу ввести следующий запрос.
Получить выбранное значение
Список хранит выбор в атрибуте data-value корневого узла в виде JSON. Одиночный список хранит объект пункта, множественный — массив пунктов.
Прочитать значение можно двумя способами.
-
Разобрать атрибут
data-valueузла. Способ работает в любой момент, в том числе до первого клика по полю, когда экземпляра еще нет. -
Вызвать метод
getDataValue()экземпляра. Он возвращает объект или массив выбранных пунктов, а если выбора нет — пустой объект{}для одиночного списка и пустой массив[]для множественного.
Метод BX.Main.ui.Factory.get(node) находит экземпляр по корневому узлу. Он возвращает объект с полями node и instance или null, если фабрика еще не создавала экземпляр для этого узла.
Фабрика не создает экземпляр по запросу. Если экземпляр нужен до первого клика, создайте его конструктором и добавьте в реестр BX.Main.ui.Factory.data, иначе при клике фабрика создаст для узла второй экземпляр. Узел к этому моменту должен быть на странице и виден, потому что ширину окна экземпляр замеряет в конструкторе. Событие UiSelect::onCreatePopup сработает сразу.
Пример. Код получает экземпляр списка или создает его, если фабрика еще этого не сделала.
let control = BX.Main.ui.Factory.get(node);
if (!control)
{
control = { node: node, instance: new BX.Main.ui.select(node) };
BX.Main.ui.Factory.data.push(control);
}
Пример. Перед отправкой формы units-form обработчик читает выбранные единицы измерения из множественного списка и записывает их значения в скрытые поля UNITS[] внутри контейнера units-values.
// Узел node создан в примере множественного списка с поиском
BX.bind(BX('units-form'), 'submit', () => {
const selected = JSON.parse(node.dataset.value || '[]');
const container = BX('units-values');
BX.cleanNode(container);
selected.forEach((item) => {
container.appendChild(BX.create('input', {
attrs: {
type: 'hidden',
name: 'UNITS[]',
value: item.VALUE,
},
}));
});
});
Обработать выбор пункта
Список вызывает два глобальных события. Подписывайтесь на них функцией BX.addCustomEvent() на объекте window. Как устроены такие подписки, описано в статье JS-ядро main.core. Подписка через EventEmitter.subscribe('UI::Select::change', handler) тоже работает. В этом случае экземпляр и пункт приходят массивом в event.getData().
| Событие | Аргументы обработчика | Когда срабатывает |
|---|---|---|
UI::Select::change |
Экземпляр BX.Main.ui.select и объект пункта |
При выборе пункта кликом или клавишей Enter, а во множественном списке — и при снятии отметки кликом по пункту |
UiSelect::onCreatePopup |
Окно BX.Main.Popup и массив пунктов |
При создании экземпляра, до первого показа окна |
Событие UI::Select::change передает в обработчик пункт, по которому кликнули, а не весь выбор. Во множественном списке текущий набор пунктов читайте методом getDataValue().
Событие UI::Select::change не срабатывает, когда пользователь удаляет элемент поля крестиком или клавишей Backspace либо нажимает кнопку очистки из valueDelete. Если значение нужно отправить с формой, читайте его при отправке, как в разделе Получить выбранное значение.
Пример. Обработчик реагирует только на свой список. Он сравнивает корневой узел экземпляра с узлом node и загружает шаблоны для выбранного типа документа.
BX.addCustomEvent(window, 'UI::Select::change', (select, item) => {
if (select.getNode() !== node)
{
return;
}
loadTemplates(item.VALUE);
});
Событие глобальное, поэтому обработчик получает выбор во всех списках BX.Main.ui.select на странице, включая поля фильтра. Метод getNode() возвращает корневой узел экземпляра — по нему обработчик отличает свой список от чужих.
Чтобы реагировать на любое изменение выбора, включая удаление крестиком и очистку, наблюдайте за атрибутом data-value через стандартный MutationObserver браузера. Так делает фильтр main.ui.filter.
Пример. Счетчик выбранных единиц измерения обновляется при каждом изменении выбора.
const observer = new MutationObserver(() => {
const selected = JSON.parse(node.dataset.value || '[]');
updateUnitsCounter(selected.length);
});
observer.observe(node, { attributes: true, attributeFilter: ['data-value'] });
Использовать клавиатуру
Пока фокус находится в строке ввода, список обрабатывает клавиши навигации и выбора.
| Клавиша | Действие |
|---|---|
ArrowDown, ArrowUp |
Подсвечивают следующий или предыдущий пункт. После последнего пункта подсветка переходит на первый. Без поиска подсветка проходит и по заголовкам групп. При включенном поиске переход идет только по найденным пунктам |
Enter |
Выбирает подсвеченный пункт |
Backspace |
Во множественном списке при пустой строке ввода первое нажатие выделяет последний выбранный элемент, второе снимает его выбор |
Клавиша Esc список не закрывает. Список закрывается, когда строка ввода теряет фокус, а одиночный список — еще и после выбора пункта.
Изменить значение и пункты из кода
У списка нет методов, которые задают значение или заменяют пункты. Пункты из data-items экземпляр читает один раз, при создании. Неактивное состояние список тоже не поддерживает — у класса нет такого параметра или метода.
Чтобы задать новое значение или новый набор пунктов, соберите новый узел функцией BX.decl() и замените им старый. Событие UI::Select::change при этом не срабатывает.
Пример. Функция находит экземпляр старого списка методом BX.Main.ui.Factory.get(), удаляет его окно вызовом getPopup().destroy() и заменяет узел новым с переданными пунктами и значением.
function rebuildSelect(oldNode, items, value)
{
const control = BX.Main.ui.Factory.get(oldNode);
if (control)
{
control.instance.getPopup().destroy();
}
const newNode = BX.decl({
block: 'main-ui-select',
name: 'DOCUMENT_TYPE',
items: items,
value: value,
params: { isMulti: false },
});
BX.Dom.replace(oldNode, newNode);
return newNode;
}