Import initial : code en production sur Hetzner
This commit is contained in:
commit
cf6ee64e31
43 changed files with 14404 additions and 0 deletions
133
app/components/AddressField.vue
Normal file
133
app/components/AddressField.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import type { Place } from '#shared/types'
|
||||
|
||||
defineProps<{ label: string, id: string }>()
|
||||
const model = defineModel<Place | null>({ default: null })
|
||||
|
||||
const query = ref('')
|
||||
const results = ref<Place[]>([])
|
||||
const open = ref(false)
|
||||
const active = ref(-1)
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
/** Empeche une ecriture programmatique de relancer une recherche. */
|
||||
let skipNext = false
|
||||
|
||||
/**
|
||||
* Le champ doit refleter le modele meme quand il change de l'exterieur :
|
||||
* inversion depart/arrivee, trajet d'exemple, restauration d'etat.
|
||||
* Sans cela le texte affiche ment sur ce qui est reellement selectionne.
|
||||
*/
|
||||
watch(model, (m) => {
|
||||
const label = m?.label ?? ''
|
||||
if (label === query.value) return
|
||||
skipNext = true
|
||||
query.value = label
|
||||
}, { immediate: true })
|
||||
|
||||
watch(query, (q) => {
|
||||
if (skipNext) {
|
||||
skipNext = false
|
||||
return
|
||||
}
|
||||
|
||||
// Toute frappe invalide la selection : on ne calcule jamais un trajet
|
||||
// vers un lieu que l'utilisateur n'a pas explicitement confirme.
|
||||
if (model.value && q !== model.value.label) model.value = null
|
||||
|
||||
clearTimeout(timer)
|
||||
if (q.trim().length < 3) {
|
||||
results.value = []
|
||||
open.value = false
|
||||
return
|
||||
}
|
||||
|
||||
timer = setTimeout(async () => {
|
||||
results.value = await $fetch<Place[]>('/api/geocode', { query: { q } })
|
||||
open.value = results.value.length > 0
|
||||
active.value = -1
|
||||
}, 250)
|
||||
})
|
||||
|
||||
function select(place: Place) {
|
||||
model.value = place
|
||||
skipNext = place.label !== query.value
|
||||
query.value = place.label
|
||||
open.value = false
|
||||
active.value = -1
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (!open.value) return
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
active.value = (active.value + 1) % results.value.length
|
||||
}
|
||||
else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
active.value = active.value <= 0 ? results.value.length - 1 : active.value - 1
|
||||
}
|
||||
else if (e.key === 'Enter' && active.value >= 0) {
|
||||
e.preventDefault()
|
||||
select(results.value[active.value]!)
|
||||
}
|
||||
else if (e.key === 'Escape') {
|
||||
open.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Le delai laisse le mousedown de la liste s'executer avant la fermeture. */
|
||||
function onBlur() {
|
||||
setTimeout(() => { open.value = false }, 150)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<label :for="id" class="text-xs font-medium uppercase tracking-widest text-ink-soft">
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
:id="id"
|
||||
v-model="query"
|
||||
type="text"
|
||||
role="combobox"
|
||||
autocomplete="off"
|
||||
:aria-expanded="open"
|
||||
:aria-controls="`${id}-list`"
|
||||
:aria-activedescendant="active >= 0 ? `${id}-opt-${active}` : undefined"
|
||||
placeholder="Ville, gare ou adresse"
|
||||
class="mt-1 w-full border-b-2 bg-transparent py-2 text-lg outline-none"
|
||||
:class="model ? 'border-bike' : 'border-ink'"
|
||||
@keydown="onKeydown"
|
||||
@focus="open = results.length > 0"
|
||||
@blur="onBlur"
|
||||
>
|
||||
|
||||
<ul
|
||||
v-if="open"
|
||||
:id="`${id}-list`"
|
||||
role="listbox"
|
||||
class="absolute z-20 mt-1 w-full border-2 border-ink bg-paper shadow-[4px_4px_0_0_var(--color-ink)]"
|
||||
>
|
||||
<li
|
||||
v-for="(place, i) in results"
|
||||
:id="`${id}-opt-${i}`"
|
||||
:key="place.label"
|
||||
role="option"
|
||||
:aria-selected="i === active"
|
||||
class="cursor-pointer px-3 py-2 text-sm"
|
||||
:class="i === active ? 'bg-ink text-paper' : 'hover:bg-paper-2'"
|
||||
@mousedown.prevent="select(place)"
|
||||
>
|
||||
{{ place.label }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="sr-only" aria-live="polite">
|
||||
{{ open ? `${results.length} lieux proposés` : '' }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue