Подсветка найденных элементов в Choices.js реализуется через механизм кастомной обработки результатов поиска и управления разметкой опций. По умолчанию библиотека предоставляет базовую фильтрацию списка, однако визуальное выделение совпадающих фрагментов требует использования специальных хук-функций и шаблонов рендера.
Choices.js использует встроенную систему поиска, основанную на
сравнении строки запроса с текстом опций. При включённом поиске
(searchEnabled) библиотека формирует отфильтрованный список
элементов, но не изменяет их визуальное представление. Это означает, что
совпадения по умолчанию не подсвечиваются.
Ключевой момент заключается в том, что каждый элемент списка проходит через рендер-функцию, которая отвечает за формирование DOM-структуры опции. Именно на этом уровне внедряется логика подсветки.
Основной способ реализации подсветки — использование параметра
callbackOnCreateTemplates или кастомизации шаблонов через
item и choice.
Структура переопределения:
const choices = new Choices('#select', {
searchEnabled: true,
callbackOnCreateTemplates: function (template) {
return {
choice: (classNames, data) => {
return template(`
<div class="${classNames.item} ${classNames.itemChoice}"
data-choice
data-id="${data.id}"
data-value="${data.value}"
data-select-text="${this.config.itemSelectText}">
${data.label}
</div>
`);
}
};
}
});
В этом варианте отображение остаётся неизменным, но создаётся точка
расширения для внедрения логики подсветки внутри
data.label.
Для реализации подсветки необходимо получить текущий поисковый ввод. Choices.js не всегда предоставляет его напрямую в шаблонах, поэтому используется доступ к внутреннему состоянию экземпляра:
const query = choices.input?.value || '';
Этот подход позволяет извлечь строку поиска из текстового поля, связанного с экземпляром компонента.
Подсветка совпадений строится на замене найденных фрагментов строки на HTML-разметку с выделением. Наиболее распространённый подход — использование регулярных выражений.
function highlight(text, query) {
if (!query) return text;
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
CSS-оформление:
.highlight {
background-color: #ffe58f;
font-weight: 600;
}
После определения функции подсветки она внедряется в рендер элемента:
const choices = new Choices('#select', {
searchEnabled: true,
callbackOnCreateTemplates: function (template) {
return {
choice: (classNames, data) => {
const query = this.input?.value || '';
const label = highlight(data.label, query);
return template(`
<div class="${classNames.item} ${classNames.itemChoice}"
data-choice
data-id="${data.id}"
data-value="${data.value}">
${label}
</div>
`);
}
};
}
});
Таким образом каждый элемент списка динамически пересобирается с учётом текущего поискового запроса.
Choices.js пересоздаёт список опций при каждом изменении поискового
поля. Это означает, что подсветка автоматически обновляется без
необходимости вручную слушать события input.
Однако в некоторых конфигурациях может потребоваться принудительное обновление:
choices.input.addEventListener('input', () => {
choices.clearChoices();
choices.setChoices([...], 'value', 'label', true);
});
Такой подход используется редко и обычно применяется при внешнем управлении данными.
При внедрении подсветки важно учитывать, что исходные данные могут содержать HTML-символы. Без экранирования возможны искажения разметки или внедрение некорректного HTML.
Функция безопасного экранирования:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Комбинированный вариант:
const safeLabel = highlight(escapeHtml(data.label), query);
При вводе составных запросов (например, «new york city») стандартная регулярка выделяет только полное совпадение. Для улучшения UX применяется разбиение строки на токены:
function highlightMultiple(text, query) {
if (!query) return text;
const words = query.trim().split(/\s+/).filter(Boolean);
let result = text;
words.forEach(word => {
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
result = result.replace(regex, '<span class="highlight">$1</span>');
});
return result;
}
Такой подход позволяет подсвечивать каждое совпадение независимо от порядка слов.
При использовании кастомных render-шаблонов важно
учитывать следующие особенности:
i в
регулярном выражении.В случаях больших наборов данных предпочтительно ограничивать количество совпадений или применять дебаунс ввода.
Для повышения производительности используется кэширование регулярных выражений:
const regexCache = new Map();
function getRegex(query) {
if (regexCache.has(query)) return regexCache.get(query);
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
regexCache.set(query, regex);
return regex;
}
Это уменьшает накладные расходы при частом вводе символов.
Подсветка часто используется совместно с кастомными шаблонами отображения:
callbackOnCreateTemplates: function (template) {
return {
choice: (classNames, data) => {
const query = this.input?.value || '';
const label = highlightMultiple(escapeHtml(data.label), query);
return template(`
<div class="${classNames.item} ${classNames.itemChoice}">
<div class="choice-label">${label}</div>
</div>
`);
}
};
}
Такой подход позволяет отделить логику данных от визуального слоя, сохраняя управляемость интерфейса.
При очистке строки поиска ('') функция подсветки должна
возвращать исходный текст без изменений. Это критично для предотвращения
накопления HTML-разметки:
<span> должны исчезать;data.label, а не из
DOM.Подсветка в Choices.js фактически является результатом сочетания кастомного рендеринга и обработки строки поиска, где ключевую роль играет контроль над шаблонами и преобразованием текста перед вставкой в DOM.