/** * — Google Maps GeoJSON import modal for rMaps. * Dispatches 'import-places' CustomEvent with { places: { name, lat, lng }[] } detail. * Dispatches 'modal-close' on dismiss. */ import { parseGoogleMapsGeoJSON } from "./map-import"; class MapImportModal extends HTMLElement { private _step: "upload" | "preview" | "done" = "upload"; private _places: { name: string; lat: number; lng: number; selected: boolean }[] = []; connectedCallback() { this.render(); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } private close() { this.dispatchEvent(new CustomEvent("modal-close", { bubbles: true, composed: true })); this.remove(); } private render() { this.style.cssText = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);`; if (this._step === "upload") this.renderUpload(); else if (this._step === "preview") this.renderPreview(); else this.renderDone(); // Shared close this.querySelector("#i-close")?.addEventListener("click", () => this.close()); this.addEventListener("click", (e) => { if (e.target === this) this.close(); }); } private renderUpload() { this.innerHTML = `
\u{1F4E5} Import Places
\u{1F4C2}
Drop a GeoJSON file here
or click to browse (.json, .geojson)
`; const handleFile = (file: File) => { if (file.size > 50 * 1024 * 1024) { const err = this.querySelector("#i-err") as HTMLElement; err.style.display = "block"; err.textContent = "File too large (max 50 MB)"; return; } const reader = new FileReader(); reader.onload = () => { const result = parseGoogleMapsGeoJSON(reader.result as string); if (!result.success) { const err = this.querySelector("#i-err") as HTMLElement; err.style.display = "block"; err.textContent = result.error || "No places found"; return; } this._places = result.places.map(p => ({ ...p, selected: true })); this._step = "preview"; this.render(); }; reader.readAsText(file); }; const drop = this.querySelector("#i-drop")!; const fileInput = this.querySelector("#i-file") as HTMLInputElement; drop.addEventListener("click", () => fileInput.click()); drop.addEventListener("dragover", (e) => { e.preventDefault(); (drop as HTMLElement).style.borderColor = "#4f46e5"; }); drop.addEventListener("dragleave", () => { (drop as HTMLElement).style.borderColor = "var(--rs-border)"; }); drop.addEventListener("drop", (e) => { e.preventDefault(); (drop as HTMLElement).style.borderColor = "var(--rs-border)"; const file = (e as DragEvent).dataTransfer?.files[0]; if (file) handleFile(file); }); fileInput.addEventListener("change", () => { if (fileInput.files?.[0]) handleFile(fileInput.files[0]); }); } private renderPreview() { const count = this._places.filter(p => p.selected).length; this.innerHTML = `
Preview (${this._places.length} places)
${this._places.map((p, i) => ` `).join("")}
`; this.querySelectorAll("[data-idx]").forEach(cb => { cb.addEventListener("change", (e) => { this._places[parseInt((cb as HTMLElement).dataset.idx!, 10)].selected = (e.target as HTMLInputElement).checked; const btn = this.querySelector("#i-confirm"); if (btn) btn.textContent = `Import ${this._places.filter(p => p.selected).length} Places as Waypoints`; }); }); this.querySelector("#i-confirm")?.addEventListener("click", () => { const selected = this._places.filter(p => p.selected).map(({ name, lat, lng }) => ({ name, lat, lng })); this.dispatchEvent(new CustomEvent("import-places", { detail: { places: selected }, bubbles: true, composed: true })); this._step = "done"; this.render(); }); } private renderDone() { const count = this._places.filter(p => p.selected).length; this.innerHTML = `
\u2705
Imported ${count} places!
They've been added as waypoints to this room.
`; this.querySelector("#i-done")?.addEventListener("click", () => this.close()); } } customElements.define("map-import-modal", MapImportModal); export { MapImportModal };