From d7eefdb6527a35653ceaf0b16ca37c7dc00669bc Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Tue, 16 Dec 2025 09:10:45 -0500 Subject: [PATCH] Lookup enhancements --- web/admin.css | 10 ++++ web/index.html | 4 +- web/lookups.js | 123 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/web/admin.css b/web/admin.css index b73a1bc..2729738 100644 --- a/web/admin.css +++ b/web/admin.css @@ -648,6 +648,16 @@ tbody tr:hover { background-color: #f8f9fa; } +.lookup-option-selected { + background-color: #e3f2fd; + border-left: 3px solid #2196f3; + padding-left: calc(0.5rem - 3px); +} + +.lookup-option-selected:hover { + background-color: #bbdefb; +} + .lookup-option:last-child { border-bottom: none; } diff --git a/web/index.html b/web/index.html index e60516d..d35a5bd 100644 --- a/web/index.html +++ b/web/index.html @@ -234,8 +234,8 @@ const data = JSON.parse(event.data); console.log('WebSocket message received:', data); - // Refresh display when any PPR-related or local flight event occurs - if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_'))) { + // Refresh display when any PPR-related, local flight, or departure event occurs + if (data.type && (data.type.includes('ppr_') || data.type === 'status_update' || data.type.includes('local_flight_') || data.type.includes('departure_'))) { console.log('Flight update detected, refreshing display...'); loadArrivals(); loadDepartures(); diff --git a/web/lookups.js b/web/lookups.js index e566ba6..7aefae7 100644 --- a/web/lookups.js +++ b/web/lookups.js @@ -2,6 +2,25 @@ * 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 @@ -19,11 +38,15 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) { }; 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(); @@ -36,6 +59,75 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) { }, 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 { @@ -71,9 +163,15 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) { const resultsDiv = document.getElementById(resultsId); if (config.isAircraft) { - // Aircraft lookup: auto-populate on single match, show message on multiple + // Aircraft lookup: auto-populate on single match, format input on no match if (!results || results.length === 0) { - resultsDiv.innerHTML = '
No matches found
'; + // 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]; @@ -108,18 +206,21 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) { `; } } else { - // Airport lookup: show list of options + // 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; } - const itemsToShow = results.slice(0, config.maxResults); - const matchText = itemsToShow.length === 1 ? 'Match found - click to select:' : 'Multiple matches found - select one:'; + 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}
`; - itemsToShow.forEach(item => { + currentResults.forEach((item, idx) => { html += `
${item.icao}
@@ -131,6 +232,9 @@ function createLookup(fieldId, resultsId, selectCallback, options = {}) { html += '
'; resultsDiv.innerHTML = html; + + // Attach keyboard handler (only once per lookup instance) + lookup.attachKeyboardHandler(); } }, @@ -238,6 +342,13 @@ function initializeLookups() { { isAircraft: true, minLength: 4, debounceMs: 300 } ); lookupManager.register('local-aircraft', localAircraftLookup); + + // Attach keyboard handlers to airport input fields + setTimeout(() => { + if (arrivalAirportLookup.attachKeyboardHandler) arrivalAirportLookup.attachKeyboardHandler(); + if (departureAirportLookup.attachKeyboardHandler) departureAirportLookup.attachKeyboardHandler(); + if (localOutToLookup.attachKeyboardHandler) localOutToLookup.attachKeyboardHandler(); + }, 100); } // Initialize on DOM ready or immediately if already loaded