Skip to content
Snippets Groups Projects
Geocoder.vue 7.68 KiB
Newer Older
DESPRES Damien's avatar
DESPRES Damien committed
<template>
Florent Lavelle's avatar
Florent Lavelle committed
  <div
    id="geocoder-container"
    :class="{ isExpanded }"
Florent Lavelle's avatar
Florent Lavelle committed
  >
    <button
      class="button-geocoder"
      title="Rechercher une adresse"
      type="button"
      @click="toggleGeocoder"
Florent Lavelle's avatar
Florent Lavelle committed
    >
      <i class="search icon" />
    </button>
    <!-- internal-search should be disabled to avoid filtering options, which is done by the api calls anyway https://stackoverflow.com/questions/57813170/vue-multi-select-not-showing-all-the-options -->
    <!-- otherwise approximate results are not shown (cannot explain why though) -->
DESPRES Damien's avatar
DESPRES Damien committed
    <Multiselect
Florent Lavelle's avatar
Florent Lavelle committed
      v-if="isExpanded"
DESPRES Damien's avatar
DESPRES Damien committed
      v-model="selection"
Florent Lavelle's avatar
Florent Lavelle committed
      class="expanded-geocoder"
DESPRES Damien's avatar
DESPRES Damien committed
      :options="addresses"
DESPRES Damien's avatar
DESPRES Damien committed
      :allow-empty="true"
      :internal-search="false"
      track-by="id"
DESPRES Damien's avatar
DESPRES Damien committed
      label="label"
      :show-labels="false"
      :reset-after="true"
      select-label=""
      selected-label=""
      deselect-label=""
      :searchable="true"
      :placeholder="placeholder"
      :show-no-results="true"
      :loading="loading"
      :clear-on-select="false"
      :preserve-search="true"
      @search-change="search"
      @select="select"
      @open="retrievePreviousPlaces"
DESPRES Damien's avatar
DESPRES Damien committed
      @close="close"
    >
      <template
        slot="option"
        slot-scope="props"
      >
        <div class="option__desc">
          <span class="option__title">{{ props.option.label }}</span>
        </div>
      </template>
      <template slot="clear">
        <div
          v-if="selection"
          class="multiselect__clear"
          @click.prevent.stop="selection = null"
        >
          <i class="close icon" />
        </div>
      </template>
      <span slot="noResult">
        Aucun résultat.
      </span>
      <span slot="noOptions">
        Saisissez les premiers caractères ...
      </span>
    </Multiselect>
    <div style="display: none;">
      <div
        id="marker"
        title="Marker"
      />
    </div>
  </div>
</template>

<script>
import Multiselect from 'vue-multiselect';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import axios from 'axios';
import mapService from '@/services/map-service';
const apiAdressAxios = axios.create({
  baseURL: 'https://api-adresse.data.gouv.fr',
  withCredentials: false,
});
DESPRES Damien's avatar
DESPRES Damien committed
export default {
  name: 'Geocoder',
DESPRES Damien's avatar
DESPRES Damien committed
  components: {
    Multiselect
  },
DESPRES Damien's avatar
DESPRES Damien committed
  data() {
    return {
      loading: false,
DESPRES Damien's avatar
DESPRES Damien committed
      selection: null,
      text: null,
      selectedAddress: null,
      addresses: [],
      resultats: [],
Florent Lavelle's avatar
Florent Lavelle committed
      placeholder: 'Rechercher une adresse ...',
      isExpanded: false
DESPRES Damien's avatar
DESPRES Damien committed
  mounted() {
    this.addressTextChange = new Subject();
    this.addressTextChange.pipe(debounceTime(200)).subscribe((res) => this.getAddresses(res));
  },
DESPRES Damien's avatar
DESPRES Damien committed
  methods: {
    toggleGeocoder() {
      this.isExpanded = !this.isExpanded;
      if (this.isExpanded) {
        this.retrievePreviousPlaces();
        this.$nextTick(()=> this.$refs.multiselect.activate());
      }
    },

DESPRES Damien's avatar
DESPRES Damien committed
    getAddresses(query){
      if (query.length < 3) {
        this.addresses = [];
        return;
      }
      const coords = mapService.getMapCenter();
      let url = `https://api-adresse.data.gouv.fr/search/?q=${query}&limit=${this.limit}`;
      if (coords) url += `&lon=${coords[0]}&lat=${coords[1]}`;
      apiAdressAxios.get(url)
DESPRES Damien's avatar
DESPRES Damien committed
        .then((retour) => {
          this.resultats = retour.data.features;
          this.addresses = retour.data.features.map(x=>x.properties);
DESPRES Damien's avatar
DESPRES Damien committed
        });
    },
DESPRES Damien's avatar
DESPRES Damien committed
    selectAddresse(event) {
      this.selectedAddress = event;
      if (this.selectedAddress !== null && this.selectedAddress.geometry) {
        let zoomlevel = 14;
        const { type } = this.selectedAddress.properties;
DESPRES Damien's avatar
DESPRES Damien committed
        if (type === 'housenumber') {
          zoomlevel = 19;
        } else if (type === 'street') {
          zoomlevel = 16;
        } else if (type === 'locality') {
          zoomlevel = 16;
        }
        // On ajoute un point pour localiser la ville
        mapService.addOverlay(this.selectedAddress.geometry.coordinates, zoomlevel);
        // On enregistre l'adresse sélectionné pour le proposer à la prochaine recherche
        this.setLocalstorageSelectedAdress(this.selectedAddress);
DESPRES Damien's avatar
DESPRES Damien committed
    search(text) {
      this.text = text;
      this.addressTextChange.next(this.text);
    },
DESPRES Damien's avatar
DESPRES Damien committed
    select(e) {
      this.selectAddresse(this.resultats.find(x=>x.properties.label === e.label));
DESPRES Damien's avatar
DESPRES Damien committed
      this.$emit('select', e);
    },
DESPRES Damien's avatar
DESPRES Damien committed
    close() {
      this.$emit('close', this.selection);
    },

    setLocalstorageSelectedAdress(newAdress) {
      let selectedAdresses = JSON.parse(localStorage.getItem('geocontrib-selected-adresses'));
      selectedAdresses = Array.isArray(selectedAdresses) ? selectedAdresses : [];
      selectedAdresses = [ newAdress, ...selectedAdresses ];

      const uniqueLabels = [...new Set(selectedAdresses.map(el => el.properties.label))];
      const uniqueAdresses = uniqueLabels.map((label) => {
        return selectedAdresses.find(adress => adress.properties.label === label);
      });

      localStorage.setItem(
        'geocontrib-selected-adresses',
        JSON.stringify(uniqueAdresses.slice(0, 5))
      );
    },

    getLocalstorageSelectedAdress() {
      return JSON.parse(localStorage.getItem('geocontrib-selected-adresses')) || [];
    },

    retrievePreviousPlaces() {
      const previousAdresses = this.getLocalstorageSelectedAdress();
      if (previousAdresses.length > 0) {
        this.addresses = previousAdresses.map(x=>x.properties);
        this.resultats = previousAdresses;
      }
    },
DESPRES Damien's avatar
DESPRES Damien committed
  }
};
</script>

    width: 14px;
    height: 14px;
    border: 2px solid #fff;
    border-radius: 7px;
    background-color: #3399CC;
DESPRES Damien's avatar
DESPRES Damien committed
  position: absolute;
  // each button have (more or less depends on borders) .5em space between
  // zoom buttons are 60px high, geolocation and full screen button is 34px high with borders
  top: calc(1.6em + 60px + 34px + 34px);
DESPRES Damien's avatar
DESPRES Damien committed
  pointer-events: auto;
Florent Lavelle's avatar
Florent Lavelle committed
  border: 2px solid rgba(0,0,0,.2);
  background-clip: padding-box;
  padding: 0;
Florent Lavelle's avatar
Florent Lavelle committed
  display: flex;

  .button-geocoder {
    border: none;
    padding: 0;
    margin: 0;
    text-align: center;
    background-color: #fff;
    color: rgb(39, 39, 39);
Florent Lavelle's avatar
Florent Lavelle committed
    font: 700 18px Lucida Console,Monaco,monospace;
    border-radius: 2px;
    line-height: 1.15;

    i {
      margin: 0;
      font-size: 0.9em;
    }
  }
  .button-geocoder:hover {
    cursor: pointer;
    background-color: #ebebeb;
  }

  .expanded-geocoder {
    max-width: 400px;
  }

  &&.isExpanded {
    .button-geocoder {
      height: 41px;
      color: rgb(99, 99, 99);
      border-radius: 2px 0 0 2px;
    }
  // /* keep placeholder width when opening dropdown */
  .multiselect {
    min-width: 208px;
  }
  /* keep font-weight from overide of semantic classes */
  .multiselect__placeholder, .multiselect__content, .multiselect__tags  {
    font-weight: initial !important;
  }
  /* keep placeholder eigth */
  .multiselect .multiselect__placeholder {
    margin-bottom: 9px !important;
    padding-top: 1px;
  }
  /* keep placeholder height when opening dropdown without selection */
  input.multiselect__input {
    padding: 3px 0 0 0 !important;
  }
  /* keep placeholder height when opening dropdown with already a value selected */
  .multiselect__tags .multiselect__single {
    padding: 1px 0 0 0 !important;
    margin-bottom: 9px;
  }
  .multiselect__tags {
    border: 0 !important;
    min-height: 41px !important;
  }
  .multiselect input {
    line-height: 1em !important;
    padding: 0 !important;
  }
  .multiselect__content-wrapper {
    border: 2px solid rgba(0,0,0,.2);
  }
DESPRES Damien's avatar
DESPRES Damien committed
}
</style>