Files
kashilo/js/components/location-map.js

243 lines
6.7 KiB
JavaScript

/**
* Location Map Component
* Displays a location on an OpenStreetMap map using Leaflet
*/
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
class LocationMap extends HTMLElement {
constructor() {
super()
this.map = null
this.marker = null
this.leafletLoaded = false
}
static get observedAttributes() {
return ['lat', 'lng', 'name', 'postal-code', 'country']
}
async connectedCallback() {
await this.loadLeaflet()
this.render()
await this.initMap()
}
disconnectedCallback() {
if (this.map) {
this.map.remove()
this.map = null
}
}
attributeChangedCallback() {
if (this.map) {
this.updateMap()
}
}
async loadLeaflet() {
if (this.leafletLoaded || window.L) {
this.leafletLoaded = true
return
}
// Load Leaflet CSS
const css = document.createElement('link')
css.rel = 'stylesheet'
css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'
css.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='
css.crossOrigin = ''
document.head.appendChild(css)
// Load Leaflet JS
await new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'
script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo='
script.crossOrigin = ''
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
this.leafletLoaded = true
}
render() {
const name = this.getAttribute('name') || ''
const postalCode = this.getAttribute('postal-code') || ''
const country = this.getAttribute('country') || ''
const locationText = [postalCode, name, country].filter(Boolean).join(', ')
this.innerHTML = /* html */`
<div class="location-map-container">
<div class="location-map-header">
<span class="location-icon">📍</span>
<span class="location-text">${this.escapeHtml(locationText)}</span>
</div>
<div class="location-map" id="map"></div>
</div>
`
}
async initMap() {
const lat = parseFloat(this.getAttribute('lat'))
const lng = parseFloat(this.getAttribute('lng'))
let coords = null
if (!isNaN(lat) && !isNaN(lng)) {
coords = [lat, lng]
} else {
// Geocode from name/postal-code/country
coords = await this.geocodeLocation()
}
if (!coords) {
this.querySelector('.location-map').innerHTML = '<p class="map-error">Standort konnte nicht geladen werden</p>'
return
}
const mapContainer = this.querySelector('#map')
if (!mapContainer || !window.L) return
this.map = L.map(mapContainer, {
scrollWheelZoom: false,
dragging: !L.Browser.mobile,
attributionControl: false
}).setView(coords, 13)
// Custom attribution (OSM required, no Leaflet branding)
L.control.attribution({
prefix: false
}).addAttribution('© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>').addTo(this.map)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: ''
}).addTo(this.map)
this.marker = L.marker(coords).addTo(this.map)
// Invalidate size after render (fixes grey tiles issue)
setTimeout(() => {
this.map.invalidateSize()
}, 100)
}
async geocodeLocation() {
const name = this.getAttribute('name') || ''
const postalCode = this.getAttribute('postal-code') || ''
const country = this.getAttribute('country') || ''
const query = [postalCode, name, country].filter(Boolean).join(' ')
if (!query) return null
try {
const params = new URLSearchParams({
q: query,
format: 'json',
limit: '1'
})
const response = await fetch(`${NOMINATIM_URL}?${params}`, {
headers: { 'User-Agent': 'dgray.io/1.0' }
})
const results = await response.json()
if (results.length > 0) {
return [parseFloat(results[0].lat), parseFloat(results[0].lon)]
}
} catch (error) {
console.error('Geocoding failed:', error)
}
return null
}
async updateMap() {
if (!this.map) return
const coords = await this.geocodeLocation()
if (coords) {
this.map.setView(coords, 13)
if (this.marker) {
this.marker.setLatLng(coords)
}
}
}
escapeHtml(text) {
if (!text) return ''
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('location-map', LocationMap)
const style = document.createElement('style')
style.textContent = /* css */`
location-map {
display: block;
}
location-map .location-map-container {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
overflow: hidden;
}
location-map .location-map-header {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
}
location-map .location-icon {
font-size: 1rem;
}
location-map .location-text {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
}
location-map .location-map {
height: 200px;
background: var(--color-bg-tertiary);
}
location-map .map-error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
/* Fix Leaflet styles for dark mode */
location-map .leaflet-container {
background: var(--color-bg-tertiary);
}
location-map .leaflet-control-attribution {
background: var(--color-bg);
color: var(--color-text-muted);
font-size: 10px;
}
location-map .leaflet-control-attribution a {
color: var(--color-primary);
}
`
document.head.appendChild(style)
export { LocationMap }