mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 14:26:43 +00:00
333 lines
9.8 KiB
Vue
333 lines
9.8 KiB
Vue
<script lang="ts" setup>
|
|
import type { GeoLocationType } from 'nocodb-sdk'
|
|
import { TypeConversionError } from '~/error/type-conversion.error'
|
|
|
|
interface Props {
|
|
modelValue?: string | null
|
|
}
|
|
|
|
interface Emits {
|
|
(event: 'update:modelValue', model: GeoLocationType): void
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emits = defineEmits<Emits>()
|
|
|
|
const column = inject(ColumnInj)
|
|
|
|
const vModel = useVModel(props, 'modelValue', emits)
|
|
|
|
const activeCell = inject(ActiveCellInj, ref(false))
|
|
|
|
const isPublic = inject(IsPublicInj, ref(false))
|
|
|
|
const readonly = inject(ReadonlyInj, ref(false))
|
|
|
|
const isExpanded = ref(false)
|
|
|
|
const isLoading = ref(false)
|
|
|
|
const identifier = computed(() => {
|
|
return {
|
|
latitude: Math.random().toString(36).substring(2, 15),
|
|
longitude: Math.random().toString(36).substring(2, 15),
|
|
}
|
|
})
|
|
|
|
const isLocationSet = computed(() => {
|
|
return !!vModel.value
|
|
})
|
|
|
|
const [latitude, longitude] = (vModel.value || '').split(';')
|
|
|
|
const latLongStr = computed(() => {
|
|
const [latitude, longitude] = (vModel.value || '').split(';')
|
|
return latitude && longitude ? `${latitude}; ${longitude}` : 'Set location'
|
|
})
|
|
|
|
const formState = reactive({
|
|
latitude,
|
|
longitude,
|
|
})
|
|
|
|
const handleFinish = () => {
|
|
vModel.value = latLongToJoinedString(parseFloat(formState.latitude), parseFloat(formState.longitude))
|
|
isExpanded.value = false
|
|
}
|
|
|
|
const clear = () => {
|
|
isExpanded.value = false
|
|
|
|
formState.latitude = latitude
|
|
formState.longitude = longitude
|
|
}
|
|
|
|
const onClickSetCurrentLocation = () => {
|
|
isLoading.value = true
|
|
const onSuccess: PositionCallback = (position: GeolocationPosition) => {
|
|
const crd = position.coords
|
|
formState.latitude = `${convertGeoNumberToString(crd.latitude)}`
|
|
formState.longitude = `${convertGeoNumberToString(crd.longitude)}`
|
|
isLoading.value = false
|
|
}
|
|
|
|
const onError: PositionErrorCallback = (err: GeolocationPositionError) => {
|
|
console.error(`ERROR(${err.code}): ${err.message}`)
|
|
isLoading.value = false
|
|
}
|
|
|
|
const options = {
|
|
enableHighAccuracy: true,
|
|
timeout: 20000,
|
|
maximumAge: 2000,
|
|
}
|
|
navigator.geolocation.getCurrentPosition(onSuccess, onError, options)
|
|
}
|
|
|
|
const openInGoogleMaps = () => {
|
|
const [latitude, longitude] = (vModel.value || '').split(';')
|
|
const url = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`
|
|
window.open(url, '_blank', 'noopener,noreferrer')
|
|
}
|
|
|
|
const openInOSM = () => {
|
|
const [latitude, longitude] = (vModel.value || '').split(';')
|
|
const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`
|
|
window.open(url, '_blank', "'noopener,noreferrer'")
|
|
}
|
|
|
|
const handleClose = (e: MouseEvent) => {
|
|
if (e.target instanceof HTMLElement && !e.target.closest('.nc-geodata-picker-overlay')) {
|
|
isExpanded.value = false
|
|
}
|
|
}
|
|
|
|
useEventListener(document, 'click', handleClose, true)
|
|
|
|
const handlePaste = (e: ClipboardEvent) => {
|
|
if ([identifier.value.latitude, identifier.value.longitude].includes(e.target?.id)) {
|
|
return
|
|
}
|
|
const clipboardData = e.clipboardData?.getData('text/plain') || ''
|
|
if (isExpanded.value) {
|
|
try {
|
|
const value = convertCellData(
|
|
{
|
|
value: clipboardData,
|
|
to: column.value.uidt,
|
|
column: column.value,
|
|
},
|
|
false,
|
|
)
|
|
if (value) {
|
|
formState.latitude = value.split(';')[0]
|
|
formState.longitude = value.split(';')[1]
|
|
}
|
|
} catch (ex) {
|
|
if (ex instanceof TypeConversionError !== true) {
|
|
throw ex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
|
|
const isCanvasInjected = inject(IsCanvasInjectionInj, false)
|
|
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
|
|
const isGrid = inject(IsGridInj, ref(false))
|
|
|
|
const isForm = inject(IsFormInj, ref(false))
|
|
|
|
const handleBlur = (e: Event) => {
|
|
const originalValue = (e.target as any).value as string
|
|
const value = convertGeoNumberToString(Number(originalValue))
|
|
if (value !== originalValue) {
|
|
if ((e.target as any)!.id === identifier.value.latitude) {
|
|
formState.latitude = value
|
|
} else if ((e.target as any)!.id === identifier.value.longitude) {
|
|
formState.longitude = value
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (!isUnderLookup.value && isCanvasInjected && !isExpandedForm.value && isGrid.value) {
|
|
forcedNextTick(() => {
|
|
isExpanded.value = true
|
|
})
|
|
}
|
|
})
|
|
|
|
watch(
|
|
() => vModel,
|
|
(oldValue, newValue) => {
|
|
if (newValue.value) {
|
|
formState.latitude = newValue.value?.split(';')[0]
|
|
formState.longitude = newValue.value?.split(';')[1]
|
|
} else {
|
|
formState.latitude = ''
|
|
formState.longitude = ''
|
|
}
|
|
},
|
|
)
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && isExpanded.value) {
|
|
e.preventDefault()
|
|
e.stopImmediatePropagation()
|
|
isExpanded.value = false
|
|
}
|
|
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
isExpanded.value = !isExpanded.value
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div tabindex="0" class="focus-visible:outline-none" @paste="handlePaste" @keydown="handleKeyDown">
|
|
<NcDropdown v-model:visible="isExpanded" class="w-[27.25rem]">
|
|
<div
|
|
v-if="!isLocationSet"
|
|
:class="{
|
|
'!justify-start !ml-0 ': isExpandedForm || isForm,
|
|
'mt-0.5': isForm && !isPublic,
|
|
'!-mt-0.25': isForm && isPublic,
|
|
}"
|
|
class="w-full flex justify-center max-w-64 mx-auto"
|
|
>
|
|
<NcButton
|
|
v-if="(activeCell && !readonly) || isForm"
|
|
size="xsmall"
|
|
type="secondary"
|
|
data-testid="nc-geo-data-set-location-button"
|
|
>
|
|
<div class="flex items-center px-2 gap-2">
|
|
<GeneralIcon class="text-gray-500 h-3.5 w-3.5" icon="ncMapPin" />
|
|
<span class="text-tiny">
|
|
{{ latLongStr }}
|
|
</span>
|
|
</div>
|
|
</NcButton>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
data-testid="nc-geo-data-lat-long-set"
|
|
tabindex="1"
|
|
:class="{
|
|
'!py-1': !isForm,
|
|
'pt-1': isForm && !isPublic,
|
|
}"
|
|
class="nc-cell-field h-full w-full flex items-center focus-visible:!outline-none focus:!outline-none"
|
|
>
|
|
{{ latLongStr }}
|
|
</div>
|
|
<template #overlay>
|
|
<div class="flex rounded-md nc-geodata-picker-overlay py-3" @click.stop @paste="handlePaste">
|
|
<a-form layout="vertical" :model="formState" class="flex flex-col" @finish="handleFinish">
|
|
<a-row class="flex gap-3 px-3">
|
|
<a-form-item :label="$t('labels.latitude')">
|
|
<a-input
|
|
:id="identifier.latitude"
|
|
v-model:value="formState.latitude"
|
|
data-testid="nc-geo-data-latitude"
|
|
type="number"
|
|
step="0.0000000001"
|
|
class="nc-input-shadow !w-50"
|
|
:min="-90"
|
|
:disabled="readonly"
|
|
required
|
|
:max="90"
|
|
@blur="handleBlur"
|
|
@keydown.stop
|
|
@selectstart.capture.stop
|
|
@mousedown.stop
|
|
/>
|
|
</a-form-item>
|
|
|
|
<a-form-item :label="$t('labels.longitude')">
|
|
<a-input
|
|
:id="identifier.longitude"
|
|
v-model:value="formState.longitude"
|
|
class="nc-input-shadow !w-50"
|
|
data-testid="nc-geo-data-longitude"
|
|
type="number"
|
|
step="0.0000000001"
|
|
required
|
|
:min="-180"
|
|
:disabled="readonly"
|
|
:max="180"
|
|
@blur="handleBlur"
|
|
@keydown.stop
|
|
@selectstart.capture.stop
|
|
@mousedown.stop
|
|
/>
|
|
</a-form-item>
|
|
</a-row>
|
|
<NcDivider />
|
|
|
|
<div class="flex px-3 mt-2 flex-col gap-2">
|
|
<div class="flex">
|
|
<div class="flex gap-2">
|
|
<NcButton size="small" type="secondary" @click="onClickSetCurrentLocation">
|
|
<div class="flex items-center gap-2">
|
|
<GeneralIcon icon="currentLocation" />
|
|
{{ $t('labels.currentLocation') }}
|
|
</div>
|
|
</NcButton>
|
|
</div>
|
|
<div class="flex-1" />
|
|
<div v-if="vModel" class="flex gap-2">
|
|
<NcButton type="secondary" size="small" @click="openInGoogleMaps">
|
|
<div class="flex items-center gap-2">
|
|
<GeneralIcon icon="ncLogoGoogleMapColored" />
|
|
{{ $t('activity.map.googleMaps') }}
|
|
</div>
|
|
</NcButton>
|
|
|
|
<NcButton type="secondary" size="small" @click="openInOSM">
|
|
<div class="flex items-center gap-2">
|
|
<GeneralIcon class="w-4 h-4" icon="ncLogoOpenStreetMapColored" />
|
|
{{ $t('activity.map.osm') }}
|
|
</div>
|
|
</NcButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end">
|
|
<NcButton type="secondary" size="small" @click="clear">
|
|
{{ $t('general.cancel') }}
|
|
</NcButton>
|
|
|
|
<NcButton html-type="submit" size="small" data-testid="nc-geo-data-save">
|
|
{{ $t('general.submit') }}
|
|
</NcButton>
|
|
</div>
|
|
</div>
|
|
</a-form>
|
|
</div>
|
|
</template>
|
|
</NcDropdown>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
input[type='number']:focus {
|
|
@apply ring-transparent shadow-selected;
|
|
}
|
|
input[type='number'] {
|
|
@apply !border-1 !pr-1 rounded-lg;
|
|
}
|
|
|
|
.ant-form-item {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
:deep(.ant-form-item-label > label) {
|
|
@apply !text-small !leading-[18px] mb-2 text-nc-content-gray flex;
|
|
}
|
|
</style>
|