Разделители значений используются в тех случаях, когда одно текстовое
поле должно содержать несколько независимых элементов. Вместо создания
набора отдельных <input> пользователь вводит
последовательность значений через специальные символы:
JavaScript, HTML, CSS
или:
apple; orange; banana
Библиотека Awesomplete изначально ориентирована на работу с одним значением, однако благодаря переопределению механизма вставки можно реализовать поддержку множественного ввода с разделителями практически любого типа.
Подобная схема особенно полезна при создании:
По умолчанию Awesomplete заменяет всё содержимое поля выбранным элементом:
new Awesomplete(input, {
list: ["JavaScript", "HTML", "CSS"]
});
Если ввести:
JavaScript, H
и выбрать HTML, результат окажется таким:
HTML
Предыдущая часть строки будет потеряна.
Причина заключается в стандартной реализации метода
replace().
Основная идея состоит в следующем:
Наиболее распространённый вариант — разделение через запятую.
<input id="tags">
const input = document.getElementById("tags");
new Awesomplete(input, {
list: [
"JavaScript",
"TypeScript",
"HTML",
"CSS",
"Node.js",
"React"
],
filter(text, inputValue) {
const value = inputValue.match(/[^,]*$/)[0].trim();
return Awesomplete.FILTER_CONTAINS(text, value);
},
item(text, inputValue) {
const value = inputValue.match(/[^,]*$/)[0].trim();
return Awesomplete.ITEM(text, value);
},
replace(text) {
const before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value = before + text + ", ";
}
});
inputValue.match(/[^,]*$/)[0]
Конструкция означает:
| Часть | Назначение |
|---|---|
[^,] |
любой символ кроме запятой |
* |
любое количество |
$ |
конец строки |
Пример:
JavaScript, HT
Результат:
HT
Именно этот фрагмент участвует в фильтрации.
Метод replace() отвечает за вставку выбранного
элемента.
Из строки:
JavaScript, HT
необходимо получить:
JavaScript, HTML,
replace(text) {
const before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value = before + text + ", ";
}
^.+,\s*|Оно извлекает всё содержимое до последней запятой.
Исходная строка:
JavaScript, CSS, Re
Результат:
JavaScript, CSS,
После этого добавляется выбранное значение:
before + text + ", "
Итог:
JavaScript, CSS, React,
Иногда необходимо хранить значения без пробелов.
replace(text) {
const before = this.input.value.match(/^.+,|/)[0];
this.input.value = before + text + ",";
}
Результат:
JavaScript,HTML,CSS,
Разделителем может выступать любой символ.
;new Awesomplete(input, {
filter(text, inputValue) {
const value = inputValue.match(/[^;]*$/)[0].trim();
return Awesomplete.FILTER_CONTAINS(text, value);
},
item(text, inputValue) {
const value = inputValue.match(/[^;]*$/)[0].trim();
return Awesomplete.ITEM(text, value);
},
replace(text) {
const before = this.input.value.match(/^.+;\s*|/)[0];
this.input.value = before + text + "; ";
}
});
Иногда требуется поддержка одновременно нескольких символов:
filter(text, inputValue) {
const value = inputValue.match(/[^,;|]*$/)[0].trim();
return Awesomplete.FILTER_CONTAINS(text, value);
}
Awesomplete можно использовать даже внутри многострочного поля.
<textarea id="skills"></textarea>
const textarea = document.getElementById("skills");
new Awesomplete(textarea, {
list: [
"Docker",
"Kubernetes",
"Nginx",
"Redis"
],
filter(text, inputValue) {
const value = inputValue.match(/[^\n]*$/)[0].trim();
return Awesomplete.FILTER_CONTAINS(text, value);
},
replace(text) {
const before = this.input.value.match(/^.+\n|/s)[0];
this.input.value = before + text + "\n";
}
});
Редкий, но возможный вариант — разделение через символ табуляции.
filter(text, inputValue) {
const value = inputValue.match(/[^\t]*$/)[0];
return Awesomplete.FILTER_CONTAINS(text, value);
}
Иногда автодополнение должно работать после каждого слова.
filter(text, inputValue) {
const value = inputValue.match(/[^\s]*$/)[0];
return Awesomplete.FILTER_CONTAINS(text, value);
}
Подобный подход встречается:
Разделители хорошо работают совместно с объектными структурами.
const technologies = [
{
label: "JavaScript",
value: "js"
},
{
label: "TypeScript",
value: "ts"
},
{
label: "Python",
value: "py"
}
];
new Awesomplete(input, {
list: technologies,
replace(item) {
const before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value =
before +
item.value +
", ";
}
});
Иногда требуется отображать человекочитаемый текст.
replace(item) {
const before = this.input.value.match(/^.+,\s*|/)[0];
this.input.value =
before +
item.label +
", ";
}
При множественном вводе часто возникает проблема повторов.
replace(text) {
const values = this.input.value
.split(",")
.map(v => v.trim())
.filter(Boolean);
if (values.includes(text)) {
return;
}
values.push(text);
this.input.value = values.join(", ") + ", ";
}
Иногда полезно удалять лишние пробелы.
replace(text) {
const values = this.input.value
.split(",")
.map(v => v.trim())
.filter(Boolean);
values.push(text);
this.input.value =
values.join(", ") + ", ";
}
Стандартная реализация вставляет значение в конец строки. Это создаёт проблемы, если курсор находится в середине текста.
JavaScript, HTML, CSS
Курсор установлен после HTML.
После выбора элемента значение может оказаться добавлено в конец строки вместо нужной позиции.
const cursor = input.selectionStart;
const beforeCursor =
input.value.slice(0, cursor);
const afterCursor =
input.value.slice(cursor);
replace(text) {
const cursor = this.input.selectionStart;
const before =
this.input.value.slice(0, cursor);
const after =
this.input.value.slice(cursor);
const start =
before.replace(/[^,]*$/, "");
const end =
after.replace(/^[^,]*/, "");
this.input.value =
start +
text +
end;
}
При динамической загрузке данных важно отправлять только текущий фрагмент.
input.addEventListener("input", async () => {
const query =
input.value.match(/[^,]*$/)[0].trim();
if (!query) {
return;
}
const response =
await fetch("/search?q=" + query);
const data =
await response.json();
awesomplete.list = data;
});
Вместо одиночных символов можно использовать полноценные конструкции.
tag:javascript
tag:html
const token =
input.value.match(/tag:[^\s]*$/);
Иногда строка используется как промежуточное представление массива.
const values = input.value
.split(",")
.map(v => v.trim())
.filter(Boolean);
Результат:
[
"JavaScript",
"HTML",
"CSS"
]
replace(text) {
const values = new Set(
this.input.value
.split(",")
.map(v => v.trim())
.filter(Boolean)
);
values.add(text);
this.input.value =
[...values].join(", ") + ", ";
}
Разделитель можно полностью абстрагировать.
function getCurrentToken(value) {
return value.match(/[^,]*$/)[0].trim();
}
filter(text, inputValue) {
return Awesomplete.FILTER_CONTAINS(
text,
getCurrentToken(inputValue)
);
}
function createTokenizer(separator) {
const escaped =
separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regexp =
new RegExp(`[^${escaped}]*$`);
return function(value) {
return value.match(regexp)[0].trim();
};
}
const getToken =
createTokenizer(",");
new Awesomplete(input, {
filter(text, inputValue) {
return Awesomplete.FILTER_CONTAINS(
text,
getToken(inputValue)
);
},
item(text, inputValue) {
return Awesomplete.ITEM(
text,
getToken(inputValue)
);
}
});
Причина:
this.input.value = text;
Исправление:
this.input.value =
before + text;
Ошибка:
/[^,]$/
Такой шаблон получает только один символ.
Правильно:
/[^,]*$/
Без очистки пробелов поиск работает некорректно:
" HTML"
Вместо:
"HTML"
При работе с тысячами элементов рекомендуется:
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
На практике механизм разделителей часто становится основой полноценного компонента тегов.
[input]
↓
текущий токен
↓
фильтрация
↓
выбор элемента
↓
replace()
↓
обновление строки
Благодаря переопределению методов filter(),
item() и replace() библиотека Awesomplete
позволяет реализовывать сложные сценарии множественного ввода без
модификации внутреннего кода библиотеки.