/** * AutoComplete Manager Module * Handles intelligent auto-suggestions and field auto-completion */ export class AutoCompleteManager { constructor(apiClient, uiManager) { this.apiClient = apiClient; this.uiManager = uiManager; this.searchTimeouts = {}; this.activeDropdowns = new Set(); this.selectedIndex = -1; this.availableTreeCodes = []; } async initialize() { try { this.availableTreeCodes = await this.apiClient.loadTreeCodes(); this.setupAutocomplete('localName', 'tree-suggestions'); this.setupAutocomplete('scientificName', 'tree-suggestions'); this.setupAutocomplete('commonName', 'tree-suggestions'); this.setupAutocomplete('treeCode', 'tree-codes'); } catch (error) { console.error('Error initializing auto-suggestions:', error); } } setupAutocomplete(fieldId, apiType) { const input = document.getElementById(fieldId); if (!input) return; this.createAutocompleteContainer(input, fieldId); this.attachEventListeners(input, fieldId, apiType); } createAutocompleteContainer(input, fieldId) { if (input.parentElement.classList.contains('autocomplete-container')) return; const container = document.createElement('div'); container.className = 'autocomplete-container'; input.parentNode.insertBefore(container, input); container.appendChild(input); const dropdown = document.createElement('div'); dropdown.className = 'autocomplete-dropdown'; dropdown.id = `${fieldId}-dropdown`; container.appendChild(dropdown); } attachEventListeners(input, fieldId, apiType) { input.addEventListener('input', (e) => this.handleInputChange(e, apiType)); input.addEventListener('keydown', (e) => this.handleKeyDown(e, fieldId)); input.addEventListener('blur', (e) => this.handleInputBlur(e, fieldId)); input.addEventListener('focus', (e) => this.handleInputFocus(e, fieldId, apiType)); } async handleInputChange(event, apiType) { const input = event.target; const query = input.value.trim(); const fieldId = input.id; this.clearSearchTimeout(fieldId); if (query.length < 2) { this.hideDropdown(fieldId); return; } this.showLoadingState(fieldId); this.debounceSearch(fieldId, query, apiType); } clearSearchTimeout(fieldId) { if (this.searchTimeouts[fieldId]) { clearTimeout(this.searchTimeouts[fieldId]); } } debounceSearch(fieldId, query, apiType) { this.searchTimeouts[fieldId] = setTimeout(async () => { try { const suggestions = await this.fetchSuggestions(query, apiType, fieldId); this.showSuggestions(fieldId, suggestions, query); } catch (error) { console.error('Error fetching suggestions:', error); this.hideDropdown(fieldId); } }, 300); } async fetchSuggestions(query, apiType, fieldId) { if (apiType === 'tree-codes') { return this.filterTreeCodes(query); } else { return this.searchTreeSuggestions(query, fieldId); } } filterTreeCodes(query) { return this.availableTreeCodes .filter(code => code.toLowerCase().includes(query.toLowerCase())) .slice(0, 10) .map(code => ({ primary: code, secondary: 'Tree Reference Code', type: 'code' })); } async searchTreeSuggestions(query, fieldId) { const suggestions = await this.apiClient.searchTreeSuggestions(query, 10); return suggestions.map(suggestion => ({ primary: this.getPrimaryText(suggestion, fieldId), secondary: this.getSecondaryText(suggestion, fieldId), badges: this.getBadges(suggestion), data: suggestion })); } getPrimaryText(suggestion, fieldId) { const fieldMap = { 'localName': suggestion.local_name, 'scientificName': suggestion.scientific_name, 'commonName': suggestion.common_name }; return fieldMap[fieldId] || suggestion.local_name || suggestion.scientific_name || suggestion.common_name; } getSecondaryText(suggestion, fieldId) { const parts = []; if (fieldId !== 'localName' && suggestion.local_name) { parts.push(`Local: ${suggestion.local_name}`); } if (fieldId !== 'scientificName' && suggestion.scientific_name) { parts.push(`Scientific: ${suggestion.scientific_name}`); } if (fieldId !== 'commonName' && suggestion.common_name) { parts.push(`Common: ${suggestion.common_name}`); } if (suggestion.tree_code) { parts.push(`Code: ${suggestion.tree_code}`); } return parts.join(' • '); } getBadges(suggestion) { const badges = []; if (suggestion.tree_code) { badges.push(suggestion.tree_code); } if (suggestion.fruiting_season) { badges.push(`Season: ${suggestion.fruiting_season}`); } return badges; } showLoadingState(fieldId) { const dropdown = document.getElementById(`${fieldId}-dropdown`); if (!dropdown) return; dropdown.innerHTML = '
Searching...
'; dropdown.style.display = 'block'; this.activeDropdowns.add(fieldId); } showSuggestions(fieldId, suggestions, query) { const dropdown = document.getElementById(`${fieldId}-dropdown`); if (!dropdown) return; if (suggestions.length === 0) { dropdown.innerHTML = '
No matching suggestions found
'; dropdown.style.display = 'block'; this.activeDropdowns.add(fieldId); return; } const html = this.buildSuggestionsHTML(suggestions, query, fieldId); dropdown.innerHTML = html; dropdown.style.display = 'block'; this.activeDropdowns.add(fieldId); this.selectedIndex = -1; this.attachSuggestionClickHandlers(dropdown, suggestions); } buildSuggestionsHTML(suggestions, query, fieldId) { return suggestions.map((suggestion, index) => `
${this.highlightMatch(suggestion.primary, query)}
${suggestion.secondary ? `
${suggestion.secondary}
` : ''} ${this.buildBadgesHTML(suggestion.badges)}
`).join(''); } buildBadgesHTML(badges) { if (!badges || badges.length === 0) return ''; const badgeElements = badges.map(badge => `${badge}`).join(''); return `
${badgeElements}
`; } highlightMatch(text, query) { if (!query || !text) return text; const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); return text.replace(regex, '$1'); } attachSuggestionClickHandlers(dropdown, suggestions) { dropdown.querySelectorAll('.autocomplete-item').forEach(item => { item.addEventListener('mousedown', (e) => this.handleSuggestionClick(e, suggestions)); }); } handleSuggestionClick(event, suggestions) { event.preventDefault(); const item = event.target.closest('.autocomplete-item'); const index = parseInt(item.dataset.index); const fieldId = item.dataset.field; const suggestion = suggestions[index]; this.applySuggestion(fieldId, suggestion); this.hideDropdown(fieldId); } applySuggestion(fieldId, suggestion) { const input = document.getElementById(fieldId); if (suggestion.type === 'code') { input.value = suggestion.primary; } else { this.applySuggestionData(input, fieldId, suggestion); this.autoFillRelatedFields(suggestion.data, fieldId); } input.dispatchEvent(new Event('input', { bubbles: true })); } applySuggestionData(input, fieldId, suggestion) { const data = suggestion.data; const fieldValueMap = { 'localName': data.local_name, 'scientificName': data.scientific_name, 'commonName': data.common_name }; input.value = fieldValueMap[fieldId] || suggestion.primary; } autoFillRelatedFields(data, excludeFieldId) { const fields = { 'localName': data.local_name, 'scientificName': data.scientific_name, 'commonName': data.common_name, 'treeCode': data.tree_code }; Object.entries(fields).forEach(([fieldId, value]) => { if (fieldId !== excludeFieldId && value) { this.fillEmptyField(fieldId, value); } }); } fillEmptyField(fieldId, value) { const input = document.getElementById(fieldId); if (input && !input.value.trim()) { input.value = value; this.uiManager.highlightAutoFilledField(fieldId); } } handleKeyDown(event, fieldId) { const dropdown = document.getElementById(`${fieldId}-dropdown`); if (!dropdown || dropdown.style.display === 'none') return; const items = dropdown.querySelectorAll('.autocomplete-item'); if (items.length === 0) return; switch (event.key) { case 'ArrowDown': event.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1); this.updateHighlight(items); break; case 'ArrowUp': event.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, -1); this.updateHighlight(items); break; case 'Enter': event.preventDefault(); if (this.selectedIndex >= 0 && items[this.selectedIndex]) { items[this.selectedIndex].click(); } break; case 'Escape': event.preventDefault(); this.hideDropdown(fieldId); break; } } updateHighlight(items) { items.forEach((item, index) => { item.classList.toggle('highlighted', index === this.selectedIndex); }); } handleInputBlur(event, fieldId) { setTimeout(() => this.hideDropdown(fieldId), 150); } handleInputFocus(event, fieldId, apiType) { const input = event.target; if (input.value.length >= 2) { this.handleInputChange(event, apiType); } } hideDropdown(fieldId) { const dropdown = document.getElementById(`${fieldId}-dropdown`); if (dropdown) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; this.activeDropdowns.delete(fieldId); this.selectedIndex = -1; } } hideAllDropdowns() { this.activeDropdowns.forEach(fieldId => { this.hideDropdown(fieldId); }); } }