Разделители между группами

В интерфейсах автодополнения с большим набором данных группировка становится не декоративной, а структурной задачей: список перестаёт быть плоским и превращается в иерархию, где элементы логически разделены по категориям. В контексте 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()

Ключевой механизм — переопределение рендера строки через 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 поведении.


Альтернативный подход: разделители через DOM-логiku

Вместо вставки специальных элементов в список можно использовать условный рендеринг прямо в 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-логики внутри ядра автодополнения.