Подсветка совпадений

Подсветка совпадений в выпадающих списках автодополнения реализуется через модификацию рендера каждого элемента перед его вставкой в DOM. В Awesomplete базовая логика разделена на три ключевых уровня: фильтрация данных, сортировка результатов и отрисовка элемента списка. Именно последний этап отвечает за визуальное выделение совпадающих подстрок.

Принцип работы отображения элементов

Каждый элемент списка в Awesomplete проходит через функцию item, которая определяет структуру DOM-узла. По умолчанию используется простая генерация элемента <li>, содержащего текстовое значение. Однако этот механизм проектирован как расширяемый, что позволяет внедрять дополнительную логику, включая подсветку совпадений.

Функция item получает два ключевых параметра:

  • значение элемента списка;
  • текущий ввод пользователя.

На основании этих данных формируется HTML-содержимое элемента, где совпадающие фрагменты оборачиваются в специальные теги, чаще всего <mark> или <strong>.

Базовая реализация подсветки через замену подстрок

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

awesomplete.item = function (text, input) {
    const regex = new RegExp(input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");

    const highlighted = text.replace(regex, function (match) {
        return "<mark>" + match + "</mark>";
    });

    const li = document.createElement("li");
    li.innerHTML = highlighted;

    return li;
};

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

Использование <mark> позволяет делегировать стилизацию браузеру по умолчанию, однако внешний вид может быть переопределён через CSS:

mark {
    background-color: #ffe58f;
    padding: 0;
}

Нормализация регистра и нечувствительность к регистру

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

function escapeRegExp(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

Далее применяется единый шаблон сопоставления:

const pattern = new RegExp(escapeRegExp(input), "gi");

Такой подход гарантирует стабильную работу при вводе латиницы, кириллицы и смешанных наборов символов.

Подсветка с сохранением оригинальной структуры строки

Прямая вставка HTML через innerHTML повышает риск повреждения структуры данных при наличии пользовательских символов. Более устойчивый подход заключается в разбиении строки на сегменты и создании DOM-узлов вручную.

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

    const regex = new RegExp(escapeRegExp(input), "gi");

    let lastIndex = 0;
    let match;

    while ((match = regex.exec(text)) !== null) {
        if (match.index > lastIndex) {
            li.appendChild(
                document.createTextNode(text.slice(lastIndex, match.index))
            );
        }

        const mark = document.createElement("mark");
        mark.textContent = match[0];
        li.appendChild(mark);

        lastIndex = regex.lastIndex;
    }

    if (lastIndex < text.length) {
        li.appendChild(
            document.createTextNode(text.slice(lastIndex))
        );
    }

    return li;
};

Такой метод исключает необходимость работы с innerHTML и снижает риск XSS при обработке внешних данных.

Интеграция подсветки с пользовательским фильтром

Awesomplete позволяет переопределять логику фильтрации через свойство filter. В связке с подсветкой это даёт возможность синхронизировать поведение поиска и визуального выделения.

Awesomplete.FILTER_CONTAINS = function (text, input) {
    return text.toLowerCase().includes(input.toLowerCase());
};

awesomplete.filter = Awesomplete.FILTER_CONTAINS;

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

Подсветка нескольких совпадений в одной строке

При наличии нескольких вхождений строки поиска в одном элементе используется глобальный поиск (g-флаг). Это приводит к последовательной обработке всех совпадений.

const regex = new RegExp(escapeRegExp(input), "gi");

Каждое совпадение обрабатывается независимо, что позволяет корректно выделять все участки строки:

  • начало строки до совпадения;
  • само совпадение;
  • промежуток между совпадениями;
  • завершающий сегмент строки.

Работа с многословным вводом

При вводе нескольких слов (например, “new yor”) стандартная регулярная логика работает только с последовательным совпадением подстроки. Для более гибкой подсветки используется разбиение ввода на токены.

const tokens = input.trim().split(/\s+/);
const regex = new RegExp(tokens.map(escapeRegExp).join("|"), "gi");

Такой подход приводит к подсветке каждого совпадающего слова независимо, даже если они встречаются в разном порядке внутри строки.

Контроль HTML-разметки и безопасность

При использовании innerHTML критическим становится контроль входных данных. Любые значения, поступающие извне, должны рассматриваться как потенциально небезопасные. Поэтому применяется экранирование:

function escapeHtml(str) {
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

В сочетании с подсветкой это позволяет комбинировать безопасный вывод и HTML-разметку:

const safeText = escapeHtml(text);

После чего подсветка применяется уже к безопасной строке.

Подсветка с учётом диакритики и Unicode-нормализации

При работе с интернациональными данными возникает необходимость учитывать различные формы записи символов. Например, символы с диакритическими знаками могут иметь несколько представлений в Unicode.

Используется нормализация строк:

const normalizedText = text.normalize("NFD");
const normalizedInput = input.normalize("NFD");

После этого сравнение становится более стабильным, особенно при обработке европейских языков.

Комбинирование подсветки и кастомного шаблона элементов

Awesomplete допускает полную замену структуры элемента через переопределение item. Это позволяет включать не только подсветку, но и дополнительные визуальные элементы.

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

    const container = document.createElement("div");
    container.className = "aw-item";

    const regex = new RegExp(escapeRegExp(input), "gi");

    const highlighted = text.replace(regex, m => `<mark>${m}</mark>`);

    container.innerHTML = highlighted;

    li.appendChild(container);

    return li;
};

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

Стилизация подсветки через CSS-модификаторы

Визуальное оформление подсветки обычно отделяется от логики. Использование классов вместо тега <mark> позволяет гибко управлять стилями:

return "<span class='aw-highlight'>" + match + "</span>";

CSS-правило:

.aw-highlight {
    background: #fff3a0;
    font-weight: 600;
    border-radius: 2px;
}

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

Оптимизация подсветки при больших списках

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

let currentRegex = null;

function updateRegex(input) {
    currentRegex = new RegExp(escapeRegExp(input), "gi");
}

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

Поведение при пустом вводе

При отсутствии текста ввода подсветка отключается, а элементы отображаются в исходном виде. Это достигается через условную проверку:

if (!input) {
    li.textContent = text;
    return li;
}

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