Инвертирование выбора в Choices.js — операция, при
которой все ранее выбранные элементы становятся невыбранными, а все
невыбранные — выбранными. Подобная логика особенно востребована в
интерфейсах массового выбора:
Библиотека Choices.js не содержит встроенного метода
invertSelection(), поэтому инвертирование реализуется
программно через API библиотеки.
Перед реализацией инверсии необходимо понимать, как библиотека хранит выбранные элементы.
Для <sel ect multiple> библиотека работает с:
<select>.Пример исходного списка:
<select id="frameworks" multiple>
<option value="react">React</option>
<option value="vue">Vue</option>
<option value="angular">Angular</option>
<option value="svelte">Svelte</option>
</select>
Инициализация:
const choices = new Choices('#frameworks', {
removeItemButton: true
});
Для инверсии необходимо определить:
const selectedItems = choices.getValue(true);
console.log(selectedItems);
Результат:
["react", "vue"]
Метод:
getValue(true)
возвращает только значения.
Без аргумента true возвращаются полноценные объекты.
Наиболее простой способ — обратиться к оригинальному
<select>:
const select = document.getElementById('frameworks');
const allValues = Array.fr om(sel ect.options).map(
option => option.value
);
console.log(allValues);
Результат:
["react", "vue", "angular", "svelte"]
Инвертирование работает по простой схеме:
function invertSelection(choices, selectElement) {
const allValues = Array.fr om(selectElement.options).map(
option => option.value
);
const selectedValues = choices.getValue(true);
const inverted = allValues.filter(
value => !selectedValues.includes(value)
);
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
}
Использование:
const sel ect = document.getElementById('frameworks');
const choices = new Choices(select, {
removeItemButton: true
});
invertSelection(choices, select);
const allValues = Array.fr om(sel ect.options).map(
option => option.value
);
Результат:
["react", "vue", "angular", "svelte"]
const selectedValues = choices.getValue(true);
Например:
["react", "vue"]
const inverted = allValues.filter(
value => !selectedValues.includes(value)
);
Результат:
["angular", "svelte"]
choices.removeActiveItems();
Метод удаляет текущие selected item.
choices.setChoiceByValue(inverted);
После выполнения выбранными станут:
["angular", "svelte"]
Наиболее распространённый сценарий.
<select id="users" multiple>
<option value="1">Иван</option>
<option value="2">Мария</option>
<option value="3">Олег</option>
<option value="4">Анна</option>
</select>
<button id="invert">
Инвертировать
</button>
const select = document.getElementById('users');
const choices = new Choices(select);
document
.getElementById('invert')
.addEventListener('click', () => {
const allValues = Array.fr om(sel ect.options).map(
option => option.value
);
const selectedValues = choices.getValue(true);
const inverted = allValues.filter(
value => !selectedValues.includes(value)
);
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
});
Очень важный сценарий — исключение недоступных элементов.
Если option имеет:
<option value="admin" disabled>
Admin
</option>
то инверсия не должна выбирать такой элемент.
const allValues = Array.fr om(sel ect.options)
.filter(option => !option.disabled)
.map(option => option.value);
function invertSelection(choices, select) {
const allValues = Array.fr om(sel ect.options)
.filter(option => !option.disabled)
.map(option => option.value);
const selectedValues = choices.getValue(true);
const inverted = allValues.filter(
value => !selectedValues.includes(value)
);
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
}
При использовании поиска или фильтрации иногда требуется инвертировать только отображаемые option.
Пользователь ввёл:
rea
В списке отображается только:
React
Инверсия должна работать лишь с отфильтрованным набором.
Choices.js хранит choices во внутреннем хранилище:
const storeChoices = choices._store.choices;
Получение только активных элементов:
const visibleChoices = storeChoices.filter(
item => item.active
);
function invertVisible(choices) {
const visibleChoices = choices._store.choices
.filter(choice => choice.active);
const visibleValues = visibleChoices.map(
choice => choice.value
);
const selectedValues = choices.getValue(true);
const inverted = visibleValues.filter(
value => !selectedValues.includes(value)
);
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
}
Иногда удаление всех selected item вызывает:
В таком случае можно переключать элементы по одному.
function toggleValue(choices, value, selected) {
if (selected) {
choices.removeActiveItemsByValue(value);
} else {
choices.setChoiceByValue(value);
}
}
function invertSelection(choices, select) {
const selectedValues = choices.getValue(true);
Array.fr om(sel ect.options).forEach(option => {
const isSelected = selectedValues.includes(
option.value
);
if (isSelected) {
choices.removeActiveItemsByValue(
option.value
);
} else {
choices.setChoiceByValue(
option.value
);
}
});
}
Для обычного <select> без multiple
полноценная инверсия невозможна, потому что выбран может быть только
один элемент.
<select id="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
Инверсия превращается в переключение.
const current = choices.getValue(true);
const next =
current === 'light'
? 'dark'
: 'light';
choices.setChoiceByValue(next);
Инвертирование генерирует множество событий:
addItemremoveItemchangeЕсли на каждое событие подписана логика:
choices.passedElement.element.addEventListener(
'change',
event => {
console.log(event);
}
);
то массовая инверсия может вызвать десятки событий.
Распространённый подход:
let suppressEvents = false;
select.addEventListener('change', event => {
if (suppressEvents) {
return;
}
console.log('change');
});
suppressEvents = true;
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
suppressEvents = false;
При количестве option:
неэффективные операции начинают тормозить интерфейс.
selectedValues.includes(value)
Проблема — линейный поиск.
const selectedSet = new Set(
choices.getValue(true)
);
const inverted = allValues.filter(
value => !selectedSet.has(value)
);
function invertSelection(choices, select) {
const allValues = Array.fr om(sel ect.options)
.map(option => option.value);
const selectedSet = new Set(
choices.getValue(true)
);
const inverted = allValues.filter(
value => !selectedSet.has(value)
);
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
}
Иногда требуется инвертировать только определённую группу.
Инвертировать только:
frontend-технологии
но не трогать backend.
<select id="stack" multiple>
<option value="react">React</option>
<option value="vue">Vue</option>
<option value="node">Node.js</option>
<option value="php">PHP</option>
</select>
const frontend = ['react', 'vue'];
function invertFrontend(choices) {
const selected = new Set(
choices.getValue(true)
);
frontend.forEach(value => {
if (selected.has(value)) {
choices.removeActiveItemsByValue(
value
);
} else {
choices.setChoiceByValue(value);
}
});
}
Choices.js поддерживает custom properties.
new Choices('#users', {
choices: [
{
value: '1',
label: 'Иван',
customProperties: {
role: 'admin'
}
},
{
value: '2',
label: 'Мария',
customProperties: {
role: 'user'
}
}
]
});
const adminValues = choices._store.choices
.filter(choice =>
choice.customProperties.role === 'admin'
)
.map(choice => choice.value);
function invertAdmins(choices) {
const selected = new Set(
choices.getValue(true)
);
const admins = choices._store.choices
.filter(choice =>
choice.customProperties.role === 'admin'
);
admins.forEach(admin => {
if (selected.has(admin.value)) {
choices.removeActiveItemsByValue(
admin.value
);
} else {
choices.setChoiceByValue(
admin.value
);
}
});
}
Ошибка:
new Choices('#users');
new Choices('#users');
Инверсия начинает работать непредсказуемо.
Ошибка:
const selected = choices.getValue(true);
choices.removeActiveItems();
console.log(selected);
После изменения состояния старые данные уже неактуальны.
Неверно:
choices.setChoiceByValue('admin');
если option disabled.
Placeholder не должен участвовать в переключении.
Проблемный вариант:
<option value=""></option>
Инверсия может случайно выбрать пустое значение.
const values = Array.fr om(sel ect.options)
.filter(option =>
option.value !== '' &&
!option.disabled
)
.map(option => option.value);
function invertSelection(choices, select, options = {}) {
const {
ignoreDisabled = true,
ignoreEmpty = true
} = options;
const values = Array.fr om(select.options)
.filter(option => {
if (ignoreDisabled && option.disabled) {
return false;
}
if (ignoreEmpty && option.value === '') {
return false;
}
return true;
})
.map(option => option.value);
const selected = new Set(
choices.getValue(true)
);
const inverted = values.filter(
value => !selected.has(value)
);
choices.removeActiveItems();
choices.setChoiceByValue(inverted);
}
<select id="filters" multiple>
<option value="new">Новые</option>
<option value="popular">Популярные</option>
<option value="discount">Со скидкой</option>
<option value="hit">Хиты продаж</option>
</select>
<button id="invertFilters">
Инвертировать фильтры
</button>
const select = document.getElementById('filters');
const choices = new Choices(select, {
removeItemButton: true
});
document
.getElementById('invertFilters')
.addEventListener('click', () => {
invertSelection(choices, select);
});
Такой подход позволяет быстро реализовать массовое переключение состояния выбранных элементов без ручной обработки каждого option отдельно.