/**
* Multi-select dropdown component
* Creates a dropdown with checkboxes for multiple selections
*/
class MultiSelect {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
placeholder: options.placeholder || 'Select...',
searchable: options.searchable !== false,
onChange: options.onChange || (() => {}),
maxDisplay: options.maxDisplay || 2,
};
this.items = [];
this.selected = new Set();
this.isOpen = false;
this.searchQuery = '';
this.render();
this.setupEventListeners();
}
render() {
this.container.classList.add('multi-select');
this.container.innerHTML = `
${this.options.searchable ? `
` : ''}
`;
this.trigger = this.container.querySelector('.multi-select-trigger');
this.display = this.container.querySelector('.multi-select-display');
this.dropdown = this.container.querySelector('.multi-select-dropdown');
this.optionsContainer = this.container.querySelector('.multi-select-options');
this.searchInput = this.container.querySelector('.multi-select-search');
this.clearBtn = this.container.querySelector('.multi-select-clear');
this.doneBtn = this.container.querySelector('.multi-select-done');
}
setupEventListeners() {
// Toggle dropdown
this.trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Search
if (this.searchInput) {
this.searchInput.addEventListener('input', () => {
this.searchQuery = this.searchInput.value.toLowerCase();
this.renderOptions();
});
this.searchInput.addEventListener('click', (e) => e.stopPropagation());
}
// Clear selection
this.clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.clearSelection();
});
// Done button
this.doneBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.close();
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.close();
}
});
// Keyboard navigation
this.container.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
}
setItems(items) {
this.items = items.map(item => {
if (typeof item === 'string') {
return { value: item, label: item };
}
return item;
});
this.renderOptions();
}
renderOptions() {
const filteredItems = this.items.filter(item => {
if (!this.searchQuery) return true;
return item.label.toLowerCase().includes(this.searchQuery);
});
if (filteredItems.length === 0) {
this.optionsContainer.innerHTML = 'No options found
';
return;
}
this.optionsContainer.innerHTML = filteredItems.map(item => `
`).join('');
// Add change listeners to checkboxes
this.optionsContainer.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const value = e.target.closest('.multi-select-option').dataset.value;
if (e.target.checked) {
this.selected.add(value);
} else {
this.selected.delete(value);
}
this.updateDisplay();
this.options.onChange(this.getSelected());
});
});
}
updateDisplay() {
const selected = this.getSelected();
if (selected.length === 0) {
this.display.textContent = this.options.placeholder;
this.display.classList.remove('has-value');
} else if (selected.length <= this.options.maxDisplay) {
this.display.textContent = selected.join(', ');
this.display.classList.add('has-value');
} else {
this.display.textContent = `${selected.length} selected`;
this.display.classList.add('has-value');
}
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open() {
this.isOpen = true;
this.container.classList.add('is-open');
this.trigger.setAttribute('aria-expanded', 'true');
if (this.searchInput) {
this.searchInput.value = '';
this.searchQuery = '';
this.renderOptions();
setTimeout(() => this.searchInput.focus(), 10);
}
}
close() {
this.isOpen = false;
this.container.classList.remove('is-open');
this.trigger.setAttribute('aria-expanded', 'false');
}
getSelected() {
return Array.from(this.selected);
}
setSelected(values) {
this.selected = new Set(values);
this.renderOptions();
this.updateDisplay();
}
clearSelection() {
this.selected.clear();
this.renderOptions();
this.updateDisplay();
this.options.onChange(this.getSelected());
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = MultiSelect;
}