Механизм тегов и токенов в библиотеке Awesomplete предназначен для работы с множественным вводом значений внутри одного поля. Вместо выбора единственного элемента пользователь может формировать последовательность независимых значений, разделённых специальными символами: запятыми, точками с запятой, пробелами или любыми другими разделителями.
Подобный подход широко используется в интерфейсах:
Awesomplete не содержит встроенной полноценной системы тегов, однако
предоставляет гибкие механизмы через свойства replace,
filter, item, data, а также
пользовательскую обработку токенов.
Стандартный режим работы Awesomplete предполагает:
<input id="tags">
new Awesomplete(document.querySelector("#tags"), {
list: ["JavaScript", "Python", "Rust"]
});
После выбора элемента содержимое поля полностью заменяется выбранным значением.
Для работы с токенами необходимо изменить поведение вставки значения. Вместо полной замены поля библиотека должна:
Наиболее распространённый формат:
JavaScript, Python, Rust
Каждый элемент представляет отдельный токен.
Разделителем может выступать:
Например:
frontend | backend | devops
или:
css;html;javascript
Основой всей системы становится разбор введённой строки.
Простейший вариант:
function splitTags(text) {
return text.split(",");
}
Однако такой подход создаёт проблемы:
Более корректная реализация:
function splitTags(text) {
return text
.split(",")
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
}
Теперь строка:
JavaScript, Python, , Rust
преобразуется в:
["JavaScript", "Python", "Rust"]
Awesomplete должен понимать, какой именно фрагмент вводится в данный момент.
Пример:
JavaScript, Py
Текущий токен:
Py
Получение последнего элемента:
function extractCurrentToken(text) {
let tokens = text.split(",");
return tokens[tokens.length - 1].trim();
}
Свойство filter определяет, какие элементы списка должны
отображаться.
По умолчанию Awesomplete сравнивает всё содержимое поля ввода, что для тегов не подходит.
Необходимо фильтровать только активный токен.
new Awesomplete(input, {
list: languages,
filter: function(text, inputValue) {
let current = extractCurrentToken(inputValue);
return Awesomplete.FILTER_CONTAINS(text, current);
}
});
Теперь при вводе:
JavaScript, Py
поиск выполняется только по строке:
Py
Ключевым элементом системы становится переопределение метода
replace.
Стандартное поведение:
input.value = selectedValue;
Для тегов необходимо заменить только последний токен.
new Awesomplete(input, {
list: languages,
replace: function(selected) {
let before = this.input.value.split(",");
before.pop();
before.push(selected);
this.input.value = before.join(", ") + ", ";
}
});
Если поле содержит:
JavaScript, Py
то:
split(",")
даёт:
["JavaScript", " Py"]
Далее:
before.pop();
удаляет последний незавершённый токен.
После:
before.push(selected);
вставляется выбранное значение.
Результат:
JavaScript, Python,
Фрагмент:
+ ", "
добавляет:
Без этого пользователь должен самостоятельно вводить разделитель после каждого выбора.
replace: function(selected) {
let parts = this.input.value.split(";");
parts.pop();
parts.push(selected);
this.input.value = parts.join("; ") + "; ";
}
replace: function(selected) {
let parts = this.input.value.split("|");
parts.pop();
parts.push(selected);
this.input.value = parts.join(" | ") + " | ";
}
Иногда токены разделяются пробелами:
tag1 tag2 tag3
Получение активного токена:
function currentToken(value) {
return value.split(/\s+/).pop();
}
Регулярные выражения позволяют поддерживать несколько разделителей одновременно.
Пример:
function splitTokens(value) {
return value.split(/[;,|]/);
}
Поддерживаются:
;,|function tokenize(value) {
return value
.split(/[;,]/)
.map(item => item.trim())
.filter(Boolean);
}
При выборе тегов часто требуется запрет повторений.
function hasToken(value, token) {
return tokenize(value).includes(token);
}
replace: function(selected) {
let tokens = tokenize(this.input.value);
tokens.pop();
if (!tokens.includes(selected)) {
tokens.push(selected);
}
this.input.value = tokens.join(", ") + ", ";
}
Проблема:
javascript
JavaScript
JAVASCRIPT
Для системы это могут быть одинаковые значения.
Решение:
function containsToken(tokens, value) {
return tokens.some(
token => token.toLowerCase() === value.toLowerCase()
);
}
Awesomplete позволяет скрывать элементы, которые уже выбраны.
filter: function(text, inputValue) {
let tokens = tokenize(inputValue);
let current = tokens.pop() || "";
if (containsToken(tokens, text)) {
return false;
}
return Awesomplete.FILTER_CONTAINS(text, current);
}
Теги могут быть представлены объектами.
[
{
id: 1,
name: "JavaScript"
},
{
id: 2,
name: "Python"
}
]
new Awesomplete(input, {
list: tags,
data: function(item) {
return {
label: item.name,
value: item.name
};
}
});
Иногда необходимо отображать текст, но сохранять идентификаторы.
JavaScript, Python
1,2
<input id="tags-visible">
<input type="hidden" id="tags-hidden">
const selectedIds = [];
new Awesomplete(input, {
list: data,
replace: function(item) {
let label = item.label;
let value = item.value;
selectedIds.push(value);
let tokens = tokenize(this.input.value);
tokens.pop();
tokens.push(label);
this.input.value = tokens.join(", ") + ", ";
hidden.value = selectedIds.join(",");
}
});
Awesomplete работает с обычным <input>, однако
поверх него можно построить полноценный интерфейс тегов.
<div class="tags-container">
<div class="tag">JavaScript</div>
<input id="tag-input">
</div>
function addTag(name) {
const tag = document.createElement("div");
tag.className = "tag";
tag.textContent = name;
container.insertBefore(tag, input);
}
function createRemoveButton(tagElement) {
const button = document.createElement("span");
button.textContent = "×";
button.oncl ick = () => {
tagElement.remove();
};
return button;
}
Более надёжный подход:
const tags = [];
Поле ввода используется только для текущего поиска.
| Компонент | Назначение |
|---|---|
| Awesomplete | Автодополнение |
| input | Ввод текста |
| tags[] | Хранилище тегов |
| DOM | Визуализация |
| hidden input | Отправка формы |
Awesomplete позволяет динамически менять список.
awesomplete.list = getAvailableTags();
input.addEventListener("input", async () => {
let query = extractCurrentToken(input.value);
let response = await fetch("/tags?q=" + query);
let data = await response.json();
awesomplete.list = data;
});
const MAX_TAGS = 5;
if (tags.length >= MAX_TAGS) {
return;
}
if (current.length < 2) {
return false;
}
Если пользователь ввёл отсутствующее значение:
GraphQL
можно создать новый тег.
if (!availableTags.includes(current)) {
tags.push(current);
}
Событие вызывается после завершения выбора.
input.addEventListener(
"awesomplete-selectcomplete",
function(event) {
console.log(event.text.value);
}
);
Срабатывает при перемещении по списку.
input.addEventListener(
"awesomplete-highlight",
function(event) {
console.log(event.text.label);
}
);
При создании тегов Enter часто используется для:
input.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault();
let value = extractCurrentToken(input.value);
addTag(value);
}
});
Во многих интерфейсах удаление пустого токена через Backspace удаляет предыдущий тег.
input.addEventListener("keydown", function(event) {
if (
event.key === "Backspace" &&
input.value === ""
) {
removeLastTag();
}
});
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 4px 10px;
background: #ececec;
border-radius: 4px;
}
.tags-container input {
border: none;
outline: none;
flex: 1;
}
При вставке строки:
JavaScript, Python, Rust
необходимо автоматически создать несколько тегов.
input.addEventListener("paste", function(event) {
event.preventDefault();
const text = (
event.clipboardData ||
window.clipboardData
).getData("text");
const tokens = tokenize(text);
tokens.forEach(addTag);
});
Перед сохранением часто выполняется очистка:
function normalize(token) {
return token
.trim()
.toLowerCase();
}
Awesomplete корректно работает с Unicode-символами:
Программирование
Разработка
デザイン
Иногда тег может содержать пробел:
machine learning
В этом случае пробел нельзя использовать как разделитель.
"machine learning", javascript
function parseQuotedTokens(text) {
const regex = /"([^"]+)"|([^,]+)/g;
const result = [];
let match;
while ((match = regex.exec(text))) {
result.push(
(match[1] || match[2]).trim()
);
}
return result;
}
При работе с тысячами элементов необходимо:
split;function debounce(callback, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
input.addEventListener(
"input",
debounce(loadSuggestions, 300)
);
<input id="skills">
<script>
const skills = [
"JavaScript",
"Python",
"Rust",
"Go",
"TypeScript",
"C++"
];
const input = document.querySelector("#skills");
function tokenize(text) {
return text
.split(",")
.map(item => item.trim())
.filter(Boolean);
}
function currentToken(text) {
let tokens = text.split(",");
return tokens.pop().trim();
}
new Awesomplete(input, {
list: skills,
filter: function(text, inputValue) {
return Awesomplete.FILTER_CONTAINS(
text,
currentToken(inputValue)
);
},
replace: function(selected) {
let tokens = tokenize(this.input.value);
tokens.pop();
if (!tokens.includes(selected)) {
tokens.push(selected);
}
this.input.value =
tokens.join(", ") + ", ";
}
});
</script>