131 lines
5.3 KiB
JavaScript
131 lines
5.3 KiB
JavaScript
class WoMap extends HTMLElement {
|
|
#map = null;
|
|
#mounted = false;
|
|
|
|
static get observedAttributes() { return ['lat', 'lng', 'site-name', 'access-notes', 'wo-number']; }
|
|
|
|
connectedCallback() {
|
|
this.style.display = 'block';
|
|
// Light DOM — Leaflet needs real DOM, not shadow root
|
|
if (!this.querySelector('.wo-map-root')) this.#build();
|
|
}
|
|
|
|
attributeChangedCallback() {
|
|
if (this.#mounted) this.#update();
|
|
}
|
|
|
|
#build() {
|
|
const lat = parseFloat(this.getAttribute('lat'));
|
|
const lng = parseFloat(this.getAttribute('lng'));
|
|
const siteName = this.getAttribute('site-name') || 'Work Site';
|
|
const accessNotes = this.getAttribute('access-notes') || '';
|
|
const woNumber = this.getAttribute('wo-number') || '';
|
|
|
|
this.innerHTML = `
|
|
<style>
|
|
.wo-map-root { }
|
|
.wo-map-container { height: 280px; border-radius: var(--radius); border: 1px solid var(--border); overflow: hidden; }
|
|
.wo-map-actions { display: flex; gap: .5rem; margin-top: .75rem; }
|
|
.map-btn { display: inline-flex; align-items: center; gap: .4rem; padding: .45rem .9rem; border-radius: var(--radius); font-size: .813rem; font-weight: 600; cursor: pointer; transition: opacity .15s; text-decoration: none; border: 1px solid var(--border); color: var(--text); background: var(--surface); }
|
|
.map-btn:hover { background: var(--surface-2); text-decoration: none; }
|
|
.map-btn.primary { background: var(--teal); color: #fff; border-color: transparent; }
|
|
.map-btn.primary:hover { opacity: .88; }
|
|
.access-notes { margin-top: .875rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); padding: .75rem 1rem; font-size: .875rem; color: var(--text); display: flex; gap: .6rem; align-items: flex-start; }
|
|
.access-notes i { color: var(--warning); flex-shrink: 0; margin-top: .1rem; }
|
|
.access-text { line-height: 1.5; }
|
|
.no-location { text-align: center; padding: 2.5rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-muted); font-size: .875rem; }
|
|
</style>
|
|
<div class="wo-map-root">
|
|
${lat && lng ? `
|
|
<div class="wo-map-container" id="leaflet-map-${this.getAttribute('wo-id') || 'map'}"></div>
|
|
<div class="wo-map-actions">
|
|
<a class="map-btn primary" id="directions-btn" href="#" target="_blank">
|
|
<i data-lucide="navigation" style="width:14px;height:14px"></i> Get Directions
|
|
</a>
|
|
</div>` : `
|
|
<div class="no-location">
|
|
<i data-lucide="map-pin-off" style="width:28px;height:28px;margin:0 auto .5rem;opacity:.4;display:block"></i>
|
|
No location set for this work order
|
|
</div>`}
|
|
${accessNotes ? `
|
|
<div class="access-notes">
|
|
<i data-lucide="key-round" style="width:16px;height:16px"></i>
|
|
<div class="access-text">${this.#esc(accessNotes)}</div>
|
|
</div>` : ''}
|
|
</div>`;
|
|
|
|
if (window.lucide) lucide.createIcons({ root: this });
|
|
|
|
if (lat && lng) {
|
|
this.#initMap(lat, lng, siteName, woNumber);
|
|
this.#setDirectionsLink(lat, lng);
|
|
}
|
|
this.#mounted = true;
|
|
}
|
|
|
|
#initMap(lat, lng, siteName, woNumber) {
|
|
if (typeof L === 'undefined') {
|
|
// Leaflet not yet loaded — retry once it fires
|
|
window.addEventListener('load', () => this.#initMap(lat, lng, siteName, woNumber), { once: true });
|
|
return;
|
|
}
|
|
const mapId = `leaflet-map-${this.getAttribute('wo-id') || 'map'}`;
|
|
const container = this.querySelector(`#${mapId}`);
|
|
if (!container) return;
|
|
|
|
// Prevent double-init
|
|
if (container._leaflet_id) return;
|
|
|
|
this.#map = L.map(container, { zoomControl: true }).setView([lat, lng], 16);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 19,
|
|
}).addTo(this.#map);
|
|
|
|
const icon = L.divIcon({
|
|
className: '',
|
|
html: `<div style="
|
|
background: var(--teal, #0A7EA4);
|
|
color: #fff;
|
|
border-radius: 50% 50% 50% 0;
|
|
transform: rotate(-45deg);
|
|
width: 32px; height: 32px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,.3);
|
|
font-size: 10px; font-weight: 700;">
|
|
<span style="transform:rotate(45deg)">${this.#esc(woNumber || '📍')}</span>
|
|
</div>`,
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 32],
|
|
popupAnchor: [0, -32],
|
|
});
|
|
|
|
L.marker([lat, lng], { icon })
|
|
.addTo(this.#map)
|
|
.bindPopup(`<strong>${this.#esc(siteName)}</strong><br>${lat.toFixed(5)}, ${lng.toFixed(5)}`)
|
|
.openPopup();
|
|
}
|
|
|
|
#setDirectionsLink(lat, lng) {
|
|
const btn = this.querySelector('#directions-btn');
|
|
if (!btn) return;
|
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
|
btn.href = isIOS
|
|
? `maps://maps.apple.com/?daddr=${lat},${lng}&dirflg=d`
|
|
: `https://maps.google.com/maps?daddr=${lat},${lng}`;
|
|
}
|
|
|
|
#update() {
|
|
if (this.#map) {
|
|
this.#map.remove();
|
|
this.#map = null;
|
|
}
|
|
this.#mounted = false;
|
|
this.#build();
|
|
}
|
|
|
|
#esc(s) { return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
}
|
|
|
|
customElements.define('wo-map', WoMap);
|