UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

279 lines (241 loc) 8.18 kB
/** * Multi-select dropdown component with search functionality * Designed for selecting workspace administrators */ class MultiSelectDropdown { constructor(container, options = {}) { this.container = container; this.options = { placeholder: options.placeholder || 'Select items...', searchPlaceholder: options.searchPlaceholder || 'Search...', onSelectionChange: options.onSelectionChange || (() => {}), onSearch: options.onSearch || (() => {}), maxHeight: options.maxHeight || '300px', debounceDelay: options.debounceDelay || 300, ...options, }; this.selectedItems = new Map(); this.availableItems = []; this.filteredItems = []; this.isOpen = false; this.searchTimeout = null; this.init(); } init() { this.render(); this.setupEventListeners(); } render() { this.container.innerHTML = ` <div class="multi-select-dropdown"> <div class="multi-select-input" tabindex="0"> <div class="selected-items"> <!-- Selected items will be rendered here --> </div> <div class="dropdown-arrow"> <svg width="12" height="8" viewBox="0 0 12 8" fill="none"> <path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </div> </div> <div class="dropdown-panel" style="display: none;"> <div class="search-container"> <input type="text" class="search-input" placeholder="${this.options.searchPlaceholder}"> <div class="search-icon"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none"> <path d="M7 12C9.76142 12 12 9.76142 12 7C12 4.23858 9.76142 2 7 2C4.23858 2 2 4.23858 2 7C2 9.76142 4.23858 12 7 12Z" stroke="currentColor" stroke-width="1.5"/> <path d="M12 12L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg> </div> </div> <div class="filter-section"> <!-- Workspace filter will be added here --> </div> <div class="items-list" style="max-height: ${this.options.maxHeight}; overflow-y: auto;"> <!-- Items will be rendered here --> </div> </div> </div> `; this.elements = { input: this.container.querySelector('.multi-select-input'), selectedContainer: this.container.querySelector('.selected-items'), panel: this.container.querySelector('.dropdown-panel'), searchInput: this.container.querySelector('.search-input'), itemsList: this.container.querySelector('.items-list'), filterSection: this.container.querySelector('.filter-section'), }; this.updatePlaceholder(); } setupEventListeners() { // Toggle dropdown this.elements.input.addEventListener('click', e => { e.stopPropagation(); this.toggle(); }); // Search functionality this.elements.searchInput.addEventListener('input', e => { this.handleSearch(e.target.value); }); // Close dropdown when clicking outside document.addEventListener('click', e => { if (!this.container.contains(e.target)) { this.close(); } }); // Prevent dropdown close when clicking inside panel this.elements.panel.addEventListener('click', e => { e.stopPropagation(); }); // Keyboard navigation this.elements.input.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.toggle(); } else if (e.key === 'Escape') { this.close(); } }); } handleSearch(query) { // Debounce search if (this.searchTimeout) { clearTimeout(this.searchTimeout); } this.searchTimeout = setTimeout(() => { this.options.onSearch(query); }, this.options.debounceDelay); } setItems(items) { this.availableItems = items; this.filteredItems = [...items]; this.renderItems(); } setFilteredItems(items) { this.filteredItems = items; this.renderItems(); } renderItems() { if (this.filteredItems.length === 0) { this.elements.itemsList.innerHTML = '<div class="no-items">No items found</div>'; return; } const itemsHtml = this.filteredItems .map(item => { const isSelected = this.selectedItems.has(item.id); return ` <div class="dropdown-item ${isSelected ? 'selected' : ''}" data-id="${item.id}"> <div class="item-checkbox"> <input type="checkbox" ${isSelected ? 'checked' : ''} data-id="${item.id}"> </div> <div class="item-info"> <div class="item-name">${this.escapeHtml(item.name)}</div> <div class="item-details"> ${item.email ? `<span class="item-email">${this.escapeHtml(item.email)}</span>` : ''} ${item.workspace_name ? `<span class="item-workspace">${this.escapeHtml(item.workspace_name)}</span>` : ''} </div> </div> </div> `; }) .join(''); this.elements.itemsList.innerHTML = itemsHtml; // Add click listeners for items this.elements.itemsList.addEventListener('click', e => { const item = e.target.closest('.dropdown-item'); if (item) { const itemId = item.dataset.id; this.toggleItem(itemId); } }); } toggleItem(itemId) { const item = this.filteredItems.find(i => i.id === itemId) || this.availableItems.find(i => i.id === itemId); if (!item) return; if (this.selectedItems.has(itemId)) { this.selectedItems.delete(itemId); } else { this.selectedItems.set(itemId, item); } this.renderSelectedItems(); this.renderItems(); this.options.onSelectionChange(Array.from(this.selectedItems.values())); } renderSelectedItems() { if (this.selectedItems.size === 0) { this.updatePlaceholder(); return; } const selectedHtml = Array.from(this.selectedItems.values()) .map( item => ` <div class="selected-item" data-id="${item.id}"> <span class="selected-item-name">${this.escapeHtml(item.name)}</span> <button type="button" class="remove-item" data-id="${item.id}"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none"> <path d="M9 3L3 9M3 3L9 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg> </button> </div> ` ) .join(''); this.elements.selectedContainer.innerHTML = selectedHtml; // Add remove listeners this.elements.selectedContainer.addEventListener('click', e => { if (e.target.closest('.remove-item')) { e.stopPropagation(); const itemId = e.target.closest('.remove-item').dataset.id; this.toggleItem(itemId); } }); } updatePlaceholder() { if (this.selectedItems.size === 0) { this.elements.selectedContainer.innerHTML = `<div class="placeholder">${this.options.placeholder}</div>`; } } toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } open() { this.isOpen = true; this.elements.panel.style.display = 'block'; this.elements.input.classList.add('open'); setTimeout(() => this.elements.searchInput.focus(), 0); } close() { this.isOpen = false; this.elements.panel.style.display = 'none'; this.elements.input.classList.remove('open'); } getSelectedItems() { return Array.from(this.selectedItems.values()); } setSelectedItems(items) { this.selectedItems.clear(); items.forEach(item => { this.selectedItems.set(item.id, item); }); this.renderSelectedItems(); this.renderItems(); } clearSelection() { this.selectedItems.clear(); this.renderSelectedItems(); this.renderItems(); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } export default MultiSelectDropdown;