Подсветка результатов

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

Во время ввода текста библиотека анализирует содержимое опций и автоматически выделяет совпадающие фрагменты. Обычно для этого применяется HTML-разметка с тегами <mark>, <span> или специализированными CSS-классами.

Подсветка может работать:

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

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

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

  1. Найти совпадающий фрагмент строки.
  2. Разбить текст на части.
  3. Обернуть найденный участок в HTML.
  4. Отобразить результат в выпадающем списке.

Пример:

<select id="countries">
  <option>Germany</option>
  <option>Georgia</option>
  <option>Greece</option>
</select>
new SlimSelect({
  select: '#countries'
})

При вводе:

geo

Выпадающий список может визуально преобразоваться в:

<div class="option">
  <mark>Geo</mark>rgia
</div>

Стандартная подсветка совпадений

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

Типичный механизм:

new SlimSelect({
  select: '#countries',
  searchHighlight: true
})

После активации:

  • совпадающие символы выделяются;
  • поиск становится визуально информативнее;
  • пользователь быстрее ориентируется в большом списке.

Настройка визуального оформления подсветки

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

Пример:

.ss-highlight {
  background-color: yellow;
  color: black;
  font-weight: bold;
}

Если Slim Select использует класс .ss-highlight, все совпадения автоматически получат указанный стиль.

Другой пример:

mark {
  background: #ffe08a;
  padding: 2px 4px;
  border-radius: 3px;
}

Кастомная подсветка через renderer

Наиболее гибкий способ настройки — переопределение рендеринга элементов.

Пример:

const slim = new SlimSelect({
  select: '#countries',

  events: {
    search: (search, currentData) => {
      return currentData
    }
  }
})

Далее создаётся собственная функция обработки текста.


Реализация пользовательской функции highlight

Базовый алгоритм подсветки:

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

  const regex = new RegExp(`(${query})`, 'gi')

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

Принцип работы:

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

Использование highlight в render-функции

Пример интеграции:

const searchInput = 'geo'

const result = highlight('Georgia', searchInput)

console.log(result)

Результат:

<mark>Geo</mark>rgia

Подсветка при динамической генерации options

При использовании массива данных:

new SlimSelect({
  select: '#users',
  data: [
    { text: 'Alexander' },
    { text: 'Alexey' },
    { text: 'Alexa' }
  ]
})

Подсветка может формироваться во время генерации элементов:

function renderOption(option, search) {
  return `
    <div class="option">
      ${highlight(option.text, search)}
    </div>
  `
}

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

Функция может выделять все найденные участки строки.

Пример:

highlight('Banana', 'an')

Результат:

B<mark>an</mark><mark>an</mark>a

Благодаря глобальному флагу g регулярное выражение находит все совпадения.


Игнорирование регистра

Флаг i позволяет выполнять поиск без учёта регистра.

Пример:

const regex = new RegExp(`(${query})`, 'gi')

Теперь:

Geo
geo
GEO
gEo

будут считаться одинаковыми запросами.


Экранирование специальных символов

Пользователь может вводить символы регулярных выражений:

+
*
?
[
]

Без экранирования поиск вызовет ошибку.

Надёжная реализация:

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

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

function highlight(text, query) {
  const escaped = escapeRegExp(query)

  const regex = new RegExp(`(${escaped})`, 'gi')

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

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

Иногда поиск должен учитывать несколько слов одновременно.

Пример запроса:

new york

Разделение запроса:

const parts = query.split(' ')

Формирование выражения:

const regex = new RegExp(`(${parts.join('|')})`, 'gi')

Теперь подсвечиваются оба слова:

<mark>New</mark> <mark>York</mark>

Подсветка только начала строки

Иногда необходимо выделять только совпадения в начале текста.

Пример:

const regex = new RegExp(`^(${query})`, 'gi')

Теперь:

Germany

при поиске:

ger

подсветится, а:

Bulgaria

нет.


Подсветка внутри групп

Slim Select поддерживает группы options.

Пример:

<select id="products">
  <optgroup label="Phones">
    <option>iPhone</option>
    <option>Samsung</option>
  </optgroup>

  <optgroup label="Laptops">
    <option>MacBook</option>
  </optgroup>
</select>

Подсветка может применяться:

  • к названию группы;
  • к содержимому option;
  • одновременно к обоим элементам.

Пример:

highlight('Phones', 'pho')

Результат:

<mark>Pho</mark>nes

Подсветка при AJAX-поиске

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

Пример:

fetch('/api/users?q=alex')
  .then(response => response.json())
  .then(data => {
    const prepared = data.map(item => ({
      text: highlight(item.name, 'alex'),
      value: item.id
    }))
  })

Проблема XSS при подсветке

Нельзя напрямую вставлять пользовательский ввод в HTML.

Опасный пример:

return text.replace(regex, `<mark>${query}</mark>`)

Если пользователь введёт HTML:

<script>alert(1)</script>

возможна XSS-атака.

Безопаснее использовать экранирование HTML.


Экранирование HTML

Пример функции:

function escapeHtml(text) {
  const div = document.createElement('div')

  div.textContent = text

  return div.innerHTML
}

Безопасная реализация:

function highlight(text, query) {
  const escapedText = escapeHtml(text)
  const escapedQuery = escapeRegExp(query)

  const regex = new RegExp(`(${escapedQuery})`, 'gi')

  return escapedText.replace(regex, '<mark>$1</mark>')
}

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

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

Основные причины:

  • большое количество DOM-операций;
  • частое создание регулярных выражений;
  • постоянная перерисовка списка;
  • сложные HTML-шаблоны.

Оптимизация подсветки

Кэширование регулярного выражения

Плохой вариант:

items.forEach(item => {
  const regex = new RegExp(query, 'gi')
})

Хороший вариант:

const regex = new RegExp(query, 'gi')

items.forEach(item => {
})

Ограничение количества результатов

Пример:

const filtered = data.slice(0, 50)

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


Debounce поиска

Пример:

function debounce(callback, delay) {
  let timer

  return (...args) => {
    clearTimeout(timer)

    timer = setTimeout(() => {
      callback(...args)
    }, delay)
  }
}

Подсветка выбранных элементов

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

Пример:

<div class="selected">
  <mark>Alex</mark>ander
</div>

Такой подход полезен:

  • в автокомплите;
  • в поиске пользователей;
  • в теговых интерфейсах;
  • в больших системах фильтрации.

Использование span вместо mark

Тег <mark> подходит не всегда.

Альтернативный вариант:

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

CSS:

.highlight {
  background: #ffd54f;
  font-weight: 600;
}

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

Можно добавить плавное появление совпадений.

Пример:

.highlight {
  background: #ffeb3b;
  transition: background 0.3s ease;
}

Или:

.highlight {
  animation: flash 0.4s ease;
}

@keyframes flash {
  fr om {
    background: orange;
  }

  to {
    background: yellow;
  }
}

Подсветка с поддержкой Unicode

При работе с кириллицей, японскими символами и другими языками важно использовать Unicode-совместимые выражения.

Пример:

const regex = new RegExp(`(${escapedQuery})`, 'giu')

Флаг u обеспечивает корректную работу Unicode.


Подсветка транслитерации

Иногда поиск должен находить:

moskva

для:

Москва

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

Пример:

function transliterate(text) {
  return text
    .replace(/ш/g, 'sh')
    .replace(/ч/g, 'ch')
}

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


Подсветка при нечётком поиске

Для fuzzy search можно использовать библиотеки:

  • Fuse.js
  • FlexSearch
  • MiniSearch

Пример:

const fuse = new Fuse(data, {
  keys: ['text']
})

Результаты поиска:

const results = fuse.search('alx')

Затем совпадения подсвечиваются вручную.


Интеграция Fuse.js и Slim Select

Пример:

const fuse = new Fuse(users, {
  keys: ['text'],
  includeMatches: true
})

Поиск:

const results = fuse.search('alex')

Получение совпадений:

results.forEach(result => {
  console.log(result.matches)
})

Массив matches содержит индексы совпавших символов, которые можно использовать для точной подсветки.


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

Пример:

function highlightByIndexes(text, indexes) {
  let result = ''

  for (let i = 0; i < text.length; i++) {
    const matched = indexes.some(([start, end]) =>
      i >= start && i <= end
    )

    if (matched) {
      result += `<mark>${text[i]}</mark>`
    } else {
      result += text[i]
    }
  }

  return result
}

Поддержка HTML внутри option

Если option уже содержит HTML:

{
  text: '<b>Admin</b>'
}

необходимо аккуратно обрабатывать разметку, иначе подсветка может сломать DOM.

Лучший подход:

  • хранить отдельно plain text;
  • генерировать HTML после поиска;
  • избегать replace поверх готовой разметки.

Типичные ошибки

Повторная подсветка

Ошибка:

<mark><mark>Alex</mark></mark>

Причина — повторный запуск highlight по уже обработанному HTML.


Потеря обработчиков событий

При полной перерисовке списка могут исчезать события:

element.innerHTML = html

Решение:

  • делегирование событий;
  • частичное обновление DOM;
  • виртуализация списка.

Некорректная работа с пробелами

Запрос:

new york

может не находить:

New     York

Решение:

text.replace(/\s+/g, ' ')

Архитектура расширяемой подсветки

Крупные приложения часто выносят подсветку в отдельный модуль.

Пример структуры:

search/
├── highlight.js
├── escape.js
├── tokenizer.js
├── renderer.js
└── styles.css

Преимущества:

  • повторное использование;
  • удобство тестирования;
  • независимость от Slim Select;
  • лёгкая замена алгоритма поиска.

Пример полноценной реализации

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

function escapeHtml(text) {
  const div = document.createElement('div')

  div.textContent = text

  return div.innerHTML
}

function highlight(text, query) {
  if (!query) {
    return escapeHtml(text)
  }

  const escapedText = escapeHtml(text)

  const escapedQuery = escapeRegExp(query)

  const regex = new RegExp(`(${escapedQuery})`, 'giu')

  return escapedText.replace(
    regex,
    '<span class="highlight">$1</span>'
  )
}

CSS:

.highlight {
  background: #fff176;
  color: #000;
  border-radius: 2px;
  padding: 0 2px;
}

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

const result = highlight(
  'Alexander Pushkin',
  'alex'
)

console.log(result)

Результат:

<span class="highlight">Alex</span>ander Pushkin