В интерфейсах автодополнения с большим набором данных группировка становится не декоративной, а структурной задачей: список перестаёт быть плоским и превращается в иерархию, где элементы логически разделены по категориям. В контексте Awesomplete это достигается не встроенной опцией «группы», а комбинацией формата данных, кастомного рендера и управления фильтрацией.
Базовая проблема заключается в том, что стандартный список Awesomplete принимает либо строки, либо объекты вида:
{ label: "Moscow", value: "Moscow" }
Чтобы реализовать группы, список расширяется дополнительным уровнем абстракции:
const data = [
{ group: "Страны", label: "Казахстан", value: "KZ" },
{ group: "Страны", label: "Россия", value: "RU" },
{ group: "Города", label: "Алматы", value: "Almaty" },
{ group: "Города", label: "Караганда", value: "Karaganda" }
];
На этом уровне Awesomplete ещё не знает о группах — они существуют только как метаданные.
Один из подходов заключается в предварительной трансформации массива, когда в поток данных добавляются «разделители»:
function buildGroupedList(items) {
const result = [];
let lastGroup = null;
for (const item of items) {
if (item.group !== lastGroup) {
result.push({
label: item.group,
value: "__group__",
isGroup: true
});
lastGroup = item.group;
}
result.push(item);
}
return result;
}
Такой подход создаёт псевдо-элементы, которые визуально будут разделителями.
Ключевой механизм — переопределение рендера строки через
item():
new Awesomplete(input, {
list: buildGroupedList(data),
item: function(text, input) {
const li = document.createElement("li");
if (text.isGroup) {
li.textContent = text.label;
li.className = "awesomplete-group";
li.setAttribute("aria-disabled", "true");
return li;
}
li.textContent = text.label;
li.setAttribute("data-value", text.value);
return li;
}
});
Здесь появляется ключевая идея: группа не является выбираемым элементом и визуально отделяется стилями.
Даже если элемент отображается как заголовок группы, Awesomplete по умолчанию может пытаться интерпретировать его как валидный элемент. Поэтому требуется фильтрация взаимодействия.
Добавляется логика через replace:
input.addEventListener("awesomplete-select", function(e) {
if (e.text.value === "__group__") {
e.preventDefault();
}
});
Это предотвращает вставку служебных элементов.
Визуальное разделение обычно строится через CSS:
.awesomplete ul li.awesomplete-group {
font-weight: bold;
background: #f0f0f0;
cursor: default;
pointer-events: none;
padding: 6px 10px;
}
Важно отключить взаимодействие, чтобы группа не участвовала в hover и click поведении.
Вместо вставки специальных элементов в список можно использовать
условный рендеринг прямо в item():
item: function(text) {
if (text.type === "group") {
const li = document.createElement("li");
li.className = "group-separator";
li.textContent = text.title;
return li;
}
const li = document.createElement("li");
li.textContent = text.label;
return li;
}
Такой вариант упрощает структуру данных, но усложняет визуальную логику.
Стандартный filter Awesomplete может разрушить структуру
групп, поскольку он работает на уровне отдельных элементов. Для
сохранения группировки применяется кастомный фильтр:
Awesomplete.$.FILTER = function(text, input) {
if (text.isGroup) return true;
return text.label.toLowerCase().includes(input.toLowerCase());
};
Однако более корректный подход — фильтровать только элементы данных, сохраняя группы как «контейнеры»:
function filterGroupedList(items, query) {
const groups = new Map();
for (const item of items) {
if (!groups.has(item.group)) {
groups.set(item.group, []);
}
if (item.label.toLowerCase().includes(query.toLowerCase())) {
groups.get(item.group).push(item);
}
}
const result = [];
for (const [group, values] of groups.entries()) {
if (values.length === 0) continue;
result.push({ isGroup: true, label: group });
result.push(...values);
}
return result;
}
При наличии разделителей возникает проблема навигации: стрелки вверх/вниз не должны фокусироваться на группах.
Решение реализуется через модификацию поведения списка:
list.addEventListener("keydown", function(e) {
const items = Array.from(list.querySelectorAll("li:not(.awesomplete-group)"));
// логика пересчёта активного элемента только среди валидных
});
Это требует синхронизации с внутренним состоянием Awesomplete, чтобы индекс выделения не включал служебные строки.
Иногда группы используются не как заголовки, а как визуальные разделители без текста:
{ isSeparator: true }
И рендер:
if (text.isSeparator) {
li.className = "separator";
li.innerHTML = "";
return li;
}
CSS:
.separator {
height: 1px;
margin: 4px 0;
background: #ddd;
pointer-events: none;
}
Сортировка часто конфликтует с группировкой, поскольку стандартная сортировка разрушает порядок категорий. Поэтому применяется стратегия стабильной сортировки внутри группы:
function sortGrouped(items) {
return items.sort((a, b) => {
if (a.group === b.group) {
return a.label.localeCompare(b.label);
}
return a.group.localeCompare(b.group);
});
}
После этого результат снова пропускается через функцию вставки групповых заголовков.
Наиболее устойчивой становится модель, где:
isGroup,
isSeparator).{
label: string,
value: string,
group: string,
isGroup?: boolean,
isSeparator?: boolean
}
Такой формат позволяет сохранять предсказуемость поведения и минимизировать зависимость от UI-логики внутри ядра автодополнения.