@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
374 lines (322 loc) • 10.8 kB
JavaScript
// Searchable Dropdown Component for Atlas Dashboard
class SearchableDropdown {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
placeholder: 'Search...',
maxHeight: '200px',
allowEmpty: true,
multiSelect: false,
showSelectedCount: true,
searchThreshold: 0,
noResultsText: 'No results found',
clearable: true,
...options
};
this.items = [];
this.selectedItems = this.options.multiSelect ? [] : null;
this.filteredItems = [];
this.isOpen = false;
this.searchTerm = '';
this.onSelect = this.options.onSelect || (() => {});
this.onClear = this.options.onClear || (() => {});
this.onSearch = this.options.onSearch || (() => {});
this.init();
}
init() {
this.render();
this.setupEventListeners();
}
render() {
this.container.innerHTML = `
<div class="searchable-dropdown" tabindex="0">
<div class="dropdown-trigger">
<div class="selected-display">
<span class="selected-text">${this.getDisplayText()}</span>
${this.options.clearable ? '<button class="clear-btn" style="display: none;"><i class="fas fa-times"></i></button>' : ''}
</div>
<div class="dropdown-arrow">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div class="dropdown-menu">
<div class="search-container">
<input type="text" class="dropdown-search" placeholder="${this.options.placeholder}" autocomplete="off">
<i class="fas fa-search search-icon"></i>
</div>
<div class="dropdown-options" style="max-height: ${this.options.maxHeight};">
<div class="no-results" style="display: none;">${this.options.noResultsText}</div>
</div>
</div>
</div>
`;
// Store references to key elements
this.dropdown = this.container.querySelector('.searchable-dropdown');
this.trigger = this.container.querySelector('.dropdown-trigger');
this.selectedDisplay = this.container.querySelector('.selected-text');
this.clearBtn = this.container.querySelector('.clear-btn');
this.menu = this.container.querySelector('.dropdown-menu');
this.searchInput = this.container.querySelector('.dropdown-search');
this.optionsContainer = this.container.querySelector('.dropdown-options');
this.noResults = this.container.querySelector('.no-results');
this.arrow = this.container.querySelector('.dropdown-arrow i');
this.updateItems();
}
setupEventListeners() {
// Toggle dropdown on trigger click
this.trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Handle search input
this.searchInput.addEventListener('input', (e) => {
this.searchTerm = e.target.value;
this.filterItems();
this.onSearch(this.searchTerm);
});
// Handle search input key events
this.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
this.focusFirstOption();
}
});
// Handle clear button
if (this.clearBtn) {
this.clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.clear();
});
}
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.close();
}
});
// Handle keyboard navigation
this.dropdown.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
} else if (e.key === 'Enter' && !this.isOpen) {
this.open();
}
});
}
setItems(items) {
this.items = items.map(item => {
if (typeof item === 'string') {
return { value: item, label: item, searchText: item };
} else {
return {
value: item.value,
label: item.label || item.value,
searchText: item.searchText || item.label || item.value,
data: item.data || {},
disabled: item.disabled || false
};
}
});
this.filteredItems = [...this.items];
this.updateItems();
}
updateItems() {
if (!this.optionsContainer) return;
this.optionsContainer.innerHTML = '';
if (this.filteredItems.length === 0) {
this.noResults.style.display = 'block';
return;
}
this.noResults.style.display = 'none';
this.filteredItems.forEach((item, index) => {
const option = document.createElement('div');
option.className = 'dropdown-option';
option.dataset.value = item.value;
option.dataset.index = index;
if (item.disabled) {
option.classList.add('disabled');
}
if (this.isSelected(item.value)) {
option.classList.add('selected');
}
// Highlight search term in label
const highlightedLabel = this.highlightSearchTerm(item.label);
option.innerHTML = `
<div class="option-content">
${this.options.multiSelect ? `<input type="checkbox" ${this.isSelected(item.value) ? 'checked' : ''} tabindex="-1">` : ''}
<span class="option-label">${highlightedLabel}</span>
${item.data.subtitle ? `<span class="option-subtitle">${item.data.subtitle}</span>` : ''}
</div>
${this.options.multiSelect && this.isSelected(item.value) ? '<i class="fas fa-check"></i>' : ''}
`;
if (!item.disabled) {
option.addEventListener('click', (e) => {
e.stopPropagation();
this.selectItem(item);
});
option.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.selectItem(item);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
this.focusNextOption(option);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.focusPrevOption(option);
}
});
option.setAttribute('tabindex', '0');
}
this.optionsContainer.appendChild(option);
});
}
filterItems() {
if (this.searchTerm.length < this.options.searchThreshold) {
this.filteredItems = [...this.items];
} else {
const term = this.searchTerm.toLowerCase();
this.filteredItems = this.items.filter(item =>
item.searchText.toLowerCase().includes(term)
);
}
this.updateItems();
}
highlightSearchTerm(text) {
if (!this.searchTerm) return text;
const regex = new RegExp(`(${this.searchTerm})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
selectItem(item) {
if (item.disabled) return;
if (this.options.multiSelect) {
const index = this.selectedItems.findIndex(selected => selected.value === item.value);
if (index >= 0) {
this.selectedItems.splice(index, 1);
} else {
this.selectedItems.push(item);
}
} else {
this.selectedItems = item;
this.close();
}
this.updateDisplay();
this.updateItems();
this.onSelect(this.getSelectedValues(), item);
}
isSelected(value) {
if (this.options.multiSelect) {
return this.selectedItems.some(item => item.value === value);
} else {
return this.selectedItems && this.selectedItems.value === value;
}
}
getSelectedValues() {
if (this.options.multiSelect) {
return this.selectedItems.map(item => item.value);
} else {
return this.selectedItems ? this.selectedItems.value : null;
}
}
getSelectedItems() {
return this.selectedItems;
}
getDisplayText() {
if (this.options.multiSelect) {
if (this.selectedItems.length === 0) {
return this.options.placeholder;
} else if (this.selectedItems.length === 1) {
return this.selectedItems[0].label;
} else if (this.options.showSelectedCount) {
return `${this.selectedItems.length} items selected`;
} else {
return this.selectedItems.map(item => item.label).join(', ');
}
} else {
return this.selectedItems ? this.selectedItems.label : this.options.placeholder;
}
}
updateDisplay() {
this.selectedDisplay.textContent = this.getDisplayText();
if (this.clearBtn) {
const hasSelection = this.options.multiSelect ?
this.selectedItems.length > 0 :
this.selectedItems !== null;
this.clearBtn.style.display = hasSelection ? 'block' : 'none';
}
}
clear() {
this.selectedItems = this.options.multiSelect ? [] : null;
this.updateDisplay();
this.updateItems();
this.onClear();
}
setValue(value) {
if (this.options.multiSelect) {
const values = Array.isArray(value) ? value : [value];
this.selectedItems = this.items.filter(item => values.includes(item.value));
} else {
this.selectedItems = this.items.find(item => item.value === value) || null;
}
this.updateDisplay();
this.updateItems();
}
open() {
if (this.isOpen) return;
this.isOpen = true;
this.dropdown.classList.add('open');
this.arrow.classList.add('rotated');
this.searchInput.focus();
// Reset search
this.searchInput.value = '';
this.searchTerm = '';
this.filterItems();
}
close() {
if (!this.isOpen) return;
this.isOpen = false;
this.dropdown.classList.remove('open');
this.arrow.classList.remove('rotated');
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
focusFirstOption() {
const firstOption = this.optionsContainer.querySelector('.dropdown-option:not(.disabled)');
if (firstOption) {
firstOption.focus();
}
}
focusNextOption(currentOption) {
let nextOption = currentOption.nextElementSibling;
while (nextOption && nextOption.classList.contains('disabled')) {
nextOption = nextOption.nextElementSibling;
}
if (nextOption) {
nextOption.focus();
}
}
focusPrevOption(currentOption) {
let prevOption = currentOption.previousElementSibling;
while (prevOption && prevOption.classList.contains('disabled')) {
prevOption = prevOption.previousElementSibling;
}
if (prevOption) {
prevOption.focus();
} else {
this.searchInput.focus();
}
}
destroy() {
// Clean up event listeners
this.container.innerHTML = '';
}
}
// Export for use in other scripts
window.SearchableDropdown = SearchableDropdown;