Множественный выбор значений

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

Наиболее распространённые сценарии:

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

Механизм множественного выбора в Awesomplete строится вокруг двух ключевых элементов:

  1. функции разделения введённой строки;
  2. переопределения вставки выбранного элемента.

Базовая архитектура множественного выбора

Стандартный autocomplete работает с единственным значением:

<input id="skills">
new Awesomplete(document.querySelector("#skills"), {
    list: ["JavaScript", "Python", "Go", "Rust"]
});

После выбора:

JavaScript

Для множественного выбора поле должно хранить набор значений:

JavaScript, Python, Rust

В этом случае библиотека должна понимать:

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

Использование разделителей

Наиболее популярный подход — разделение элементов запятыми.

Пример строки:

JavaScript, Python, Rust

Каждое значение считается отдельным элементом списка.


Извлечение текущего фрагмента

Awesomplete анализирует всё содержимое input. Для множественного выбора необходимо выделять только последний вводимый фрагмент.

Обычно используется функция split():

function split(input) {
    return input.split(/\s*,\s*/);
}

Разбор регулярного выражения:

/\s*,\s*/

Компоненты:

Конструкция Значение
\s* любое количество пробелов
, разделитель
\s* пробелы после запятой

Строка:

JavaScript,    Python ,Rust

преобразуется в:

[
    "JavaScript",
    "Python",
    "Rust"
]

Получение последнего элемента

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

function extractLast(input) {
    return split(input).pop();
}

Метод pop() возвращает последний элемент массива.

Пример:

extractLast("JavaScript, Pyt");

Результат:

"Pyt"

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


Полноценная реализация множественного выбора

<input id="languages">
const input = document.querySelector("#languages");

new Awesomplete(input, {
    list: [
        "JavaScript",
        "TypeScript",
        "Python",
        "Go",
        "Rust",
        "C++",
        "Java"
    ],

    filter: function(text, input) {
        return Awesomplete.FILTER_CONTAINS(
            text,
            extractLast(input)
        );
    },

    item: function(text, input) {
        return Awesomplete.ITEM(
            text,
            extractLast(input)
        );
    },

    replace: function(text) {
        const before = split(this.input.value);

        before.pop();

        before.push(text);

        before.push("");

        this.input.value = before.join(", ");
    }
});

function split(input) {
    return input.split(/\s*,\s*/);
}

function extractLast(input) {
    return split(input).pop();
}

Как работает replace()

Метод replace() — центральный механизм множественного выбора.

Стандартное поведение:

this.input.value = text;

Полностью заменяет содержимое поля.

Для множественного выбора требуется:

  1. сохранить предыдущие элементы;
  2. удалить незавершённый фрагмент;
  3. вставить выбранное значение;
  4. подготовить поле к следующему вводу.

Подробный разбор replace()

Получение массива значений

const before = split(this.input.value);

Строка:

JavaScript, Pyt

превращается в:

["JavaScript", "Pyt"]

Удаление незавершённого фрагмента

before.pop();

Теперь массив:

["JavaScript"]

Добавление выбранного значения

before.push(text);

Результат:

["JavaScript", "Python"]

Подготовка нового пустого элемента

before.push("");

Теперь массив:

["JavaScript", "Python", ""]

Это необходимо для автоматического добавления завершающей запятой.


Объединение строки

this.input.value = before.join(", ");

Результат:

JavaScript, Python,

Пользователь сразу может вводить следующий элемент.


Использование других разделителей

Вместо запятых можно использовать любые символы.

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

function split(input) {
    return input.split(/\s*;\s*/);
}

Результат:

JavaScript; Python; Rust

Разделение через вертикальную черту

function split(input) {
    return input.split(/\s*\|\s*/);
}

Результат:

JavaScript | Python | Rust

Разделение через перенос строки

function split(input) {
    return input.split(/\n+/);
}

Полезно для:

  • списков email;
  • bulk-ввода;
  • textarea autocomplete.

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

Множественный выбор особенно удобен в textarea.

<textarea id="tags"></textarea>
new Awesomplete("#tags", {
    list: [
        "frontend",
        "backend",
        "database",
        "devops",
        "security"
    ]
});

В textarea можно:

  • переносить строки;
  • хранить длинные списки;
  • создавать редакторы тегов;
  • реализовывать bulk autocomplete.

Автоматическое исключение дубликатов

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

Пример:

JavaScript, JavaScript

Для предотвращения дубликатов:

replace: function(text) {
    let values = split(this.input.value);

    values.pop();

    if (!values.includes(text)) {
        values.push(text);
    }

    values.push("");

    this.input.value = values.join(", ");
}

Нормализация регистра

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

Пример:

javascript
JavaScript
JAVASCRIPT

Проверка:

const exists = values.some(item => {
    return item.toLowerCase() === text.toLowerCase();
});

Ограничение количества элементов

Для ограничения числа выбранных значений:

const LIMIT = 5;
replace: function(text) {
    let values = split(this.input.value);

    values.pop();

    if (values.length >= LIMIT) {
        return;
    }

    values.push(text);
    values.push("");

    this.input.value = values.join(", ");
}

Работа с объектами

Множественный выбор может использовать объекты вместо строк.

[
    {
        label: "JavaScript",
        value: 1
    },
    {
        label: "Python",
        value: 2
    }
]

Пример вставки идентификаторов:

replace: function(item) {
    let values = split(this.input.value);

    values.pop();

    values.push(item.value);

    values.push("");

    this.input.value = values.join(", ");
}

Результат:

1, 2, 5

Отображение label при сохранении value

Часто требуется:

  • отображать человеку понятный текст;
  • сохранять идентификаторы.

Схема:

<input id="visible">
<input type="hidden" id="ids">

Видимое поле:

JavaScript, Python

Скрытое поле:

1,2

Разделение отображения и хранения

Пример:

const selectedIds = [];

new Awesomplete(input, {
    list: skills,

    replace: function(item) {
        const labels = split(this.input.value);

        labels.pop();

        labels.push(item.label);
        labels.push("");

        this.input.value = labels.join(", ");

        if (!selectedIds.includes(item.value)) {
            selectedIds.push(item.value);
        }

        hidden.value = selectedIds.join(",");
    }
});

Удаление элементов вручную

Пользователь может удалять значения через Backspace.

Строка:

JavaScript, Python, Rust

После удаления:

JavaScript, Python

Awesomplete автоматически продолжит работу, если функция split() корректно обрабатывает строку.


Автоматическое открытие списка после разделителя

После ввода запятой можно автоматически показывать список:

input.addEventListener("keyup", function(event) {
    if (event.key === ",") {
        awesomplete.evaluate();
    }
});

Это улучшает UX при быстром вводе тегов.


Поддержка Tab

Autocomplete часто комбинируется с клавишей Tab.

input.addEventListener("keydown", function(event) {
    if (event.key === "Tab" && awesomplete.opened) {
        event.preventDefault();
        awesomplete.select();
    }
});

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

  • быстрый ввод;
  • навигация без мыши;
  • ускоренная работа с тегами.

Асинхронный множественный autocomplete

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

Пример:

input.addEventListener("input", async function() {

    const query = extractLast(input.value);

    if (query.length < 2) {
        return;
    }

    const response = await fetch(
        "/search?q=" + encodeURIComponent(query)
    );

    const data = await response.json();

    awesomplete.list = data;
});

Минимальная длина поиска

Для снижения нагрузки:

minChars: 2

Полный пример:

new Awesomplete(input, {
    minChars: 2,
    list: []
});

Использование HTML-тегов как chips-интерфейса

Иногда текстовое поле преобразуют в визуальные теги:

[JavaScript] [Python] [Rust]

Схема работы:

  • Awesomplete отвечает за поиск;
  • выбранные элементы рендерятся как отдельные DOM-узлы;
  • input остаётся пустым.

Такой подход используется в:

  • системах тегов;
  • email-клиентах;
  • CRM;
  • административных панелях.

Пример chips-интерфейса

<div id="tags"></div>
<input id="tag-input">
const tags = [];

function renderTags() {

    container.innerHTML = "";

    tags.forEach(tag => {

        const el = document.createElement("span");

        el.textContent = tag;

        container.appendChild(el);
    });
}

После выбора элемента:

replace: function(text) {

    if (!tags.includes(text)) {
        tags.push(text);
    }

    renderTags();

    this.input.value = "";
}

Синхронизация с сервером

Множественный выбор обычно отправляется в одном из форматов:

CSV

JavaScript,Python,Rust

JSON

[
    "JavaScript",
    "Python",
    "Rust"
]

Массив form-data

skills[]=JavaScript
skills[]=Python
skills[]=Rust

Проблемы экранирования разделителей

Если значения сами содержат запятые:

New York, USA

обычное разделение становится некорректным.

Решения:

  • использовать другой разделитель;
  • хранить ID вместо текста;
  • применять JSON;
  • использовать chips-интерфейс.

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

Если список содержит тысячи элементов:

list: hugeArray

могут появляться:

  • задержки фильтрации;
  • лаги интерфейса;
  • медленная перерисовка.

Оптимизации:

  • асинхронный поиск;
  • серверная фильтрация;
  • debounce;
  • ограничение числа результатов.

Debounce для множественного поиска

let timer;

input.addEventListener("input", function() {

    clearTimeout(timer);

    timer = setTimeout(async () => {

        const query = extractLast(input.value);

        const response = await fetch(
            "/api/search?q=" + query
        );

        awesomplete.list = await response.json();

    }, 300);
});

Проверка пустых элементов

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

[
    "JavaScript",
    "",
    "Python"
]

Фильтрация:

values = values.filter(Boolean);

Тримминг значений

Для удаления лишних пробелов:

values = values.map(item => item.trim());

Комбинирование trim и filter

function split(input) {

    return input
        .split(",")
        .map(item => item.trim())
        .filter(Boolean);
}

Это один из наиболее надёжных вариантов обработки множественного ввода.


Поддержка Unicode

Awesomplete корректно работает с Unicode-строками:

Москва, Алматы, 東京

Регулярные выражения также поддерживают национальные символы.


Навигация по множественным значениям

В сложных интерфейсах можно:

  • редактировать отдельные элементы;
  • удалять chips;
  • перемещаться стрелками;
  • менять порядок тегов;
  • реализовывать drag-and-drop.

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