163 lines
4.5 KiB
Vue
163 lines
4.5 KiB
Vue
<script setup>
|
|
import { filterInputClass } from '@/Components/Supervisor/ui.js';
|
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
address: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
city: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
zipCode: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
required: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(['update:address', 'update:city', 'update:zipCode']);
|
|
|
|
const root = ref(null);
|
|
const query = ref(props.address ?? '');
|
|
const suggestions = ref([]);
|
|
const isOpen = ref(false);
|
|
const isLoading = ref(false);
|
|
const activeIndex = ref(-1);
|
|
|
|
let debounceTimer = null;
|
|
|
|
watch(
|
|
() => props.address,
|
|
(value) => {
|
|
if (value !== query.value) {
|
|
query.value = value ?? '';
|
|
}
|
|
},
|
|
);
|
|
|
|
const closeSuggestions = () => {
|
|
isOpen.value = false;
|
|
activeIndex.value = -1;
|
|
};
|
|
|
|
const searchAddresses = async () => {
|
|
if (query.value.trim().length < 3) {
|
|
suggestions.value = [];
|
|
closeSuggestions();
|
|
return;
|
|
}
|
|
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
const response = await window.axios.get(route('supervisor.organizations.addresses.search'), {
|
|
params: { q: query.value.trim() },
|
|
});
|
|
|
|
suggestions.value = response.data ?? [];
|
|
isOpen.value = suggestions.value.length > 0;
|
|
activeIndex.value = -1;
|
|
} catch {
|
|
suggestions.value = [];
|
|
closeSuggestions();
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const onInput = () => {
|
|
emit('update:address', query.value);
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(searchAddresses, 300);
|
|
};
|
|
|
|
const selectSuggestion = (suggestion) => {
|
|
query.value = suggestion.address;
|
|
emit('update:address', suggestion.address);
|
|
emit('update:city', suggestion.city ?? '');
|
|
emit('update:zipCode', suggestion.zip_code ?? '');
|
|
closeSuggestions();
|
|
};
|
|
|
|
const onKeydown = (event) => {
|
|
if (!isOpen.value || suggestions.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'ArrowDown') {
|
|
event.preventDefault();
|
|
activeIndex.value = (activeIndex.value + 1) % suggestions.value.length;
|
|
} else if (event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
activeIndex.value = activeIndex.value <= 0
|
|
? suggestions.value.length - 1
|
|
: activeIndex.value - 1;
|
|
} else if (event.key === 'Enter' && activeIndex.value >= 0) {
|
|
event.preventDefault();
|
|
selectSuggestion(suggestions.value[activeIndex.value]);
|
|
} else if (event.key === 'Escape') {
|
|
closeSuggestions();
|
|
}
|
|
};
|
|
|
|
const onClickOutside = (event) => {
|
|
if (root.value && !root.value.contains(event.target)) {
|
|
closeSuggestions();
|
|
}
|
|
};
|
|
|
|
onMounted(() => document.addEventListener('click', onClickOutside));
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', onClickOutside);
|
|
clearTimeout(debounceTimer);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="root" class="relative">
|
|
<input
|
|
v-model="query"
|
|
type="text"
|
|
autocomplete="off"
|
|
placeholder="Rechercher une adresse…"
|
|
:required="required"
|
|
:class="filterInputClass"
|
|
@input="onInput"
|
|
@focus="searchAddresses"
|
|
@keydown="onKeydown"
|
|
/>
|
|
|
|
<p v-if="isLoading" class="mt-1 text-xs text-slate-400">
|
|
Recherche en cours…
|
|
</p>
|
|
|
|
<ul
|
|
v-if="isOpen && suggestions.length > 0"
|
|
class="absolute z-20 mt-1 max-h-56 w-full overflow-y-auto rounded-xl border border-slate-200 bg-white py-1 shadow-lg"
|
|
>
|
|
<li
|
|
v-for="(suggestion, index) in suggestions"
|
|
:key="`${suggestion.label}-${index}`"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="block w-full px-3 py-2 text-left text-sm transition hover:bg-indigo-50"
|
|
:class="index === activeIndex ? 'bg-indigo-50 text-indigo-700' : 'text-slate-700'"
|
|
@mousedown.prevent="selectSuggestion(suggestion)"
|
|
>
|
|
<span class="block font-medium">{{ suggestion.address }}</span>
|
|
<span class="block text-xs text-slate-400">
|
|
{{ [suggestion.zip_code, suggestion.city].filter(Boolean).join(' ') }}
|
|
</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|