Выделение совпадений

В основе выделения совпадений в Tom Select лежит механизм кастомного рендеринга опций. Библиотека не ограничивается простым отображением текста: каждая опция проходит через функцию render.option, а выбранные элементы — через render.item. Именно на этом уровне формируется визуальное представление совпавших фрагментов поискового запроса.

Стандартное поведение предполагает, что текст опции сравнивается с введённым запросом, после чего совпавшие части оборачиваются в HTML-разметку. Обычно используется <span> или <mark>, что позволяет визуально отделить релевантные фрагменты от остального текста.


Механизм поиска и определение совпадений

Поиск в Tom Select строится вокруг функции ранжирования (score) и нормализации строк. На этапе ввода запроса выполняются следующие шаги:

  • нормализация строки (приведение к нижнему регистру);
  • удаление или учёт диакритических символов;
  • разбиение строки на токены;
  • вычисление релевантности совпадения.

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

Подсветка строится на основе тех же вычислений: библиотека определяет границы совпавших сегментов и передаёт их в рендер-функции.


Встроенная подсветка и её особенности

В стандартной конфигурации Tom Select подсветка активируется автоматически. Внутренний алгоритм формирует HTML-фрагменты с выделением совпадающих частей запроса.

Пример поведения:

  • ввод: car
  • опция: Caribbean Carnival
  • результат: <span class="highlight">Car</span>ibbean <span class="highlight">Car</span>nival

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

Ключевая особенность заключается в том, что библиотека не просто заменяет подстроки, а использует результат token-based поиска, что делает подсветку более точной при сложных запросах.


Кастомизация через render.option и render.item

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

new TomSelect("#select", {
  render: {
    option: function(data, escape) {
      return `
        <div>
          <span class="title">${escape(data.text)}</span>
        </div>
      `;
    },
    item: function(data, escape) {
      return `<div>${escape(data.text)}</div>`;
    }
  }
});

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


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

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

function highlight(text, query) {
  if (!query) return text;

  const pattern = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'i');

  return text.replace(pattern, '<span class="highlight">$1</span>');
}

Использование в render.option:

new TomSelect("#select", {
  render: {
    option: function(data, escape) {
      return `
        <div>
          ${highlight(escape(data.text), this.lastQuery)}
        </div>
      `;
    }
  }
});

Здесь важно учитывать контекст lastQuery, который содержит текущее значение поиска.


Обработка нескольких токенов запроса

При вводе составных запросов необходимо учитывать множественные совпадения. В таких случаях используется разбиение строки на слова:

function highlightMultiple(text, query) {
  if (!query) return text;

  const words = query
    .trim()
    .split(/\s+/)
    .map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));

  const pattern = new RegExp(`(${words.join('|')})`, 'gi');

  return text.replace(pattern, '<mark>$1</mark>');
}

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


Диакритика и нормализация строк

В языках с диакритическими символами стандартное сравнение строк становится недостаточным. Tom Select использует нормализацию Unicode, но при кастомной реализации подсветки требуется учитывать этот аспект вручную.

function normalize(str) {
  return str
    .toLowerCase()
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "");
}

После нормализации поиск совпадений становится устойчивым к различным вариантам ввода, например:

  • cafe
  • café

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


Экранирование HTML и безопасность

Любая подсветка совпадений связана с генерацией HTML. Это требует обязательного экранирования входных данных.

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

Использование:

return highlight(escapeHtml(data.text), escapeHtml(query));

Без экранирования возможно внедрение нежелательного HTML-кода через пользовательский ввод или данные источника.


Стилизация выделенных совпадений

Визуальное оформление выделения обычно задаётся через CSS:

.highlight {
  background: rgba(255, 230, 0, 0.4);
  font-weight: 600;
  border-radius: 2px;
  padding: 0 2px;
}

При использовании <mark> можно полагаться на встроенные стили браузера, однако кастомный класс даёт полный контроль над внешним видом.


Взаимодействие подсветки с асинхронным поиском

При загрузке данных через load подсветка должна учитывать задержки и обновление состояния:

new TomSelect("#select", {
  load: function(query, callback) {
    fetch(`/api?q=${encodeURIComponent(query)}`)
      .then(res => res.json())
      .then(data => callback(data));
  },
  render: {
    option: function(data, escape) {
      return `<div>${escape(data.name)}</div>`;
    }
  }
});

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


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

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

  • регулярные выражения в реальном времени;
  • многократное экранирование строк;
  • перерасчёт совпадений при каждом событии input.

Оптимизация достигается через:

  • кэширование нормализованных строк;
  • предварительную индексацию данных;
  • ограничение частоты обновлений (debounce);
  • минимизацию DOM-операций в render.

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