add location-map and location-picker; improve page create and page listing
This commit is contained in:
242
js/components/location-map.js
Normal file
242
js/components/location-map.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 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 }
|
||||
Reference in New Issue
Block a user