<template> <div class="popup" :class="{ hidden: !point }" ref="popup"> <div class="popup-content" v-if="point"> <div class="header"> <button @click="$emit('close')" title="Close">✕</button> </div> <div class="point-info"> <h2 class="address" v-if="point.address">{{ point.address }}</h2> <h2 class="latlng" v-else> <a :href="osmUrl" target="_blank" rel="noopener noreferrer"> <font-awesome-icon icon="fas fa-map-marker-alt" /> {{ point.latitude }}, {{ point.longitude }} </a> </h2> <p class="latlng" v-if="point.address"> <a :href="osmUrl" target="_blank" rel="noopener noreferrer"> <font-awesome-icon icon="fas fa-map-marker-alt" /> {{ point.latitude }}, {{ point.longitude }} </a> </p> <p class="altitude" v-if="point.altitude"> <font-awesome-icon icon="fas fa-mountain" /> {{ Math.round(point.altitude) }} m </p> <form class="description editor" @submit.prevent="editPoint" v-if="editDescription"> <div class="row"> <textarea :value="point.description" @keydown.enter="editPoint" @blur="onDescriptionBlur" ref="description" placeholder="Enter a description" /> <button type="submit" title="Save"> <font-awesome-icon icon="fas fa-save" /> </button> </div> </form> <p class="description" :class="{ 'no-content': !point.description?.length }" @click="editDescription = true" v-else> <span class="icon"> <font-awesome-icon icon="fas fa-edit" /> </span> <span class="text"> {{ point.description?.length ? point.description : 'No description' }} </span> </p> <p class="locality" v-if="point.locality">{{ point.locality }}</p> <p class="postal-code" v-if="point.postalCode">{{ point.postalCode }}</p> <p class="country" v-if="country"> <span class="flag" v-if="countryFlag">{{ countryFlag }} </span> <span class="name">{{ country.name }}</span>, <span class="continent">{{ country.continent }}</span> </p> <p class="timestamp" v-if="timeString">{{ timeString }}</p> <div class="remove"> <button title="Remove" @click="$emit('remove', point)"> <font-awesome-icon icon="fas fa-trash-alt" /> Remove </button> </div> </div> </div> </div> </template> <script lang="ts"> import Map from 'ol/Map'; import Overlay from 'ol/Overlay'; import type { TCountryCode } from 'countries-list'; import { getCountryData, getEmojiFlag } from 'countries-list'; import Dates from '../mixins/Dates.vue'; import GPSPoint from '../models/GPSPoint'; export default { emit: ['close', 'edit', 'remove'], mixins: [Dates], props: { point: { type: [GPSPoint, null], }, }, data() { return { newValue: {} as GPSPoint, editDescription: false, popup: null as Overlay | null, } }, computed: { country() { const cc = this.point?.country as string | undefined if (cc?.length) { return getCountryData(cc.toUpperCase() as TCountryCode) } return null }, countryFlag() { return this.country ? getEmojiFlag(this.country.iso2 as TCountryCode) : null }, osmUrl(): string { return `https://www.openstreetmap.org/?mlat=${this.point?.latitude}&mlon=${this.point?.longitude}` }, timeString(): string | null { return this.formatDate(this.point?.timestamp) }, }, methods: { bindPopup(map: Map) { this.popup = new Overlay({ element: this.$refs.popup as HTMLElement, autoPan: true, }) // @ts-ignore map.addOverlay(this.popup) }, editPoint() { this.newValue.description = (this.$refs.description as HTMLTextAreaElement).value this.$emit('edit', this.newValue) this.editDescription = false }, onDescriptionBlur() { // Give it a moment to allow relevant click events to trigger setTimeout(() => { this.editDescription = false }, 100) }, setPosition(coordinates: number[]) { if (this.popup) { this.popup.setPosition(coordinates) } }, }, watch: { point: { immediate: true, handler(point: GPSPoint | null) { if (point) { this.newValue = point } }, }, editDescription(edit: boolean) { if (edit) { this.$nextTick(() => { (this.$refs.description as HTMLTextAreaElement).focus() }) } }, }, } </script> <style lang="scss" scoped> @import "ol/ol.css"; .popup { position: absolute; background: var(--color-background); min-width: 20em; padding: 1em; border-radius: 1em; box-shadow: 2px 2px 2px 2px var(--color-border); .popup-content { display: flex; flex-direction: column; gap: 1.5em; } .header { position: absolute; top: 0.5em; right: 0; button { background: none; border: none; color: var(--color-heading); font-size: 1.2em; cursor: pointer; &:hover { color: var(--color-accent); } } } &.hidden { padding: 0; border-radius: 0; box-shadow: none; width: 0; height: 0; min-width: 0; pointer-events: none; } p.latlng, p.altitude { font-size: 0.8em; margin: -0.25em 0 0.25em 0; } .description { cursor: pointer; &:hover { .icon { color: var(--color-accent); } } .icon { margin-right: 0.5em; } .text { font-style: italic; } &:not(.no-content) { .text { font-weight: bold; } .icon { color: var(--color-accent); } } &.no-content { font-size: 0.9em; opacity: 0.5; } textarea { min-height: 5em; } button { background: var(--color-accent); border: none; color: var(--color-accent); font-size: 0.9em; margin: 0; padding: 0.5em 1.5em; cursor: pointer; &:hover { color: var(--color-heading); } } } .timestamp { color: var(--color-heading); font-weight: bold; font-size: 0.9em; } .remove { font-size: 0.85em; margin-top: 0.5em; button { width: 100%; background: none; border: none; color: var(--vt-c-red-fg-light); margin-left: -0.5em; &:hover { color: var(--vt-c-red-fg-dark); } } } } </style>