Address.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <template>
  2. <div>
  3. <v-autocomplete
  4. v-model="model"
  5. :loading="loading"
  6. :items="items"
  7. :search-input.sync="search"
  8. hide-no-data
  9. hide-details
  10. return-object
  11. auto-select-first
  12. clearable
  13. no-filter
  14. :label="$t('where') + ' ?'"
  15. outlined
  16. append-icon="mdi-crosshairs-gps"
  17. @click="onClick"
  18. @change="$emit('change', $event ? $event.value : '')"
  19. @click:append="geolocalizeUser"
  20. />
  21. <v-snackbar :value="errorMsg !== ''">
  22. {{ errorMsg }}
  23. <template #action="{ attrs }">
  24. <v-btn text v-bind="attrs" @click="errorMsg=''">
  25. {{ $t('close') }}
  26. </v-btn>
  27. </template>
  28. </v-snackbar>
  29. </div>
  30. </template>
  31. <script lang="ts">
  32. import Vue from 'vue'
  33. /**
  34. * Address properties as returned by the https://geo.api.gouv.fr/adresse API
  35. */
  36. interface AddressProperties {
  37. label: string,
  38. score: number,
  39. housenumber: string,
  40. id: string,
  41. name: string,
  42. postcode: string,
  43. citycode: string,
  44. x: number,
  45. y: number,
  46. city: string,
  47. district: string,
  48. context: string,
  49. type: 'housenumber' | 'street' | 'locality' | 'municipality'
  50. importance: number,
  51. street: string
  52. }
  53. /**
  54. * Localized addresses as returned by the https://geo.api.gouv.fr/adresse API
  55. */
  56. interface Feature {
  57. type: 'Feature',
  58. geometry: { type: 'Point', coordinates: [ number, number ]},
  59. properties: AddressProperties
  60. }
  61. export default Vue.extend({
  62. props: {
  63. value: {
  64. type: String,
  65. required: false,
  66. default: ''
  67. },
  68. type: {
  69. type: String,
  70. required: false,
  71. default: 'housenumber',
  72. validator (value: string) {
  73. return ['housenumber', 'street', 'locality', 'municipality'].includes(value)
  74. }
  75. },
  76. limit: {
  77. type: Number,
  78. required: false,
  79. default: 10
  80. },
  81. autocomplete: {
  82. type: Boolean,
  83. required: false,
  84. default: true
  85. }
  86. },
  87. data () {
  88. return {
  89. model: null as UiSearchAddressItem | null,
  90. search: null,
  91. features: [] as Array<Feature>,
  92. items: [] as Array<UiSearchAddressItem>,
  93. loading: false,
  94. errorMsg: '',
  95. geolocalized: false
  96. }
  97. },
  98. watch: {
  99. search (val: string) {
  100. if (this.geolocalized) {
  101. return
  102. }
  103. if (!val) {
  104. this.features = []
  105. return
  106. }
  107. this.loading = true
  108. // Lazily load input items
  109. let apiUrl = 'https://api-adresse.data.gouv.fr/search/' +
  110. `?type=${this.type}` +
  111. `&autocomplete=${this.autocomplete ? 1 : 0}` +
  112. `&q=${val.replace(/[\s&?]/gi, '+')}`
  113. if (this.limit > 0) {
  114. apiUrl += `&limit=${this.limit}`
  115. }
  116. fetch(encodeURI(apiUrl))
  117. .then(res => res.json())
  118. .then(({ features }) => {
  119. this.items = features.map((f: Feature) => {
  120. return {
  121. text: f.properties.name + ' (' + f.properties.postcode + ')',
  122. value: { longitude: f.geometry.coordinates[0] as number, latitude: f.geometry.coordinates[1] as number },
  123. disabled: false
  124. }
  125. })
  126. })
  127. .catch((err) => {
  128. // eslint-disable-next-line no-console
  129. console.error(err)
  130. })
  131. .finally(() => {
  132. this.loading = false
  133. })
  134. }
  135. },
  136. methods: {
  137. clear () {
  138. this.model = null
  139. this.search = null
  140. this.items = []
  141. this.loading = false
  142. this.errorMsg = ''
  143. this.geolocalized = false
  144. },
  145. onClick () {
  146. this.clear()
  147. },
  148. geolocalizeUser () {
  149. if (navigator.geolocation) {
  150. navigator.geolocation.getCurrentPosition(
  151. (position: { coords: { longitude: number, latitude: number }}) => {
  152. this.clear()
  153. const loc = {
  154. text: this.$t('my_position') as string,
  155. value: { longitude: position.coords.longitude, latitude: position.coords.latitude },
  156. disabled: false
  157. }
  158. this.geolocalized = true
  159. this.items = [loc]
  160. this.model = loc
  161. this.$emit('change', loc.value)
  162. },
  163. () => {
  164. this.errorMsg = this.$t('geolocation_error') as string
  165. }
  166. )
  167. } else {
  168. this.errorMsg = this.$t('geolocation_not_supported') as string
  169. }
  170. }
  171. }
  172. })
  173. </script>
  174. <style>
  175. .v-input__control {
  176. height: 56px;
  177. }
  178. </style>