Files
ppr-ng/web/lookups.js

504 lines
18 KiB
JavaScript

/**
* 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 = `
<div class="aircraft-match">
${aircraft.manufacturer_name || ''} ${aircraft.model || aircraft.type_code || ''}
</div>
`;
// 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 = `
<div class="aircraft-no-match">
Multiple matches found (${results.length}) - please be more specific
</div>
`;
}
} else {
// Airport lookup: show list of options with keyboard navigation
if (!results || results.length === 0) {
resultsDiv.innerHTML = '<div class="lookup-no-match">No matches found - will use as entered</div>';
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 = `<div class="lookup-no-match" style="margin-bottom: 0.5rem;">${matchText}</div><div class="lookup-list">`;
currentResults.forEach((item, idx) => {
html += `
<div class="lookup-option" onclick="lookupManager.selectItem('${resultsId}', '${fieldId}', '${item.icao}')">
<div class="lookup-code">${item.icao}</div>
<div class="lookup-name">${item.name || '-'}</div>
${item.city ? `<div class="lookup-location">${item.city}, ${item.country}</div>` : ''}
</div>
`;
});
html += '</div>';
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 = '<div class="lookup-searching">Searching...</div>';
}
},
// Show error state
showError: () => {
const resultsDiv = document.getElementById(resultsId);
if (resultsDiv) {
resultsDiv.innerHTML = '<div class="lookup-no-match">Lookup failed - will use as entered</div>';
}
},
// 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);
}