Covivo/mobicoop

View on GitHub
client/src/MobicoopBundle/Resources/assets/js/components/utilities/geography/Geocomplete.vue

Summary

Maintainability
Test Coverage
<template>
  <div>
    <v-autocomplete
      v-model="selection"
      :label="label + (required ? ' *' : '')"
      :search-input.sync="search"
      :items="propositions"
      hide-no-data
      no-filter
      :required="required"
      aria-required="true"
      :hint="hint"
      :rules="rules"
      :aria-label="ariaLabel"
      :loading="loading"
      return-object
      :clearable="!chip"
      :prepend-inner-icon="prependIcon"
      @change="change"
    >
      <template
        v-if="chip"
        v-slot:selection="data"
      >
        <template>
          <v-chip
            class="ma-2"
            style="max-width: 95%;"
            :color="chipColor(data.item.type)"
            :text-color="chipTextColor(data.item.type)"
            close
            label
            @click:close="clearSelection"
          >
            <v-img
              v-if="data.item.customIcon"
              contain
              :src="data.item.customIcon"
              class="mr-1"
              max-height="25"
              max-width="25"
            />
            <v-icon
              v-else-if="data.item.icon"
              left
            >
              {{
                data.item.icon
              }}
            </v-icon>
            <v-icon
              v-else
              left
            >
              mdi-earth
            </v-icon>
            <span
              class="chip-overflow font-weight-medium"
            >{{ data.item.text }}
            </span>
            <country-flag
              v-if="data.item.value.countryCode && data.item.value.countryCode != country"
              :country="data.item.value.countryCode"
              size="small"
            />
          </v-chip>
        </template>
      </template>

      <template
        v-else
        v-slot:selection="data"
      >
        {{ data.item.text }}
        <country-flag
          v-if="data.item.value.countryCode && data.item.value.countryCode != country"
          :country="data.item.value.countryCode"
          size="small"
        />
      </template>
      <template
        v-if="!chip && selection"
        v-slot:prepend-inner
      >
        <v-icon
          v-if="selection.icon"
          :color="noChipColor(selection.type)"
          left
        >
          {{
            selection.icon
          }}
        </v-icon>
        <v-icon
          v-else
          left
        >
          mdi-earth
        </v-icon>
      </template>
      <!-- template for list items  -->
      <template v-slot:item="data">
        <template>
          <v-img
            v-if="data.item.customIcon"
            contain
            :src="data.item.customIcon"
            class="mr-1"
            max-height="40"
            max-width="40"
          />

          <v-list-item-avatar
            v-else-if="data.item.icon"
            :class="iconColor(data.item.type)"
          >
            <v-icon
              :class="iconTextColor(data.item.type)"
            >
              {{ data.item.icon }}
            </v-icon>
          </v-list-item-avatar>

          <v-list-item-content>
            <v-list-item-title :class="titleColor(data.item.type)">
              {{ data.item.propositionTitle }}
              <country-flag
                v-if="data.item.value.countryCode && data.item.value.countryCode != country"
                :country="data.item.value.countryCode"
                size="small"
              />
            </v-list-item-title>
            <v-list-item-subtitle
              :class="subTitleColor(data.item.type)"
              v-text="data.item.propositionText"
            />
          </v-list-item-content>
        </template>
      </template>
    </v-autocomplete>
  </div>
</template>

<script>
import axios from "axios";
import { debounce } from "lodash";
import CountryFlag from "vue-country-flag";

import {messages_en, messages_fr, messages_eu, messages_nl} from "@translations/components/utilities/geography/Geocomplete/";

export default {
  name: "Geocomplete",

  components: {
    CountryFlag,
  },
  i18n: {
    messages: {
      'en': messages_en,
      'nl': messages_nl,
      'fr': messages_fr,
      'eu':messages_eu
    }
  },

  props: {
    chip: {
      type: Boolean,
      default: false
    },
    uri: {
      type: String,
      default: null
    },
    label: {
      type: String,
      default: null
    },
    hint: {
      type: String,
      default: null
    },
    address: {
      type: Object,
      default: null
    },
    required:  {
      type: Boolean,
      default: false
    },
    prependIcon:{
      type: String,
      default: null
    },
    country: {
      type: String,
      default: 'FR'
    },
    showName: {
      type: Boolean,
      default: true
    },
    resultsOrder: {
      type: Array,
      default: () => []
    },
    restrict: {
      type: Array,
      default: () => []
    },
    palette: {
      type: Object,
      default: () => ({})
    },
    ariaLabel: {
      type: String,
      default: null
    }
  },

  data: () => ({
    search: null,
    items: [],
    selection: null,
    loading: null
  }),

  computed: {
    sort() {
      if (this.resultsOrder.length > 0) return this.resultsOrder;
      return [
        "user",
        "relaypoint",
        "locality",
        "housenumber",
        "street",
        "venue",
        "event",
        "other"
      ];
    },
    defaultPalette() {
      if (Object.keys(this.palette).length > 0) return this.palette;
      return {
        nochip: "black",
        locality: {
          main: "indigo",
          text: "white",
        },
        street: {
          main: "deep-purple",
          text: "white",
        },
        housenumber: {
          main: "purple",
          text: "white",
        },
        venue: {
          main: "pink",
          text: "white",
        },
        other: {
          main: "teal",
          text: "white",
        },
        relaypoint: {
          main: "teal",
          text: "white",
        },
        user: {
          main: "teal",
          text: "white",
        },
        event: {
          main: "teal",
          text: "white",
        }
      };
    },
    colors() {
      return {
        locality: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.locality.main,
          "chip-text": this.defaultPalette.locality.text,
          icon: this.defaultPalette.locality.main+" accent-2",
          "icon-text": this.defaultPalette.locality.text+"--text",
          title: this.defaultPalette.locality.main+"--text text--darken-3",
          subtitle: this.defaultPalette.locality.main+"--text text--lighten-1",
        },
        street: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.street.main,
          "chip-text": this.defaultPalette.street.text,
          icon: this.defaultPalette.street.main+" accent-2",
          "icon-text": this.defaultPalette.street.text+"--text",
          title: this.defaultPalette.street.main+"--text text--darken-3",
          subtitle: this.defaultPalette.street.main+"--text text--lighten-1",
        },
        housenumber: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.housenumber.main,
          "chip-text": this.defaultPalette.housenumber.text,
          icon: this.defaultPalette.housenumber.main+" accent-2",
          "icon-text": this.defaultPalette.housenumber.text+"--text",
          title: this.defaultPalette.housenumber.main+"--text text--darken-3",
          subtitle: this.defaultPalette.housenumber.main+"--text text--lighten-1",
        },
        venue: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.venue.main,
          "chip-text": this.defaultPalette.venue.text,
          icon: this.defaultPalette.venue.main+" accent-2",
          "icon-text": this.defaultPalette.venue.text+"--text",
          title: this.defaultPalette.venue.main+"--text text--darken-3",
          subtitle: this.defaultPalette.venue.main+"--text text--lighten-1",
        },
        other: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.other.main,
          "chip-text": this.defaultPalette.other.text,
          icon: this.defaultPalette.other.main+" accent-2",
          "icon-text": this.defaultPalette.other.text+"--text",
          title: this.defaultPalette.other.main+"--text text--darken-3",
          subtitle: this.defaultPalette.other.main+"--text text--lighten-1",
        },
        relaypoint: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.relaypoint.main,
          "chip-text": this.defaultPalette.relaypoint.text,
          icon: this.defaultPalette.relaypoint.main+" accent-2",
          "icon-text": this.defaultPalette.relaypoint.text+"--text",
          title: this.defaultPalette.relaypoint.main+"--text text--darken-3",
          subtitle: this.defaultPalette.relaypoint.main+"--text text--lighten-1",
        },
        user: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.user.main,
          "chip-text": this.defaultPalette.user.text,
          icon: this.defaultPalette.user.main+" accent-2",
          "icon-text": this.defaultPalette.user.text+"--text",
          title: this.defaultPalette.user.main+"--text text--darken-3",
          subtitle: this.defaultPalette.user.main+"--text text--lighten-1",
        },
        event: {
          "no-chip": this.defaultPalette.nochip,
          chip: this.defaultPalette.event.main,
          "chip-text": this.defaultPalette.event.text,
          icon: this.defaultPalette.event.main+" accent-2",
          "icon-text": this.defaultPalette.event.text+"--text",
          title: this.defaultPalette.event.main+"--text text--darken-3",
          subtitle: this.defaultPalette.event.main+"--text text--lighten-1",
        }
      };
    },
    rules() {
      if (this.required) {
        return [
          v => !!v || this.$t('required')
        ];
      }
      return [];
    },
    propositions() {
      return this.sort
        .map( (type) => this.propositionsForType(type))
        .filter( (type) => type !== null)
        .flat();
    },
    selectionToAddress() {
      if (this.selection) {
        const address = {
          "houseNumber":this.selection.value.houseNumber,
          "street":this.selection.value.streetName,
          "postalCode":this.selection.value.postalCode,
          "addressLocality":this.selection.value.locality,
          "region":this.selection.value.region,
          "regionCode":this.selection.value.regionCode,
          "macroRegion":this.selection.value.macroRegion,
          "addressCountry":this.selection.value.country,
          "countryCode":this.selection.value.countryCode,
          "latitude":this.selection.value.lat,
          "longitude":this.selection.value.lon,
          "name":this.selection.value.name,
          "providedBy":this.selection.value.provider,
          "distance":this.selection.value.distance,
          "type":this.selection.type,
          "id":this.selection.value.id,
          "displayLabel":this.selection.value.displayLabel ? this.selection.value.displayLabel : null
        };
        if (this.selection.type == "event") {
          // so nice...
          address.event = {"id": this.selection.value.id, "name": this.selection.value.name}
        }
        return address;
      }
      return null;
    },
    addressToSelection() {
      if (this.address)
        return  {
          "houseNumber":this.address.houseNumber,
          "streetName":this.address.street,
          "postalCode":this.address.postalCode,
          "locality":this.address.addressLocality,
          "region":this.address.region,
          "macroRegion":this.address.macroRegion,
          "country":this.address.addressCountry,
          "countryCode":this.address.countryCode,
          "lat":this.address.latitude,
          "lon":this.address.longitude,
          "name":this.address.event ? this.address.event.name : this.address.name,
          "provider":this.address.providedBy,
          "distance":this.address.distance,
          "type":this.address.type ? this.address.type : "other",
          "id":this.address.id,
          "regionCode":this.address.regionCode,
          "displayLabel":this.address.displayLabel ? this.address.displayLabel : null
        };
      return null;
    }
  },

  watch: {
    address: {
      immediate: true,
      handler() {
        if(this.address) this.setSelection();
      }
    },
    search(val) {
      if (val) {
        this.debouncedSearch();
      } else if (val === "") {
        this.clearPropositions();
      }
    },
  },

  created() {
    this.debouncedSearch = debounce(this.getPropositions, 500);
  },

  methods: {
    propositionsForType(type) {
      const propositions = this.items
        .filter((item) => item.type == type)
        .map((item) => this.createProposition(item));
      if (propositions.length>0) {
        return [
          { divider: true },
          { header: this.$t(type) },
          ...propositions
        ];
      }
      return null;
    },
    createProposition(item) {
      return {
        text: this.selectionText(item),
        propositionTitle: this.propositionTitle(item),
        propositionText: this.propositionText(item),
        value: item,
        group: this.$t(item.type),
        type: item.type,
        icon: this.getIcon(item.type),
        customIcon: (item.icon && item.icon.url) ? item.icon.url : null
      }
    },
    selectionText(item) {
      let text = '';
      if (item.type == 'street') {
        text += item.streetName + ', ';
        if (item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'housenumber') {
        text += item.houseNumber + ', ' + item.streetName + ', ';
        if (item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'venue' || item.type == 'event') {
        text += item.name + ', ';
        if (item.houseNumber) text += item.houseNumber + ', ';
        if (item.streetName) text += item.streetName + ', ';
        if (item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'user') {
        if (item.houseNumber) text += item.houseNumber + ', ';
        if (item.streetName) text += item.streetName + ', ';
        if (this.showName && item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'relaypoint') {
        if (item.name) text += item.name + ', ';
        if (item.houseNumber) text += item.houseNumber + ', ';
        if (item.streetName) text += item.streetName + ', ';
        if (item.postalCode && item.streetName) text += item.postalCode + ', ';
      }
      if (item.locality) text += item.locality;
      if (item.type == 'locality' && item.regionCode !== null)
        text += ', ' + item.regionCode;
      if (item.type == 'locality' && item.countryCode !== this.country)
        text += ', ' + item.country;
      if (item.type == 'relaypoint' && text == '') {
        if(item.displayLabel && item.displayLabel.length > 0 && item.displayLabel[0] !== ""){
          text = item.displayLabel[0];
        }
        else{
          text = this.$t('gps') + ' [' + item.lat + ', ' + item.lon + ']';
        }
      }
      if (text.slice(text.length - 2) == ', ') {
        text = text.slice(0, -2);
      }
      return text;
    },
    propositionTitle(item) {
      let text = '';
      if (item.type == 'street') {
        text += item.streetName + ', ';
        if (item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'housenumber') {
        text += item.houseNumber + ', ' + item.streetName + ', ';
        if (item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'venue' || item.type == 'event') {
        text += item.name + ', ';
        if (item.houseNumber) text += item.houseNumber + ', ';
        if (item.streetName) text += item.streetName + ', ';
        if (item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'user') {
        if (this.showName) text += item.name + ', ';
        if (item.houseNumber) text += item.houseNumber + ', ';
        if (item.streetName) text += item.streetName + ', ';
        if (this.showName && item.postalCode) text += item.postalCode + ', ';
      }
      if (item.type == 'relaypoint') {
        if (item.name) text += item.name + ', ';
        if (item.houseNumber) text += item.houseNumber + ', ';
        if (item.streetName) text += item.streetName + ', ';
        if (item.postalCode && item.streetName) text += item.postalCode + ', ';
      }
      if (item.locality) text += item.locality;
      if (item.type == 'relaypoint' && text == '') {
        if(item.displayLabel && item.displayLabel.length > 0 && item.displayLabel[0] !== ""){
          text = item.displayLabel[0];
        }
        else{
          text = this.$t('gps') + ' [' + item.lat + ', ' + item.lon + ']';
        }
      }
      if (text.slice(text.length - 2) == ', ') {
        text = text.slice(0, -2);
      }
      return text;
    },
    propositionText(item) {
      let text = '';
      if (item.regionCode) text += item.regionCode;
      if (item.region) text += (text != '' ? ', ' : '') + item.region;
      if (item.macroRegion)
        text += (text != '' ? ', ' : '') + item.macroRegion;
      if (item.country) text += (text != '' ? ', ' : '') + item.country;
      return text;
    },
    getIcon(type) {
      switch(type) {
      case "locality" : return "mdi-city-variant";
      case "street" : return "mdi-road-variant";
      case "housenumber" : return "mdi-home-map-marker";
      case "venue" : return "mdi-map-marker";
      case "relaypoint" : return "mdi-parking";
      case "user" : return "mdi-home-heart";
      case "event" : return "mdi-stadium-variant";
      }
      return "mdi-earth";
    },
    setSelection() {
      if (!this.address) {
        this.clearSelection();
      } else if (!this.selection || !(
        this.selection.id === this.address.id &&
        this.selection.type === this.address.type
      )) {
        this.selection = this.createProposition(this.addressToSelection);
        this.items = [this.selection.value];
      }
    },
    getPropositions() {
      if (
        this.search &&
        (
          !this.selection ||
          (
            this.selection &&
            this.selection.text &&
            this.selection.text !== this.search
          )
        )
      ) {
        this.clearPropositions();
        this.loading = true;
        axios
          .get(this.uri + "?search=" + this.search, {
            headers: { Authorization: 'Bearer ' + this.$store.getters['a/token'] },
          })
          .then((response) => {
            this.setItems(response.data["hydra:member"]);
          })
          .catch(error => {
            if (error.response) {
              switch (error.response.status) {
              case 401:
                if (error.response.data.message == 'Expired JWT Token') {
                  return this.refreshToken()
                    .then( () => {
                      this.getPropositions();
                    });
                }
              }
            }
            this.clearPropositions();
          })
          .finally(() => {
            this.loading = false;
          });
      }
    },
    clearPropositions() {
      this.items = [];
      this.$emit('clear');
    },
    clearSelection() {
      this.selection = null;
      this.change();
    },
    setItems(items) {
      if (this.restrict.length == 0) {
        this.items = items;
      } else {
        this.items = items.filter((item) =>
          this.restrict.includes(item.type)
        );
      }
    },
    change(address) {
      this.$emit("address-selected", this.selectionToAddress);
      if (!address) this.clearPropositions();
    },
    noChipColor(type) {
      return this.colors[type]["no-chip"];
    },
    chipColor(type) {
      return this.colors[type]["chip"];
    },
    chipTextColor(type) {
      return this.colors[type]["chip-text"];
    },
    iconColor(type) {
      return this.colors[type]["icon"];
    },
    iconTextColor(type) {
      return this.colors[type]["icon-text"];
    },
    titleColor(type) {
      return this.colors[type]["title"];
    },
    subTitleColor(type) {
      return this.colors[type]["subtitle"];
    },
    refreshToken() {
      return axios
        .post('/refreshToken')
        .then( response => {
          if (response.data.token) {
            this.$store.commit('a/setToken',response.data.token);
          }
          return Promise.resolve();
        })
        .catch( error => {
          return Promise.reject(error);
        });
    }
  },
};
</script>

<style lang="scss" scoped>
.chip-overflow {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.small-flag {
  margin-left:-0.5em !important;
}
</style>