Source

components/shared/AddressAutocomplete/AddressAutocomplete.vue

<template lang="pug">
	.address_autocomplete
		Multiselect( 
			v-model="selectedPlace"
			label="name"
			track-by="code"
			:placeholder="getText(this.placeholder)"
			open-direction="bottom"
			:options="places"
			:multiple="false"
			:searchable="true"
			:loading="isLoading"
			:internal-search="false" 
			:clear-on-select="false"
			:close-on-select="true"
			:selectLabel="$t('geocoding.autocomplete.select_hint')"
			:deselectLabel="$t('geocoding.autocomplete.deselect_hint')"
			:selectedLabel="$t('geocoding.autocomplete.selected_label')"
			@search-change="asyncFind"
			@select="onPlaceSelected"
			v-bind="options"
			:class="theme"
			)
			template(slot="noResult")
				slot(name="noResult")
					span  {{getText(noResultText)}}
			template(slot="noOptions")
				slot(name="noOptions")
					//By default, do not display "List is empty" if data was not yet fetch
					div
			template(slot="singleLabel", slot-scope="props")
				slot(name="singleLabel" v-bind:item="props.option")
</template>
<script>
import Multiselect from 'vue-multiselect'
import { autocompleteProviders } from '@/services/geocoding-service.js'
import envService from '@/services/env-service.js'
/**
 *
 * Wrapper on top of Multiselect for Address autocompletation with OSM and custom geocoding APIs
 *
 * Note:
 * :internal-search="false" should be set to false, otherwise, async fetching does't work.
 *
 *   @vue-prop {String} value - Two-way-binding object (v-model) to store place details
 * @vue-prop {String} providerName - API to hit (OSM, OpenCage, GoogleMaps, Simpliciti APIs, etc)
 * @vue-prop {String} placeholder - Select placeholder (Uses i18n $t method if available)
 * @vue-prop {String} extraOptions - https://vue-multiselect.js.org/#sub-props
 * @namespace components
 * @category components
 * @subcategory shared/geocoding
 * @module AddressAutocomplete
 */
export default {
  name: 'AddressAutocomplete',
  components: { Multiselect },
  props: {
    value: {
      type: Object,
      default: () => ({}),
    },
    /**
     * osm: Hits OSM Nominatim directly
     * simplicti_v3: Proxy OSM Nominatim though our servers
     */
    providerName: {
      type: String,
      default: envService.getEnvValue(
        'VUE_APP_AUTOCOMPLETE_DEFAULT_PROVIDER',
        'simpliciti_v3'
      ),
      enum: autocompleteProviders,
    },
    placeholder: {
      type: String,
      default: 'shared.address_autocomplete.placeholder',
    },
    noResultText: {
      type: String,
      default: 'shared.address_autocomplete.no_results',
    },
    extraOptions: {
      type: Object,
      default: () => ({}),
    },
    theme: {
      type: String,
      default: 'theme-default',
    },
  },
  data() {
    return {
      options: {
        optionsLimit: 4,
        ...this.extraOptions,
      },
      selectedPlace: null,
      places: [],
      isLoading: false,
    }
  },
  computed: {
    providerURL() {
      return (this.providers[this.providerName] || {}).url || ''
    },
  },
  watch: {
    extraOptions() {
      this.options = { ...this.options, ...(this.extraOptions || {}) }
    },
    value() {
      this.updateFromValue()
    },
    /**
     * Handle deselect (two ways binding)
     */
    selectedPlace() {
      if (!this.selectedPlace) {
        this.$emit('input', {})
      }
    },
  },
  created() {
    if (!autocompleteProviders.includes(this.providerName)) {
      throw new Error(
        `Invalid provider '${
          this.providerName
        }' (Expected providers: ${autocompleteProviders.join(', ')})`
      )
    }
  },
  mounted() {
    this.updateFromValue()
  },
  methods: {
    /**
     * If no selection, try to forward geocoding the address given by v-model
     */
    updateFromValue() {
      //this.$log.debug('Address::updateFromValue::Check')
      if (
        this.selectedPlace === null &&
        Object.keys(this.value || {}).length > 0
      ) {
        //this.$log.debug('Address::updateFromValue::Find')
        this.asyncFind(this.value.formatted).then((places) => {
          //this.$log.debug('Address::updateFromValue::Places',places.length)
          if (places.length > 0) {
            this.selectedPlace = places[0]
            this.onPlaceSelected(this.selectedPlace)
          }
        })
      }
    },
    getText(text) {
      return this.$t && this.$te(text) ? this.$t(text) : text
    },
    limitText(count) {
      return `and ${count} other places`
    },
    asyncFind(query) {
      return new Promise((resolve, reject) => {
        this.isLoading = true
        this.$geocoding
          .autocompleteFetchPlaces(query, {
            provider: this.providerName,
          })
          .then((response) => {
            this.places = response
            this.isLoading = false
            resolve(this.places)
          })
      })
    },
    async onPlaceSelected(selectedPlace) {
      let placeDetails = await this.$geocoding.autocompleteFetchPlaceDetails(
        selectedPlace.code,
        {
          provider: this.providerName,
          formatted: selectedPlace.name,
        }
      )
      this.$emit('input', placeDetails)
      this.$emit('select', placeDetails)
    },
  },
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
/** Customize the styles by overriding multiselect library styles either here or in a wrapper component
	https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-multiselect.min.css
	*/

.address_autocomplete .multiselect.theme-simpliciti > .multiselect__tags {
  border: 0px;
}

.address_autocomplete .multiselect.theme-simpliciti > .multiselect__tags {
  border: 0px;
  border-bottom: 1px solid var(--color-slate-gray);
  border-radius: 0px;
}
.address_autocomplete .multiselect.theme-simpliciti > .multiselect__tags:focus,
.address_autocomplete
  .multiselect.theme-simpliciti
  > .multiselect__tags:active {
  outline: 0px;
  box-shadow: none;
  border-bottom: 1px solid var(--color-main);
}
</style>