<template> <div id="geocoder-container" :class="{ isExpanded }" > <button class="button-geocoder" title="Rechercher une adresse" type="button" @click="toggleGeocoder" > <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) --> <Multiselect v-if="isExpanded" ref="multiselect" v-model="selection" class="expanded-geocoder" :options="addresses" :options-limit="limit" :allow-empty="true" :internal-search="false" track-by="id" 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" @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, }); export default { name: 'Geocoder', components: { Multiselect }, data() { return { loading: false, limit: 10, selection: null, text: null, selectedAddress: null, addresses: [], resultats: [], placeholder: 'Rechercher une adresse ...', isExpanded: false }; }, mounted() { this.addressTextChange = new Subject(); this.addressTextChange.pipe(debounceTime(200)).subscribe((res) => this.getAddresses(res)); }, methods: { toggleGeocoder() { this.isExpanded = !this.isExpanded; if (this.isExpanded) { this.retrievePreviousPlaces(); this.$nextTick(()=> this.$refs.multiselect.activate()); } }, 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) .then((retour) => { this.resultats = retour.data.features; this.addresses = retour.data.features.map(x=>x.properties); }); }, selectAddresse(event) { this.selectedAddress = event; if (this.selectedAddress !== null && this.selectedAddress.geometry) { let zoomlevel = 14; const { type } = this.selectedAddress.properties; 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); } }, search(text) { this.text = text; this.addressTextChange.next(this.text); }, select(e) { this.selectAddresse(this.resultats.find(x=>x.properties.label === e.label)); this.$emit('select', e); }, 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; } }, } }; </script> <style lang="less"> #marker { width: 14px; height: 14px; border: 2px solid #fff; border-radius: 7px; background-color: #3399CC; } #geocoder-container { position: absolute; right: 6px; // 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); pointer-events: auto; z-index: 999; border: 2px solid rgba(0,0,0,.2); background-clip: padding-box; padding: 0; border-radius: 4px; display: flex; .button-geocoder { border: none; padding: 0; margin: 0; text-align: center; background-color: #fff; color: rgb(39, 39, 39); width: 30px; height: 30px; 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); } } </style>