UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

374 lines (322 loc) 10.8 kB
// 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;