191 lines
5.3 KiB
TypeScript
191 lines
5.3 KiB
TypeScript
import { ref, watch } from 'vue';
|
|
import { addressService, TownSuggestion, StreetSuggestion } from '../services/addressService';
|
|
|
|
/**
|
|
* Debounce utility function
|
|
*/
|
|
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
|
|
let timeout: ReturnType<typeof setTimeout>;
|
|
return ((...args: any[]) => {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => fn(...args), delay);
|
|
}) as T;
|
|
}
|
|
|
|
/**
|
|
* Composable for town autocomplete functionality
|
|
*/
|
|
export function useTownAutocomplete() {
|
|
const townSuggestions = ref<TownSuggestion[]>([]);
|
|
const isLoadingTowns = ref(false);
|
|
const showTownDropdown = ref(false);
|
|
const highlightedTownIndex = ref(-1);
|
|
const townError = ref('');
|
|
|
|
const searchTowns = debounce(async (query: string) => {
|
|
if (!query || query.length < 2) {
|
|
townSuggestions.value = [];
|
|
showTownDropdown.value = false;
|
|
return;
|
|
}
|
|
|
|
isLoadingTowns.value = true;
|
|
townError.value = '';
|
|
|
|
try {
|
|
const response = await addressService.searchTowns(query);
|
|
if (response.data.ok) {
|
|
townSuggestions.value = response.data.suggestions;
|
|
showTownDropdown.value = townSuggestions.value.length > 0;
|
|
highlightedTownIndex.value = -1;
|
|
}
|
|
} catch (error) {
|
|
console.error('Town search error:', error);
|
|
townError.value = 'Unable to search towns';
|
|
townSuggestions.value = [];
|
|
} finally {
|
|
isLoadingTowns.value = false;
|
|
}
|
|
}, 300);
|
|
|
|
const closeTownDropdown = () => {
|
|
// Delay to allow click events to fire
|
|
setTimeout(() => {
|
|
showTownDropdown.value = false;
|
|
highlightedTownIndex.value = -1;
|
|
}, 150);
|
|
};
|
|
|
|
const handleTownKeydown = (
|
|
event: KeyboardEvent,
|
|
onSelect: (suggestion: TownSuggestion) => void
|
|
) => {
|
|
if (!showTownDropdown.value || townSuggestions.value.length === 0) return;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
highlightedTownIndex.value = Math.min(
|
|
highlightedTownIndex.value + 1,
|
|
townSuggestions.value.length - 1
|
|
);
|
|
break;
|
|
case 'ArrowUp':
|
|
event.preventDefault();
|
|
highlightedTownIndex.value = Math.max(highlightedTownIndex.value - 1, 0);
|
|
break;
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
if (highlightedTownIndex.value >= 0) {
|
|
onSelect(townSuggestions.value[highlightedTownIndex.value]);
|
|
} else if (townSuggestions.value.length === 1) {
|
|
onSelect(townSuggestions.value[0]);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
showTownDropdown.value = false;
|
|
highlightedTownIndex.value = -1;
|
|
break;
|
|
}
|
|
};
|
|
|
|
return {
|
|
townSuggestions,
|
|
isLoadingTowns,
|
|
showTownDropdown,
|
|
highlightedTownIndex,
|
|
townError,
|
|
searchTowns,
|
|
closeTownDropdown,
|
|
handleTownKeydown,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Composable for street/address autocomplete functionality
|
|
*/
|
|
export function useStreetAutocomplete() {
|
|
const streetSuggestions = ref<StreetSuggestion[]>([]);
|
|
const isLoadingStreets = ref(false);
|
|
const showStreetDropdown = ref(false);
|
|
const highlightedStreetIndex = ref(-1);
|
|
const streetError = ref('');
|
|
|
|
const searchStreets = debounce(async (town: string, state: string, query: string) => {
|
|
if (!town || !state || !query || query.length < 1) {
|
|
streetSuggestions.value = [];
|
|
showStreetDropdown.value = false;
|
|
return;
|
|
}
|
|
|
|
isLoadingStreets.value = true;
|
|
streetError.value = '';
|
|
|
|
try {
|
|
const response = await addressService.searchStreets(town, state, query);
|
|
if (response.data.ok) {
|
|
streetSuggestions.value = response.data.suggestions;
|
|
showStreetDropdown.value = streetSuggestions.value.length > 0;
|
|
highlightedStreetIndex.value = -1;
|
|
}
|
|
} catch (error) {
|
|
console.error('Street search error:', error);
|
|
streetError.value = 'Unable to search addresses';
|
|
streetSuggestions.value = [];
|
|
} finally {
|
|
isLoadingStreets.value = false;
|
|
}
|
|
}, 300);
|
|
|
|
const closeStreetDropdown = () => {
|
|
setTimeout(() => {
|
|
showStreetDropdown.value = false;
|
|
highlightedStreetIndex.value = -1;
|
|
}, 150);
|
|
};
|
|
|
|
const handleStreetKeydown = (
|
|
event: KeyboardEvent,
|
|
onSelect: (suggestion: StreetSuggestion) => void
|
|
) => {
|
|
if (!showStreetDropdown.value || streetSuggestions.value.length === 0) return;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
highlightedStreetIndex.value = Math.min(
|
|
highlightedStreetIndex.value + 1,
|
|
streetSuggestions.value.length - 1
|
|
);
|
|
break;
|
|
case 'ArrowUp':
|
|
event.preventDefault();
|
|
highlightedStreetIndex.value = Math.max(highlightedStreetIndex.value - 1, 0);
|
|
break;
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
if (highlightedStreetIndex.value >= 0) {
|
|
onSelect(streetSuggestions.value[highlightedStreetIndex.value]);
|
|
} else if (streetSuggestions.value.length === 1) {
|
|
onSelect(streetSuggestions.value[0]);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
showStreetDropdown.value = false;
|
|
highlightedStreetIndex.value = -1;
|
|
break;
|
|
}
|
|
};
|
|
|
|
return {
|
|
streetSuggestions,
|
|
isLoadingStreets,
|
|
showStreetDropdown,
|
|
highlightedStreetIndex,
|
|
streetError,
|
|
searchStreets,
|
|
closeStreetDropdown,
|
|
handleStreetKeydown,
|
|
};
|
|
}
|