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