// Custom Select Component for VisaPics class CustomSelect { constructor(selectElement, options = {}) { // Check if already initialized if (selectElement.customSelectInstance) { console.warn('CustomSelect: Select already has instance', selectElement.id || '(no id)'); return selectElement.customSelectInstance; } console.log('CustomSelect: Creating new instance for', selectElement.id || '(no id)'); this.select = selectElement; this.options = { searchable: options.searchable || false, placeholder: options.placeholder || 'Select an option', noResultsText: options.noResultsText || 'No results found', ...options }; // Store instance reference this.select.customSelectInstance = this; this.init(); } init() { // Check if already initialized if (this.select.dataset.customSelectInitialized) { console.warn('CustomSelect: Select already initialized', this.select.id || '(no id)'); return; } // Mark as initialized this.select.dataset.customSelectInitialized = 'true'; // Hide original select this.select.style.display = 'none'; // Create custom select structure this.wrapper = document.createElement('div'); this.wrapper.className = 'custom-select-wrapper relative'; // Create display element this.display = document.createElement('div'); this.display.className = 'custom-select-display flex items-center justify-between px-3 py-2.5 border border-gray-300 rounded-lg cursor-pointer hover:border-blue-400 transition-all duration-200 bg-white'; const selectedText = this.getSelectedText(); const displayText = selectedText || this.options.placeholder; const textColorClass = selectedText ? 'text-gray-900' : 'text-gray-400'; this.display.innerHTML = ` ${displayText}
`; // Create dropdown this.dropdown = document.createElement('div'); this.dropdown.className = 'custom-select-dropdown absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg hidden overflow-hidden transform opacity-0 scale-95 transition-all duration-200'; // Add search if enabled if (this.options.searchable) { this.searchWrapper = document.createElement('div'); this.searchWrapper.className = 'p-3 border-b border-gray-100 bg-gray-50'; this.searchWrapper.innerHTML = `
`; this.dropdown.appendChild(this.searchWrapper); this.searchInput = this.searchWrapper.querySelector('input'); } // Create options container this.optionsContainer = document.createElement('div'); this.optionsContainer.className = 'custom-select-options max-h-60 overflow-y-auto'; this.dropdown.appendChild(this.optionsContainer); // Insert custom select this.select.parentNode.insertBefore(this.wrapper, this.select); this.wrapper.appendChild(this.display); this.wrapper.appendChild(this.dropdown); this.wrapper.appendChild(this.select); // Populate options this.populateOptions(); // Bind events this.bindEvents(); } getSelectedText() { const selected = this.select.options[this.select.selectedIndex]; return selected ? selected.text : ''; } populateOptions(searchTerm = '') { console.log('CustomSelect.populateOptions called for', this.select.id, 'with', this.select.options.length, 'options'); if (!this.optionsContainer) { console.error('CustomSelect: optionsContainer not found for', this.select.id); return; } this.optionsContainer.innerHTML = ''; const options = Array.from(this.select.options); let hasVisibleOptions = false; options.forEach((option, index) => { if (option.value === '') return; // Skip placeholder option const text = option.text; const value = option.value; // Filter by search term if (searchTerm && !text.toLowerCase().includes(searchTerm.toLowerCase())) { return; } hasVisibleOptions = true; const optionEl = document.createElement('div'); optionEl.className = 'custom-select-option px-3 py-2.5 hover:bg-gray-50 cursor-pointer transition-all duration-150 text-sm text-gray-700 hover:text-gray-900'; optionEl.dataset.value = value; optionEl.dataset.index = index; if (option.selected) { optionEl.classList.add('bg-blue-50', 'text-blue-600', 'font-medium'); } optionEl.innerHTML = this.options.renderOption ? this.options.renderOption(option) : text; this.optionsContainer.appendChild(optionEl); }); // Show no results message if (!hasVisibleOptions) { const noResults = document.createElement('div'); noResults.className = 'px-4 py-8 text-gray-400 text-center font-medium'; noResults.innerHTML = ` ${this.options.noResultsText} `; this.optionsContainer.appendChild(noResults); } } bindEvents() { // Toggle dropdown this.display.addEventListener('click', () => { this.toggle(); }); // Search functionality if (this.searchInput) { this.searchInput.addEventListener('input', (e) => { this.populateOptions(e.target.value); }); this.searchInput.addEventListener('click', (e) => { e.stopPropagation(); }); } // Option selection this.optionsContainer.addEventListener('click', (e) => { const option = e.target.closest('.custom-select-option'); if (option) { const value = option.dataset.value; const index = option.dataset.index; this.select.selectedIndex = index; this.updateDisplay(); this.close(); // Trigger change event const event = new Event('change', { bubbles: true }); this.select.dispatchEvent(event); } }); // Close on outside click document.addEventListener('click', (e) => { if (!this.wrapper.contains(e.target)) { this.close(); } }); // Close on escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.close(); } }); } updateDisplay() { const valueSpan = this.display.querySelector('.select-value'); const text = this.getSelectedText() || this.options.placeholder; valueSpan.textContent = text; // Update style based on whether a value is selected if (this.getSelectedText() && this.getSelectedText() !== this.options.placeholder) { valueSpan.classList.remove('text-gray-400'); valueSpan.classList.add('text-gray-900'); } else { valueSpan.classList.remove('text-gray-900'); valueSpan.classList.add('text-gray-400'); } } toggle() { if (this.dropdown.classList.contains('hidden')) { this.open(); } else { this.close(); } } open() { this.dropdown.classList.remove('hidden'); // Add small delay for animation setTimeout(() => { this.dropdown.classList.remove('opacity-0', 'scale-95'); this.dropdown.classList.add('opacity-100', 'scale-100'); }, 10); this.display.classList.add('ring-2', 'ring-blue-500', 'border-blue-500'); const svg = this.display.querySelector('svg'); svg.style.transform = 'rotate(180deg)'; svg.classList.add('text-gray-600'); if (this.searchInput) { this.searchInput.value = ''; this.searchInput.focus(); this.populateOptions(); } } close() { this.dropdown.classList.remove('opacity-100', 'scale-100'); this.dropdown.classList.add('opacity-0', 'scale-95'); // Hide after animation setTimeout(() => { this.dropdown.classList.add('hidden'); }, 200); this.display.classList.remove('ring-2', 'ring-blue-500', 'border-blue-500'); const svg = this.display.querySelector('svg'); svg.style.transform = 'rotate(0deg)'; svg.classList.remove('text-gray-600'); } } // Initialize all custom selects on page load document.addEventListener('DOMContentLoaded', function() { console.log('CustomSelect: Initializing custom selects...'); // Initialize all selects with data-custom-select attribute const selects = document.querySelectorAll('select[data-custom-select]'); console.log('CustomSelect: Found', selects.length, 'select elements to customize'); selects.forEach((select, index) => { console.log('CustomSelect: Initializing select', index + 1, select.id || '(no id)'); // Auto-enable searchable for vault selects const isVaultSelect = select.id === 'upload-vault' || select.id === 'import-vault-select'; const options = { searchable: select.dataset.searchable === 'true' || isVaultSelect, placeholder: select.dataset.placeholder || 'Select an option' }; new CustomSelect(select, options); }); console.log('CustomSelect: Initialization complete'); }); // Export for use in other scripts window.CustomSelect = CustomSelect;