/** * Lookup Utilities - Reusable functions for aircraft and airport lookups */ /** * Format aircraft registration based on UK rules * - 5 alphabetic chars: add hyphen after first char (GIVYY -> G-IVYY) * - Otherwise: just uppercase (N123AD -> N123AD) */ function formatAircraftRegistration(input) { if (!input) return ''; const cleaned = input.trim().toUpperCase(); // If exactly 5 characters and all alphabetic, add hyphen if (cleaned.length === 5 && /^[A-Z]{5}$/.test(cleaned)) { return cleaned[0] + '-' + cleaned.substring(1); } // Otherwise just return uppercase version return cleaned; } /** * Creates a reusable lookup handler * @param {string} fieldId - ID of the input field * @param {string} resultsId - ID of the results container * @param {function} selectCallback - Function to call when item is selected * @param {object} options - Additional options (minLength, debounceMs, etc.) */ function createLookup(fieldId, resultsId, selectCallback, options = {}) { const defaults = { minLength: 2, debounceMs: 300, isAirport: false, isAircraft: false, maxResults: 10 }; const config = { ...defaults, ...options }; let debounceTimeout; let currentResults = []; let selectedIndex = -1; let keydownHandlerAttached = false; const lookup = { // Main handler called by oninput handle: (value) => { clearTimeout(debounceTimeout); selectedIndex = -1; // Reset selection on new input if (!value || value.trim().length < config.minLength) { lookup.clear(); return; } lookup.showSearching(); debounceTimeout = setTimeout(() => { lookup.perform(value); }, config.debounceMs); }, // Attach keyboard handler once (for airport lookups) attachKeyboardHandler: () => { if (config.isAirport && !keydownHandlerAttached) { try { const inputField = document.getElementById(fieldId); if (inputField) { inputField.addEventListener('keydown', (e) => lookup.handleKeydown(e)); keydownHandlerAttached = true; } } catch (error) { console.error('Error attaching keyboard handler:', error); } } }, // Handle keyboard events handleKeydown: (event) => { if (!currentResults || currentResults.length === 0) return; if (event.key === 'ArrowDown') { event.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, currentResults.length - 1); lookup.updateSelection(); } else if (event.key === 'ArrowUp') { event.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, -1); lookup.updateSelection(); } else if (event.key === 'Enter') { event.preventDefault(); if (selectedIndex >= 0 && currentResults[selectedIndex]) { lookup.selectResult(currentResults[selectedIndex]); } else if (currentResults.length === 1) { // Auto-select if only one result and Enter pressed lookup.selectResult(currentResults[0]); } } else if (event.key === 'Escape') { lookup.clear(); selectedIndex = -1; } }, // Update visual selection updateSelection: () => { const resultsDiv = document.getElementById(resultsId); if (!resultsDiv) return; const options = resultsDiv.querySelectorAll('.lookup-option'); options.forEach((opt, idx) => { if (idx === selectedIndex) { opt.classList.add('lookup-option-selected'); opt.scrollIntoView({ block: 'nearest' }); } else { opt.classList.remove('lookup-option-selected'); } }); }, // Select a result item selectResult: (item) => { const field = document.getElementById(fieldId); if (field) { field.value = item.icao; } lookup.clear(); currentResults = []; selectedIndex = -1; if (selectCallback) selectCallback(item.icao); }, // Perform the lookup perform: async (searchTerm) => { try { const cleanInput = searchTerm.trim(); let endpoint; if (config.isAircraft) { const cleaned = cleanInput.replace(/[^a-zA-Z0-9]/g, '').toUpperCase(); if (cleaned.length < config.minLength) { lookup.clear(); return; } endpoint = `/api/v1/aircraft/lookup/${cleaned}`; } else if (config.isAirport) { endpoint = `/api/v1/airport/lookup/${encodeURIComponent(cleanInput)}`; } if (!endpoint) throw new Error('Invalid lookup type'); const response = await authenticatedFetch(endpoint); if (!response.ok) throw new Error('Lookup failed'); const results = await response.json(); lookup.display(results, cleanInput); } catch (error) { console.error('Lookup error:', error); lookup.showError(); } }, // Display results display: (results, searchTerm) => { const resultsDiv = document.getElementById(resultsId); if (config.isAircraft) { // Aircraft lookup: auto-populate on single match, format input on no match if (!results || results.length === 0) { // Format the aircraft registration and auto-populate const formatted = formatAircraftRegistration(searchTerm); const field = document.getElementById(fieldId); if (field) { field.value = formatted; } resultsDiv.innerHTML = ''; // Clear results, field is auto-populated } else if (results.length === 1) { // Single match - auto-populate const aircraft = results[0]; resultsDiv.innerHTML = `
✓ ${aircraft.manufacturer_name || ''} ${aircraft.model || aircraft.type_code || ''}
`; // Auto-populate the form fields const field = document.getElementById(fieldId); if (field) field.value = aircraft.registration; // Also populate type field let typeFieldId; if (fieldId === 'ac_reg') { typeFieldId = 'ac_type'; } else if (fieldId === 'local_registration') { typeFieldId = 'local_type'; } else if (fieldId === 'book_in_registration') { typeFieldId = 'book_in_type'; } else if (fieldId === 'overflight_registration') { typeFieldId = 'overflight_type'; } if (typeFieldId) { const typeField = document.getElementById(typeFieldId); if (typeField) typeField.value = aircraft.type_code || ''; } } else { // Multiple matches resultsDiv.innerHTML = `
Multiple matches found (${results.length}) - please be more specific
`; } } else { // Airport lookup: show list of options with keyboard navigation if (!results || results.length === 0) { resultsDiv.innerHTML = '
No matches found - will use as entered
'; currentResults = []; return; } currentResults = results.slice(0, config.maxResults); selectedIndex = -1; // Reset selection when showing new results const matchText = currentResults.length === 1 ? 'Match found - press ENTER or click to select:' : 'Multiple matches found - use arrow keys and ENTER to select:'; let html = `
${matchText}
`; currentResults.forEach((item, idx) => { html += `
${item.icao}
${item.name || '-'}
${item.city ? `
${item.city}, ${item.country}
` : ''}
`; }); html += '
'; resultsDiv.innerHTML = html; // Attach keyboard handler (only once per lookup instance) lookup.attachKeyboardHandler(); } }, // Show searching state showSearching: () => { const resultsDiv = document.getElementById(resultsId); if (resultsDiv) { resultsDiv.innerHTML = '
Searching...
'; } }, // Show error state showError: () => { const resultsDiv = document.getElementById(resultsId); if (resultsDiv) { resultsDiv.innerHTML = '
Lookup failed - will use as entered
'; } }, // Clear results clear: () => { const resultsDiv = document.getElementById(resultsId); if (resultsDiv) { resultsDiv.innerHTML = ''; } }, // Set the selected value setValue: (value) => { const field = document.getElementById(fieldId); if (field) { field.value = value; } lookup.clear(); if (selectCallback) selectCallback(value); } }; return lookup; } /** * Global lookup manager for all lookups on the page */ const lookupManager = { lookups: {}, // Register a lookup instance register: (name, lookup) => { lookupManager.lookups[name] = lookup; }, // Generic item selection handler selectItem: (resultsId, fieldId, itemCode) => { const field = document.getElementById(fieldId); if (field) { field.value = itemCode; } const resultsDiv = document.getElementById(resultsId); if (resultsDiv) { resultsDiv.innerHTML = ''; } } }; // Initialize all lookups when page loads function initializeLookups() { // Create reusable lookup instances const arrivalAirportLookup = createLookup( 'in_from', 'arrival-airport-lookup-results', null, { isAirport: true, minLength: 2 } ); lookupManager.register('arrival-airport', arrivalAirportLookup); const departureAirportLookup = createLookup( 'out_to', 'departure-airport-lookup-results', null, { isAirport: true, minLength: 2 } ); lookupManager.register('departure-airport', departureAirportLookup); const localOutToLookup = createLookup( 'local_out_to', 'local-out-to-lookup-results', null, { isAirport: true, minLength: 2 } ); lookupManager.register('local-out-to', localOutToLookup); const aircraftLookup = createLookup( 'ac_reg', 'aircraft-lookup-results', null, { isAircraft: true, minLength: 4, debounceMs: 300 } ); lookupManager.register('aircraft', aircraftLookup); const localAircraftLookup = createLookup( 'local_registration', 'local-aircraft-lookup-results', null, { isAircraft: true, minLength: 4, debounceMs: 300 } ); lookupManager.register('local-aircraft', localAircraftLookup); const bookInAircraftLookup = createLookup( 'book_in_registration', 'book-in-aircraft-lookup-results', null, { isAircraft: true, minLength: 4, debounceMs: 300 } ); lookupManager.register('book-in-aircraft', bookInAircraftLookup); const bookInArrivalAirportLookup = createLookup( 'book_in_from', 'book-in-arrival-airport-lookup-results', null, { isAirport: true, minLength: 2 } ); lookupManager.register('book-in-arrival-airport', bookInArrivalAirportLookup); const overflightAircraftLookup = createLookup( 'overflight_registration', 'overflight-aircraft-lookup-results', null, { isAircraft: true, minLength: 4, debounceMs: 300 } ); lookupManager.register('overflight-aircraft', overflightAircraftLookup); const overflightDepartureLookup = createLookup( 'overflight_departure_airfield', 'overflight-departure-airport-lookup-results', null, { isAirport: true, minLength: 2 } ); lookupManager.register('overflight-departure', overflightDepartureLookup); const overflightDestinationLookup = createLookup( 'overflight_destination_airfield', 'overflight-destination-airport-lookup-results', null, { isAirport: true, minLength: 2 } ); lookupManager.register('overflight-destination', overflightDestinationLookup); // Attach keyboard handlers to airport input fields setTimeout(() => { if (arrivalAirportLookup.attachKeyboardHandler) arrivalAirportLookup.attachKeyboardHandler(); if (departureAirportLookup.attachKeyboardHandler) departureAirportLookup.attachKeyboardHandler(); if (localOutToLookup.attachKeyboardHandler) localOutToLookup.attachKeyboardHandler(); if (bookInArrivalAirportLookup.attachKeyboardHandler) bookInArrivalAirportLookup.attachKeyboardHandler(); if (overflightDepartureLookup.attachKeyboardHandler) overflightDepartureLookup.attachKeyboardHandler(); if (overflightDestinationLookup.attachKeyboardHandler) overflightDestinationLookup.attachKeyboardHandler(); }, 100); } // Initialize on DOM ready or immediately if already loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeLookups); } else { initializeLookups(); } /** * Convenience wrapper functions for backward compatibility */ function handleArrivalAirportLookup(value) { const lookup = lookupManager.lookups['arrival-airport']; if (lookup) lookup.handle(value); } function handleDepartureAirportLookup(value) { const lookup = lookupManager.lookups['departure-airport']; if (lookup) lookup.handle(value); } function handleLocalOutToAirportLookup(value) { const lookup = lookupManager.lookups['local-out-to']; if (lookup) lookup.handle(value); } function handleAircraftLookup(value) { const lookup = lookupManager.lookups['aircraft']; if (lookup) lookup.handle(value); } function handleLocalAircraftLookup(value) { const lookup = lookupManager.lookups['local-aircraft']; if (lookup) lookup.handle(value); } function clearArrivalAirportLookup() { const lookup = lookupManager.lookups['arrival-airport']; if (lookup) lookup.clear(); } function clearDepartureAirportLookup() { const lookup = lookupManager.lookups['departure-airport']; if (lookup) lookup.clear(); } function clearLocalOutToAirportLookup() { const lookup = lookupManager.lookups['local-out-to']; if (lookup) lookup.clear(); } function clearAircraftLookup() { const lookup = lookupManager.lookups['aircraft']; if (lookup) lookup.clear(); } function clearLocalAircraftLookup() { const lookup = lookupManager.lookups['local-aircraft']; if (lookup) lookup.clear(); } function selectArrivalAirport(icaoCode) { lookupManager.selectItem('arrival-airport-lookup-results', 'in_from', icaoCode); } function selectDepartureAirport(icaoCode) { lookupManager.selectItem('departure-airport-lookup-results', 'out_to', icaoCode); } function selectLocalOutToAirport(icaoCode) { lookupManager.selectItem('local-out-to-lookup-results', 'local_out_to', icaoCode); } function selectLocalAircraft(registration) { lookupManager.selectItem('local-aircraft-lookup-results', 'local_registration', registration); } function handleBookInAircraftLookup(value) { const lookup = lookupManager.lookups['book-in-aircraft']; if (lookup) lookup.handle(value); } function handleBookInArrivalAirportLookup(value) { const lookup = lookupManager.lookups['book-in-arrival-airport']; if (lookup) lookup.handle(value); } function clearBookInAircraftLookup() { const lookup = lookupManager.lookups['book-in-aircraft']; if (lookup) lookup.clear(); } function clearBookInArrivalAirportLookup() { const lookup = lookupManager.lookups['book-in-arrival-airport']; if (lookup) lookup.clear(); } function selectBookInAircraft(registration) { lookupManager.selectItem('book-in-aircraft-lookup-results', 'book_in_registration', registration); } function selectBookInArrivalAirport(icaoCode) { lookupManager.selectItem('book-in-arrival-airport-lookup-results', 'book_in_from', icaoCode); }