Skip to content

Instantly share code, notes, and snippets.

@rglover
Created November 15, 2024 17:23
Show Gist options
  • Save rglover/2bc9e9480e59e060cf107bf942683bb1 to your computer and use it in GitHub Desktop.
Save rglover/2bc9e9480e59e060cf107bf942683bb1 to your computer and use it in GitHub Desktop.
Mod Combobox Example
import fuzzysort from "fuzzysort";
class Combobox {
constructor(element, options = {}) {
this.container = element;
this.select = element.querySelector('.mod-combobox-select');
this.selectedText = this.select.querySelector('p');
this.dropdown = element.querySelector('.mod-combobox-dropdown');
this.input = this.dropdown.querySelector('.mod-input');
this.list = this.dropdown.querySelector('ul');
this.options = {
min_chars: options.min_chars || 1,
max_items: options.max_items || 10,
events: {
on_select_item: options.events?.on_select_item || (() => {}),
on_clear_search: options.events?.on_clear_search || (() => {})
}
};
this.selected_index = -1;
this.selected_value = null;
this.items = Array.from(this.list.children).map(li => ({
value: li.dataset.value,
name: li.textContent
}));
this.setup_event_listeners();
}
setup_event_listeners() {
// Toggle dropdown on select click
this.select.addEventListener('click', () => {
this.toggle_dropdown();
});
// Handle input changes
this.input.addEventListener('input', () => this.handle_input());
// Handle keyboard navigation
this.input.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
this.select_next();
break;
case 'ArrowUp':
e.preventDefault();
this.select_previous();
break;
case 'Enter':
e.preventDefault();
// If there's a selected index, use that
if (this.selected_index >= 0) {
this.select_item(this.selected_index, e);
}
// If there's no selected index but we have filtered results and input has value
else if (this.input.value.trim() && this.list.children.length > 0) {
this.select_item(0, e); // Select the first item
}
break;
case 'Escape':
this.hide_dropdown();
break;
}
});
// Handle list item clicks
this.list.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) {
const index = Array.from(this.list.children).indexOf(li);
this.select_item(index, e);
}
});
// Handle clicks outside
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.hide_dropdown();
}
});
}
handle_input() {
const search_value = this.input.value.toLowerCase().trim();
if (search_value.length < this.options.min_chars) {
this.show_all_items();
return;
}
const filtered_items = this.filter_items(search_value);
this.update_dropdown(filtered_items);
}
filter_items(search_value) {
const search_results = fuzzysort.go(search_value, this.items, {
keys: ['name'],
limit: this.options.max_items,
threshold: -10000
});
return search_results.map(result => result.obj);
}
update_dropdown(items) {
this.list.innerHTML = '';
this.selected_index = -1;
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
li.dataset.value = item.value;
if (item.value === this.selected_value) {
li.classList.add('is-selected');
}
this.list.appendChild(li);
});
}
show_all_items() {
this.update_dropdown(this.items);
}
select_next() {
const items = this.list.children;
if (this.selected_index < items.length - 1) {
this.selected_index++;
this.update_selection();
}
}
select_previous() {
if (this.selected_index > 0) {
this.selected_index--;
this.update_selection();
}
}
update_selection() {
const items = Array.from(this.list.children);
items.forEach((item, index) => {
item.classList.toggle('is-selected', index === this.selected_index);
});
if (this.selected_index >= 0) {
const selected_item = items[this.selected_index];
selected_item.scrollIntoView({
block: 'nearest',
behavior: 'smooth'
});
}
}
select_item(index, event) {
const items = this.list.children;
if (index >= 0 && index < items.length) {
const selected_node = items[index];
const value = selected_node.dataset.value;
// Store the selected value
this.selected_value = value;
this.selectedText.textContent = selected_node.textContent;
this.hide_dropdown();
this.clear_search();
// Trigger custom callback
this.options.events.on_select_item(value, event, selected_node);
}
}
clear_search() {
this.input.value = '';
this.show_all_items();
this.options.events.on_clear_search();
}
toggle_dropdown() {
if (this.container.classList.contains('is-open')) {
this.hide_dropdown();
} else {
this.show_dropdown();
}
}
show_dropdown() {
this.container.classList.add('is-open');
this.input.focus();
this.show_all_items();
}
hide_dropdown() {
this.container.classList.remove('is-open');
this.clear_search();
}
set_selected(value) {
const item = this.items.find(item => item.value === value);
if (item) {
this.selected_value = value;
this.selectedText.textContent = item.name;
this.show_all_items(); // Refresh dropdown to show selection
}
}
}
export default function combobox(element, options = {}) {
return new Combobox(element, options);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment