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
JavaScript
/**
* 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;