2020-07-31 17:52:26 +02:00
|
|
|
<template>
|
2022-07-12 10:55:28 +02:00
|
|
|
<div class="address-autocomplete">
|
|
|
|
<div class="">
|
|
|
|
<o-field
|
2021-10-10 16:25:50 +02:00
|
|
|
:label-for="id"
|
2020-07-31 17:52:26 +02:00
|
|
|
expanded
|
2021-10-10 16:25:50 +02:00
|
|
|
:message="fieldErrors"
|
2022-07-12 10:55:28 +02:00
|
|
|
:type="{ 'is-danger': fieldErrors }"
|
|
|
|
class="!-mt-2"
|
2022-08-26 16:08:58 +02:00
|
|
|
:labelClass="labelClass"
|
2020-07-31 17:52:26 +02:00
|
|
|
>
|
2022-07-12 10:55:28 +02:00
|
|
|
<template #label>
|
2021-10-10 16:25:50 +02:00
|
|
|
{{ actualLabel }}
|
2022-08-26 16:08:58 +02:00
|
|
|
<span v-if="gettingLocation">{{ t("Getting location") }}</span>
|
2020-07-31 17:52:26 +02:00
|
|
|
</template>
|
2022-07-12 10:55:28 +02:00
|
|
|
<p class="control" v-if="canShowLocateMeButton">
|
|
|
|
<o-loading
|
|
|
|
:full-page="false"
|
|
|
|
v-model:active="gettingLocation"
|
|
|
|
:can-cancel="false"
|
|
|
|
:container="mapMarker?.$el"
|
|
|
|
/>
|
|
|
|
<o-button
|
|
|
|
ref="mapMarker"
|
2021-11-08 18:46:04 +01:00
|
|
|
icon-right="map-marker"
|
|
|
|
@click="locateMe"
|
2022-07-12 10:55:28 +02:00
|
|
|
:title="t('Use my location')"
|
2021-11-08 18:46:04 +01:00
|
|
|
/>
|
|
|
|
</p>
|
2022-07-12 10:55:28 +02:00
|
|
|
<o-autocomplete
|
2021-10-10 16:25:50 +02:00
|
|
|
:data="addressData"
|
|
|
|
v-model="queryText"
|
2022-07-12 10:55:28 +02:00
|
|
|
:placeholder="placeholderWithDefault"
|
|
|
|
:customFormatter="(elem: IAddress) => addressFullName(elem)"
|
2021-10-10 16:25:50 +02:00
|
|
|
:loading="isFetching"
|
2022-07-12 10:55:28 +02:00
|
|
|
:debounceTyping="debounceDelay"
|
|
|
|
@typing="asyncData"
|
2021-11-08 18:46:04 +01:00
|
|
|
:icon="canShowLocateMeButton ? null : 'map-marker'"
|
2021-10-10 16:25:50 +02:00
|
|
|
expanded
|
|
|
|
@select="updateSelected"
|
|
|
|
:id="id"
|
2021-10-15 15:59:49 +02:00
|
|
|
:disabled="disabled"
|
2021-11-07 21:02:06 +01:00
|
|
|
dir="auto"
|
2022-07-12 10:55:28 +02:00
|
|
|
class="!mt-0"
|
2021-10-10 16:25:50 +02:00
|
|
|
>
|
|
|
|
<template #default="{ option }">
|
2022-07-12 10:55:28 +02:00
|
|
|
<o-icon :icon="option.poiInfos.poiIcon.icon" />
|
2021-10-10 16:25:50 +02:00
|
|
|
<b>{{ option.poiInfos.name }}</b
|
|
|
|
><br />
|
|
|
|
<small>{{ option.poiInfos.alternativeName }}</small>
|
|
|
|
</template>
|
2021-11-06 10:08:20 +01:00
|
|
|
<template #empty>
|
2022-07-12 10:55:28 +02:00
|
|
|
<span v-if="isFetching">{{ t("Searching…") }}</span>
|
2022-08-26 16:08:58 +02:00
|
|
|
<div v-else-if="queryText.length >= 3" class="enabled">
|
2021-10-10 16:25:50 +02:00
|
|
|
<span>{{
|
2022-07-12 10:55:28 +02:00
|
|
|
t('No results for "{queryText}"', { queryText })
|
2021-10-10 16:25:50 +02:00
|
|
|
}}</span>
|
|
|
|
<span>{{
|
2022-07-12 10:55:28 +02:00
|
|
|
t(
|
2021-10-10 16:25:50 +02:00
|
|
|
"You can try another search term or drag and drop the marker on the map",
|
|
|
|
{
|
|
|
|
queryText,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}}</span>
|
|
|
|
<!-- <p class="control" @click="openNewAddressModal">-->
|
2022-07-12 10:55:28 +02:00
|
|
|
<!-- <button type="button" class="button is-primary">{{ t('Add') }}</button>-->
|
2021-10-10 16:25:50 +02:00
|
|
|
<!-- </p>-->
|
|
|
|
</div>
|
|
|
|
</template>
|
2022-07-12 10:55:28 +02:00
|
|
|
</o-autocomplete>
|
|
|
|
<o-button
|
2021-10-10 16:25:50 +02:00
|
|
|
:disabled="!queryText"
|
|
|
|
@click="resetAddress"
|
|
|
|
class="reset-area"
|
|
|
|
icon-left="close"
|
2022-07-12 10:55:28 +02:00
|
|
|
:title="t('Clear address field')"
|
2021-10-10 16:25:50 +02:00
|
|
|
/>
|
2022-07-12 10:55:28 +02:00
|
|
|
</o-field>
|
2021-11-08 18:46:04 +01:00
|
|
|
<div
|
2022-07-12 10:55:28 +02:00
|
|
|
class="mt-2 p-2 rounded-lg shadow-md dark:bg-violet-3"
|
|
|
|
v-if="!hideSelected && (selected?.originId || selected?.url)"
|
2021-11-08 18:46:04 +01:00
|
|
|
>
|
2022-07-12 10:55:28 +02:00
|
|
|
<div class="">
|
2021-10-10 16:25:50 +02:00
|
|
|
<address-info
|
|
|
|
:address="selected"
|
|
|
|
:show-icon="true"
|
|
|
|
:show-timezone="true"
|
|
|
|
:user-timezone="userTimezone"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div
|
2022-07-12 10:55:28 +02:00
|
|
|
class="map"
|
2021-11-06 10:08:20 +01:00
|
|
|
v-if="!hideMap && selected && selected.geom && selected.poiInfos"
|
2021-10-10 16:25:50 +02:00
|
|
|
>
|
2020-07-31 17:52:26 +02:00
|
|
|
<map-leaflet
|
|
|
|
:coords="selected.geom"
|
|
|
|
:marker="{
|
|
|
|
text: [selected.poiInfos.name, selected.poiInfos.alternativeName],
|
|
|
|
icon: selected.poiInfos.poiIcon.icon,
|
|
|
|
}"
|
|
|
|
:updateDraggableMarkerCallback="reverseGeoCode"
|
|
|
|
:options="{ zoom: mapDefaultZoom }"
|
|
|
|
:readOnly="false"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
2022-07-12 10:55:28 +02:00
|
|
|
<script lang="ts" setup>
|
2022-08-19 15:07:58 +02:00
|
|
|
import { LatLng } from "leaflet";
|
2022-07-12 10:55:28 +02:00
|
|
|
import { Address, IAddress, addressFullName } from "../../types/address.model";
|
2021-10-10 16:25:50 +02:00
|
|
|
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
2022-07-12 10:55:28 +02:00
|
|
|
import { computed, ref, watch, defineAsyncComponent } from "vue";
|
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
import { useGeocodingAutocomplete } from "@/composition/apollo/config";
|
|
|
|
import { ADDRESS } from "@/graphql/address";
|
|
|
|
import { useReverseGeocode } from "@/composition/apollo/address";
|
|
|
|
import { useLazyQuery } from "@vue/apollo-composable";
|
2022-08-26 16:08:58 +02:00
|
|
|
const MapLeaflet = defineAsyncComponent(
|
|
|
|
() => import("@/components/LeafletMap.vue")
|
|
|
|
);
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
const props = withDefaults(
|
|
|
|
defineProps<{
|
|
|
|
modelValue: IAddress | null;
|
2022-08-26 16:08:58 +02:00
|
|
|
defaultText?: string | null;
|
2022-07-12 10:55:28 +02:00
|
|
|
label?: string;
|
2022-08-26 16:08:58 +02:00
|
|
|
labelClass?: string;
|
2022-07-12 10:55:28 +02:00
|
|
|
userTimezone?: string;
|
|
|
|
disabled?: boolean;
|
|
|
|
hideMap?: boolean;
|
|
|
|
hideSelected?: boolean;
|
|
|
|
placeholder?: string;
|
|
|
|
}>(),
|
|
|
|
{
|
2022-08-26 16:08:58 +02:00
|
|
|
labelClass: "",
|
|
|
|
defaultText: "",
|
2022-07-12 10:55:28 +02:00
|
|
|
disabled: false,
|
|
|
|
hideMap: false,
|
|
|
|
hideSelected: false,
|
2020-07-31 17:52:26 +02:00
|
|
|
}
|
2022-07-12 10:55:28 +02:00
|
|
|
);
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-09-20 16:53:26 +02:00
|
|
|
// const addressModalActive = ref(false);
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
const componentId = 0;
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
const emit = defineEmits(["update:modelValue"]);
|
|
|
|
|
|
|
|
const gettingLocationError = ref<string | null>(null);
|
|
|
|
const gettingLocation = ref(false);
|
|
|
|
const mapDefaultZoom = ref(15);
|
|
|
|
|
|
|
|
const addressData = ref<IAddress[]>([]);
|
|
|
|
|
|
|
|
const selected = ref<IAddress | null>(null);
|
|
|
|
|
|
|
|
const isFetching = ref(false);
|
|
|
|
|
|
|
|
const mapMarker = ref();
|
|
|
|
|
|
|
|
const placeholderWithDefault = computed(
|
|
|
|
() => props.placeholder ?? t("e.g. 10 Rue Jangot")
|
|
|
|
);
|
|
|
|
|
|
|
|
// created(): void {
|
|
|
|
// componentId += 1;
|
|
|
|
// }
|
|
|
|
|
|
|
|
const id = computed((): string => {
|
|
|
|
return `full-address-autocomplete-${componentId}`;
|
|
|
|
});
|
|
|
|
|
|
|
|
const modelValue = computed(() => props.modelValue);
|
|
|
|
|
|
|
|
watch(modelValue, () => {
|
|
|
|
if (!modelValue.value) return;
|
|
|
|
selected.value = modelValue.value;
|
|
|
|
});
|
|
|
|
|
|
|
|
const updateSelected = (option: IAddress): void => {
|
|
|
|
if (option == null) return;
|
|
|
|
selected.value = option;
|
|
|
|
emit("update:modelValue", selected.value);
|
|
|
|
};
|
|
|
|
|
2022-09-20 16:53:26 +02:00
|
|
|
// const resetPopup = (): void => {
|
|
|
|
// selected.value = new Address();
|
|
|
|
// };
|
2022-07-12 10:55:28 +02:00
|
|
|
|
2022-09-20 16:53:26 +02:00
|
|
|
// const openNewAddressModal = (): void => {
|
|
|
|
// resetPopup();
|
|
|
|
// addressModalActive.value = true;
|
|
|
|
// };
|
2022-07-12 10:55:28 +02:00
|
|
|
|
|
|
|
const checkCurrentPosition = (e: LatLng): boolean => {
|
|
|
|
if (!selected.value?.geom) return false;
|
|
|
|
const lat = parseFloat(selected.value?.geom.split(";")[1]);
|
|
|
|
const lon = parseFloat(selected.value?.geom.split(";")[0]);
|
|
|
|
|
|
|
|
return e.lat === lat && e.lng === lon;
|
|
|
|
};
|
|
|
|
|
|
|
|
const { t, locale } = useI18n({ useScope: "global" });
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
const actualLabel = computed((): string => {
|
2022-08-26 16:08:58 +02:00
|
|
|
return props.label ?? t("Find an address");
|
2022-07-12 10:55:28 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
|
|
const canShowLocateMeButton = computed((): boolean => {
|
|
|
|
return window.isSecureContext;
|
|
|
|
});
|
|
|
|
|
|
|
|
const { geocodingAutocomplete } = useGeocodingAutocomplete();
|
|
|
|
|
|
|
|
const debounceDelay = computed(() =>
|
|
|
|
geocodingAutocomplete.value === true ? 200 : 2000
|
|
|
|
);
|
|
|
|
|
|
|
|
const { onResult: onAddressSearchResult, load: searchAddress } = useLazyQuery<{
|
|
|
|
searchAddress: IAddress[];
|
|
|
|
}>(ADDRESS);
|
|
|
|
|
|
|
|
onAddressSearchResult((result) => {
|
|
|
|
if (result.loading) return;
|
|
|
|
const { data } = result;
|
|
|
|
addressData.value = data.searchAddress.map(
|
|
|
|
(address: IAddress) => new Address(address)
|
|
|
|
);
|
|
|
|
isFetching.value = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
const asyncData = async (query: string): Promise<void> => {
|
|
|
|
if (!query.length) {
|
|
|
|
addressData.value = [];
|
|
|
|
selected.value = new Address();
|
|
|
|
return;
|
2020-07-31 17:52:26 +02:00
|
|
|
}
|
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
if (query.length < 3) {
|
|
|
|
addressData.value = [];
|
|
|
|
return;
|
2020-07-31 17:52:26 +02:00
|
|
|
}
|
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
isFetching.value = true;
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
searchAddress(undefined, {
|
|
|
|
query,
|
|
|
|
locale: locale.value,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const queryText = computed({
|
|
|
|
get() {
|
2022-08-26 16:08:58 +02:00
|
|
|
return (
|
|
|
|
(selected.value ? addressFullName(selected.value) : props.defaultText) ??
|
|
|
|
""
|
|
|
|
);
|
2022-07-12 10:55:28 +02:00
|
|
|
},
|
|
|
|
set(text) {
|
|
|
|
if (text === "" && selected.value?.id) {
|
2022-08-26 16:08:58 +02:00
|
|
|
console.debug("doing reset");
|
2022-07-12 10:55:28 +02:00
|
|
|
resetAddress();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const resetAddress = (): void => {
|
|
|
|
emit("update:modelValue", null);
|
|
|
|
selected.value = new Address();
|
|
|
|
};
|
2020-07-31 17:52:26 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
const locateMe = async (): Promise<void> => {
|
|
|
|
gettingLocation.value = true;
|
|
|
|
gettingLocationError.value = null;
|
|
|
|
try {
|
|
|
|
const location = await getLocation();
|
|
|
|
mapDefaultZoom.value = 12;
|
|
|
|
reverseGeoCode(
|
|
|
|
new LatLng(location.coords.latitude, location.coords.longitude),
|
|
|
|
12
|
|
|
|
);
|
|
|
|
} catch (e: any) {
|
|
|
|
gettingLocationError.value = e.message;
|
2020-08-05 16:44:08 +02:00
|
|
|
}
|
2022-07-12 10:55:28 +02:00
|
|
|
gettingLocation.value = false;
|
|
|
|
};
|
2020-08-05 16:44:08 +02:00
|
|
|
|
2022-07-12 10:55:28 +02:00
|
|
|
const { onResult: onReverseGeocodeResult, load: loadReverseGeocode } =
|
|
|
|
useReverseGeocode();
|
|
|
|
|
|
|
|
onReverseGeocodeResult((result) => {
|
|
|
|
if (result.loading !== false) return;
|
|
|
|
const { data } = result;
|
|
|
|
addressData.value = data.reverseGeocode.map(
|
|
|
|
(elem: IAddress) => new Address(elem)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (addressData.value.length > 0) {
|
|
|
|
const defaultAddress = new Address(addressData.value[0]);
|
|
|
|
selected.value = defaultAddress;
|
|
|
|
emit("update:modelValue", selected.value);
|
2020-08-05 16:44:08 +02:00
|
|
|
}
|
2022-07-12 10:55:28 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const reverseGeoCode = (e: LatLng, zoom: number) => {
|
|
|
|
// If the position has been updated through autocomplete selection, no need to geocode it!
|
|
|
|
if (checkCurrentPosition(e)) return;
|
|
|
|
|
|
|
|
loadReverseGeocode(undefined, {
|
|
|
|
latitude: e.lat,
|
|
|
|
longitude: e.lng,
|
|
|
|
zoom,
|
|
|
|
locale: locale.value as string,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-undef
|
|
|
|
const getLocation = async (): Promise<GeolocationPosition> => {
|
|
|
|
let errorMessage = t("Failed to get location.");
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (!("geolocation" in navigator)) {
|
|
|
|
reject(new Error(errorMessage as string));
|
|
|
|
}
|
|
|
|
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
|
|
(pos) => {
|
|
|
|
resolve(pos);
|
|
|
|
},
|
|
|
|
(err) => {
|
|
|
|
switch (err.code) {
|
|
|
|
case GeolocationPositionError.PERMISSION_DENIED:
|
|
|
|
errorMessage = t("The geolocation prompt was denied.");
|
|
|
|
break;
|
|
|
|
case GeolocationPositionError.POSITION_UNAVAILABLE:
|
|
|
|
errorMessage = t("Your position was not available.");
|
|
|
|
break;
|
|
|
|
case GeolocationPositionError.TIMEOUT:
|
|
|
|
errorMessage = t("Geolocation was not determined in time.");
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
errorMessage = err.message;
|
|
|
|
}
|
|
|
|
reject(new Error(errorMessage as string));
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const fieldErrors = computed(() => {
|
|
|
|
return gettingLocationError.value;
|
|
|
|
});
|
2020-07-31 17:52:26 +02:00
|
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
|
|
.autocomplete {
|
|
|
|
.dropdown-menu {
|
|
|
|
z-index: 2000;
|
|
|
|
}
|
|
|
|
|
|
|
|
.dropdown-item.is-disabled {
|
|
|
|
opacity: 1 !important;
|
|
|
|
cursor: auto;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.read-only {
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
|
|
|
|
.map {
|
|
|
|
height: 400px;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
</style>
|