Группировка элементов

Базовая модель работы Awesomplete предполагает линейный список строк или объектов, который фильтруется и отображается в виде плоской выдачи. При необходимости группировки элементов эта модель расширяется за счёт кастомизации трёх ключевых механизмов: list, filter, item, а также частично sort. Группировка не является встроенной функцией, поэтому вся логика реализуется на уровне представления и подготовки данных.

Группировка в контексте автодополнения обычно означает визуальное и логическое разделение элементов на категории без изменения базового механизма выбора значения.


Структура данных для категорий

Для организации группировки требуется перейти от простых строк к объектной модели данных. Каждый элемент списка начинает включать метаданные:

const data = [
  { label: "JavaScript", value: "JavaScript", group: "Languages" },
  { label: "TypeScript", value: "TypeScript", group: "Languages" },
  { label: "React", value: "React", group: "Frameworks" },
  { label: "Vue", value: "Vue", group: "Frameworks" }
];

В Awesomplete допустимо использовать объекты, где:

  • label — текст для отображения
  • value — значение, подставляемое в input
  • дополнительные поля (group) используются для логики отображения

Предварительная сортировка по группам

Поскольку Awesomplete отображает список в том порядке, в котором он приходит, сортировка выполняется заранее либо через sort-функцию библиотеки.

awesomplete.sort = (a, b) => {
  if (a.group === b.group) {
    return a.label.localeCompare(b.label);
  }
  return a.group.localeCompare(b.group);
};

Такой подход обеспечивает стабильное расположение элементов внутри одной категории и группировку по алфавиту или заданному порядку категорий.


Кастомизация отображения элементов

Ключевой механизм группировки — функция item, которая отвечает за генерацию DOM-элемента.

awesomplete.item = function (text, input) {
  const li = document.createElement("li");

  li.textContent = text.label || text.value;
  li.setAttribute("data-value", text.value);

  if (text.group) {
    li.setAttribute("data-group", text.group);
    li.classList.add("awesomplete-item");
  }

  return li;
};

Через эту функцию можно добавлять дополнительные атрибуты, классы и структуру, включая визуальные маркеры групп.


Добавление заголовков групп через псевдо-элементы

Awesomplete не поддерживает отдельные заголовки списков, поэтому применяется техника вставки “служебных элементов”. Такие элементы не участвуют в выборе, но служат визуальными разделителями.

function buildGroupedList(data) {
  const result = [];
  let currentGroup = null;

  data.forEach(item => {
    if (item.group !== currentGroup) {
      currentGroup = item.group;

      result.push({
        label: currentGroup,
        value: "",
        isHeader: true
      });
    }

    result.push(item);
  });

  return result;
}

Далее в item происходит различие:

awesomplete.item = function (text) {
  const li = document.createElement("li");

  if (text.isHeader) {
    li.textContent = text.label;
    li.classList.add("awesomplete-group-header");
    li.setAttribute("aria-disabled", "true");
  } else {
    li.textContent = text.label;
    li.setAttribute("data-value", text.value);
  }

  return li;
};

Такой подход позволяет визуально отделить группы без изменения внутренней логики библиотеки.


Исключение заголовков из выбора

Псевдо-элементы групп не должны участвовать в выборе. Для этого применяется replace:

awesomplete.replace = function (suggestion) {
  if (suggestion.isHeader) return;

  this.input.value = suggestion.value;
};

Также можно дополнительно блокировать навигацию:

awesomplete.filter = function (text, input) {
  if (text.isHeader) return false;
  return Awesomplete.FILTER_CONTAINS(text.label, input);
};

Кастомная фильтрация с учётом групп

При группировке часто требуется сохранить контекст категорий даже после фильтрации. Это достигается через расширенную фильтрацию списка:

awesomplete.filter = function (text, input) {
  if (text.isHeader) return false;

  return text.label.toLowerCase().includes(input.toLowerCase());
};

При более сложной логике можно фильтровать по группе:

awesomplete.filter = function (text, input) {
  if (text.isHeader) return false;

  const matchText = text.label.toLowerCase().includes(input.toLowerCase());
  const matchGroup = text.group && text.group.toLowerCase().includes(input.toLowerCase());

  return matchText || matchGroup;
};

Поддержка клавиатурной навигации

Псевдо-заголовки групп могут ломать стандартную навигацию стрелками. Для корректной работы требуется исключить их из фокуса.

awesomplete.data = buildGroupedList(data);

И дополнительная фильтрация на уровне DOM:

.awesomplete-group-header {
  font-weight: bold;
  pointer-events: none;
  cursor: default;
}

Для строгого управления навигацией можно переопределить внутреннюю логику выбора активного элемента через модификацию классов aria-selected.


Альтернативный подход: группировка через сортировку без заголовков

Иногда группировка реализуется исключительно визуально через отступы и порядок:

awesomplete.sort = (a, b) => {
  return a.group.localeCompare(b.group) || a.label.localeCompare(b.label);
};

И отображение:

awesomplete.item = function (text) {
  const li = document.createElement("li");

  li.textContent = text.label;
  li.classList.add("group-" + text.group.toLowerCase());

  return li;
};

CSS-стили:

.group-languages {
  padding-left: 10px;
}

.group-frameworks {
  padding-left: 20px;
}

Такой подход проще, но не даёт явных разделителей.


Управление порядком групп

Для стабильного UX часто требуется фиксированный порядок категорий:

const groupOrder = {
  "Languages": 1,
  "Frameworks": 2,
  "Tools": 3
};

awesomplete.sort = (a, b) => {
  const ga = groupOrder[a.group] || 999;
  const gb = groupOrder[b.group] || 999;

  if (ga !== gb) return ga - gb;

  return a.label.localeCompare(b.label);
};

Обогащённая модель данных с состояниями

В более сложных интерфейсах элементы могут содержать дополнительные состояния:

{
  label: "React",
  value: "React",
  group: "Frameworks",
  disabled: false,
  description: "UI library"
}

И обработка в item:

awesomplete.item = function (text) {
  const li = document.createElement("li");

  if (text.disabled) {
    li.classList.add("disabled");
    li.setAttribute("aria-disabled", "true");
  }

  li.innerHTML = `
    <span class="label">${text.label}</span>
    <span class="description">${text.description || ""}</span>
  `;

  if (!text.isHeader) {
    li.setAttribute("data-value", text.value);
  }

  return li;
};

Производительность при большом количестве групп

При росте списка до сотен и тысяч элементов группировка начинает влиять на скорость фильтрации. Оптимизация достигается за счёт:

  • предварительной сортировки и кэширования групп
  • минимизации операций в filter
  • избегания пересоздания DOM-структур
  • хранения готового сгруппированного массива
const groupedCache = buildGroupedList(data);
awesomplete.list = groupedCache;

Итоговая архитектурная модель группировки

Группировка в Awesomplete строится как комбинация нескольких слоёв:

  • слой данных (объекты с group)
  • слой подготовки (сортировка и вставка заголовков)
  • слой фильтрации (filter)
  • слой отображения (item)
  • слой выбора (replace)
  • слой поведения интерфейса (CSS + aria-атрибуты)

Такой подход позволяет реализовать многоуровневые списки автодополнения без изменения ядра библиотеки и сохранять контроль над визуальной и логической структурой данных.