Поиск в библиотеке Slim Select
построен вокруг внутреннего механизма фильтрации массива опций. По
умолчанию библиотека выполняет простое текстовое сравнение введённой
строки с содержимым элементов <option>, однако
система допускает глубокую кастомизацию поведения.
Базовая активация поиска:
<select id="users">
<option value="1">Александр</option>
<option value="2">Екатерина</option>
<option value="3">Максим</option>
</select>
new SlimSelect({
select: '#users',
settings: {
search: true
}
})
При включённом поиске Slim Select:
input;Иногда требуется полностью убрать встроенную фильтрацию:
new SlimSelect({
select: '#users',
settings: {
search: false
}
})
Такой режим полезен:
По умолчанию Slim Select игнорирует регистр символов. Для строгого
сравнения используется параметр searchText в
пользовательской логике.
Пример кастомного поиска с учётом регистра:
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
return currentData.filter(option => {
return option.text.includes(search)
})
}
}
})
Теперь:
макс не найдёт Максим;Макс вернёт результат.Главный механизм кастомизации — событие search.
Оно получает:
| Аргумент | Описание |
|---|---|
search |
Введённая строка |
currentData |
Текущий массив опций |
Возвращаемое значение должно содержать массив найденных элементов.
Базовый пример:
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
return currentData.filter(option => {
return option.text
.toLowerCase()
.includes(search.toLowerCase())
})
}
}
})
Фактически встроенный поиск можно полностью заменить собственной системой фильтрации.
Каждая опция может содержать дополнительные данные.
new SlimSelect({
select: '#users',
data: [
{
text: 'Александр',
value: '1',
data: {
email: 'alex@example.com',
city: 'Москва'
}
},
{
text: 'Мария',
value: '2',
data: {
email: 'maria@example.com',
city: 'Казань'
}
}
],
events: {
search: (search, currentData) => {
const query = search.toLowerCase()
return currentData.filter(option => {
return (
option.text.toLowerCase().includes(query) ||
option.data.email.toLowerCase().includes(query) ||
option.data.city.toLowerCase().includes(query)
)
})
}
}
})
Теперь поиск работает одновременно:
Стандартный includes() ищет только точные подстроки. Для
более гибкой логики применяется нечёткое сравнение.
Пример поиска по символам:
function fuzzySearch(text, query) {
text = text.toLowerCase()
query = query.toLowerCase()
let index = 0
for (let char of text) {
if (char === query[index]) {
index++
}
if (index === query.length) {
return true
}
}
return false
}
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
return currentData.filter(option => {
return fuzzySearch(option.text, search)
})
}
}
})
Результаты:
| Запрос | Совпадение |
|---|---|
мкс |
Максим |
алс |
Александр |
Подобная схема часто используется:
Поиск можно перевести на RegExp.
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
const regex = new RegExp(search, 'i')
return currentData.filter(option => {
return regex.test(option.text)
})
}
}
})
Поддерживаются сложные шаблоны:
| Запрос | Результат |
|---|---|
^А |
Все имена на А |
ин$ |
Окончание “ин” |
[0-9] |
Элементы с цифрами |
Неправильное регулярное выражение вызывает исключение.
Опасный ввод:
(
Без обработки:
new RegExp('(')
возникает ошибка.
Безопасная реализация:
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
try {
const regex = new RegExp(search, 'i')
return currentData.filter(option => {
return regex.test(option.text)
})
} catch {
return []
}
}
}
})
Пользователи часто вводят:
Перед фильтрацией данные обычно нормализуются.
function normalize(text) {
return text
.trim()
.toLowerCase()
}
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
const query = normalize(search)
return currentData.filter(option => {
return normalize(option.text)
.includes(query)
})
}
}
})
Для международных проектов критически важна поддержка Unicode.
Например:
| Ввод | Должен найти |
|---|---|
resume |
résumé |
cafe |
café |
Решение:
function removeDiacritics(text) {
return text.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
const query = removeDiacritics(search.toLowerCase())
return currentData.filter(option => {
const normalized = removeDiacritics(
option.text.toLowerCase()
)
return normalized.includes(query)
})
}
}
})
Иногда необходимо искать только префиксы.
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
const query = search.toLowerCase()
return currentData.filter(option => {
return option.text
.toLowerCase()
.startsWith(query)
})
}
}
})
Такой алгоритм особенно полезен:
Частая задача — поиск по отдельным токенам.
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
const words = search
.toLowerCase()
.split(' ')
return currentData.filter(option => {
const text = option.text.toLowerCase()
return words.every(word => {
return text.includes(word)
})
})
}
}
})
Запрос:
иван менеджер
найдёт:
Иван Петров — менеджер
Иногда разные поля должны иметь разный приоритет.
new SlimSelect({
select: '#users',
data: [
{
text: 'Иван',
value: '1',
data: {
role: 'Менеджер'
}
}
],
events: {
search: (search, currentData) => {
const query = search.toLowerCase()
return currentData
.map(option => {
let score = 0
if (option.text.toLowerCase().includes(query)) {
score += 10
}
if (option.data.role.toLowerCase().includes(query)) {
score += 5
}
return {
option,
score
}
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.map(item => item.option)
}
}
})
Преимущества подхода:
Slim Select поддерживает возврат Promise.
Это позволяет:
Пример:
new SlimSelect({
select: '#users',
events: {
search: async (search) => {
const response = await fetch(
`/api/users?q=${encodeURIComponent(search)}`
)
const users = await response.json()
return users.map(user => ({
text: user.name,
value: user.id
}))
}
}
})
Без debounce каждый символ вызывает запрос.
Проблемы:
Простейший debounce:
function debounce(callback, delay) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
callback(...args)
}, delay)
}
}
Использование:
const searchUsers = debounce(async (search, resolve) => {
const response = await fetch(`/api/users?q=${search}`)
const data = await response.json()
resolve(data)
}, 300)
new SlimSelect({
select: '#users',
events: {
search: (search) => {
return new Promise(resolve => {
searchUsers(search, resolve)
})
}
}
})
При повторяющихся запросах полезно сохранять результаты.
const cache = {}
new SlimSelect({
select: '#users',
events: {
search: async (search) => {
if (cache[search]) {
return cache[search]
}
const response = await fetch(`/api/users?q=${search}`)
const data = await response.json()
cache[search] = data
return data
}
}
})
Преимущества:
Поиск с одного символа часто создаёт слишком много результатов.
Ограничение:
new SlimSelect({
select: '#users',
events: {
search: (search, currentData) => {
if (search.length < 3) {
return []
}
return currentData.filter(option => {
return option.text
.toLowerCase()
.includes(search.toLowerCase())
})
}
}
})
Настройка текста интерфейса:
new SlimSelect({
select: '#users',
settings: {
searchText: 'Поиск пользователей',
searchPlaceholder: 'Введите имя',
searchingText: 'Идёт поиск...',
searchFocus: true
}
})
Описание параметров:
| Параметр | Назначение |
|---|---|
searchText |
Текст поискового блока |
searchPlaceholder |
Placeholder поля |
searchingText |
Сообщение загрузки |
searchFocus |
Автофокус |
Иногда поиск должен выполняться только сервером.
new SlimSelect({
select: '#users',
events: {
search: async (search) => {
const response = await fetch(`/search?q=${search}`)
const data = await response.json()
return data
}
}
})
В этом режиме:
Для профессионального нечёткого поиска часто подключается библиотека Fuse.js.
Установка:
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
Интеграция:
const data = [
{ text: 'Александр', value: '1' },
{ text: 'Максим', value: '2' },
{ text: 'Екатерина', value: '3' }
]
const fuse = new Fuse(data, {
keys: ['text'],
threshold: 0.3
})
new SlimSelect({
select: '#users',
events: {
search: (search) => {
return fuse.search(search)
.map(result => result.item)
}
}
})
Fuse.js предоставляет: