164 lines
9.6 KiB
TypeScript
164 lines
9.6 KiB
TypeScript
/**
|
|
* <map-meeting-modal> — meeting point creation modal for rMaps.
|
|
* Dispatches 'meeting-create' CustomEvent with { name, lat, lng, emoji } detail.
|
|
* Dispatches 'modal-close' on dismiss.
|
|
*/
|
|
|
|
const MODAL_STYLE = `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);`;
|
|
const MEETING_EMOJIS = ["\u{1F4CD}", "\u{2B50}", "\u{1F3E0}", "\u{1F37D}", "\u{26FA}", "\u{1F3AF}", "\u{1F680}", "\u{1F33F}", "\u{26A1}", "\u{1F48E}"];
|
|
|
|
class MapMeetingModal extends HTMLElement {
|
|
private _centerLat = 0;
|
|
private _centerLng = 0;
|
|
private _myLat: number | null = null;
|
|
private _myLng: number | null = null;
|
|
private _selectedEmoji = "\u{1F4CD}";
|
|
private _searchResults: { display_name: string; lat: string; lon: string }[] = [];
|
|
|
|
set center(v: { lat: number; lng: number }) { this._centerLat = v.lat; this._centerLng = v.lng; }
|
|
set myLocation(v: { lat: number; lng: number } | null) {
|
|
if (v) { this._myLat = v.lat; this._myLng = v.lng; } else { this._myLat = null; this._myLng = null; }
|
|
}
|
|
|
|
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 = MODAL_STYLE;
|
|
|
|
this.innerHTML = `
|
|
<div style="background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);border-radius:14px;padding:20px;max-width:400px;width:90%;max-height:80vh;overflow-y:auto;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
|
<div style="font-size:16px;font-weight:600;color:var(--rs-text-primary);">\u{1F4CD} Set Meeting Point</div>
|
|
<button id="m-close" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:18px;">\u2715</button>
|
|
</div>
|
|
|
|
<label style="font-size:12px;color:var(--rs-text-secondary);display:block;margin-bottom:4px;">Name</label>
|
|
<input type="text" id="m-name" placeholder="Meeting point" value="Meeting point"
|
|
style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:13px;margin-bottom:12px;">
|
|
|
|
<label style="font-size:12px;color:var(--rs-text-secondary);display:block;margin-bottom:6px;">Emoji</label>
|
|
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;" id="m-emoji-picker">
|
|
${MEETING_EMOJIS.map((e, i) => `<button class="m-emoji" data-emoji="${e}" style="width:32px;height:32px;border-radius:6px;border:1px solid ${i === 0 ? "#4f46e5" : "var(--rs-border)"};background:var(--rs-input-bg);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;">${e}</button>`).join("")}
|
|
</div>
|
|
|
|
<label style="font-size:12px;color:var(--rs-text-secondary);display:block;margin-bottom:4px;">Location</label>
|
|
<div style="display:flex;gap:6px;margin-bottom:10px;">
|
|
<button id="m-gps" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;">\u{1F4CD} Current GPS</button>
|
|
<button id="m-search" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;">\u{1F50E} Search</button>
|
|
<button id="m-manual" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;">\u{1F4DD} Manual</button>
|
|
</div>
|
|
|
|
<div id="m-loc-content">
|
|
<div style="font-size:12px;color:var(--rs-text-muted);text-align:center;padding:12px;">
|
|
Map center: ${this._centerLat.toFixed(5)}, ${this._centerLng.toFixed(5)}
|
|
</div>
|
|
</div>
|
|
|
|
<input type="hidden" id="m-lat" value="${this._centerLat}">
|
|
<input type="hidden" id="m-lng" value="${this._centerLng}">
|
|
|
|
<button id="m-create" style="width:100%;margin-top:14px;padding:10px;border-radius:8px;border:none;background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:13px;">Drop Meeting Point</button>
|
|
</div>
|
|
`;
|
|
|
|
// Close
|
|
this.querySelector("#m-close")?.addEventListener("click", () => this.close());
|
|
this.addEventListener("click", (e) => { if (e.target === this) this.close(); });
|
|
|
|
// Emoji picker
|
|
this.querySelectorAll(".m-emoji").forEach(btn => {
|
|
btn.addEventListener("click", () => {
|
|
this._selectedEmoji = (btn as HTMLElement).dataset.emoji!;
|
|
this.querySelectorAll(".m-emoji").forEach(b => (b as HTMLElement).style.borderColor = "var(--rs-border)");
|
|
(btn as HTMLElement).style.borderColor = "#4f46e5";
|
|
});
|
|
});
|
|
|
|
// GPS
|
|
this.querySelector("#m-gps")?.addEventListener("click", () => {
|
|
const content = this.querySelector("#m-loc-content")!;
|
|
if (this._myLat !== null && this._myLng !== null) {
|
|
(this.querySelector("#m-lat") as HTMLInputElement).value = String(this._myLat);
|
|
(this.querySelector("#m-lng") as HTMLInputElement).value = String(this._myLng);
|
|
content.innerHTML = `<div style="font-size:12px;color:#22c55e;text-align:center;padding:12px;">\u2713 GPS: ${this._myLat!.toFixed(5)}, ${this._myLng!.toFixed(5)}</div>`;
|
|
} else {
|
|
content.innerHTML = `<div style="font-size:12px;color:#f59e0b;text-align:center;padding:12px;">Share your location first</div>`;
|
|
}
|
|
});
|
|
|
|
// Search
|
|
this.querySelector("#m-search")?.addEventListener("click", () => {
|
|
const content = this.querySelector("#m-loc-content")!;
|
|
content.innerHTML = `
|
|
<div style="display:flex;gap:6px;margin-bottom:8px;">
|
|
<input type="text" id="m-addr" placeholder="Search address..." style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:12px;">
|
|
<button id="m-addr-go" style="padding:8px 12px;border-radius:6px;border:none;background:#4f46e5;color:#fff;cursor:pointer;font-size:12px;">Search</button>
|
|
</div>
|
|
<div id="m-sr"></div>
|
|
`;
|
|
this.querySelector("#m-addr-go")?.addEventListener("click", async () => {
|
|
const q = (this.querySelector("#m-addr") as HTMLInputElement).value.trim();
|
|
if (!q) return;
|
|
const sr = this.querySelector("#m-sr")!;
|
|
sr.innerHTML = '<div style="font-size:11px;color:var(--rs-text-muted);padding:6px;">Searching...</div>';
|
|
try {
|
|
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q)}&limit=5`, {
|
|
headers: { "User-Agent": "rMaps/1.0" }, signal: AbortSignal.timeout(5000),
|
|
});
|
|
this._searchResults = await res.json();
|
|
sr.innerHTML = this._searchResults.length ? this._searchResults.map((r, i) => `
|
|
<div data-sr="${i}" style="padding:6px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--rs-text-secondary);border:1px solid transparent;margin-bottom:4px;">${this.esc(r.display_name?.substring(0, 80))}</div>
|
|
`).join("") : '<div style="font-size:11px;color:var(--rs-text-muted);padding:6px;">No results</div>';
|
|
sr.querySelectorAll("[data-sr]").forEach(el => {
|
|
el.addEventListener("click", () => {
|
|
const r = this._searchResults[parseInt((el as HTMLElement).dataset.sr!, 10)];
|
|
(this.querySelector("#m-lat") as HTMLInputElement).value = r.lat;
|
|
(this.querySelector("#m-lng") as HTMLInputElement).value = r.lon;
|
|
sr.querySelectorAll("[data-sr]").forEach(e => (e as HTMLElement).style.borderColor = "transparent");
|
|
(el as HTMLElement).style.borderColor = "#4f46e5";
|
|
});
|
|
});
|
|
} catch { sr.innerHTML = '<div style="font-size:11px;color:#ef4444;padding:6px;">Search failed</div>'; }
|
|
});
|
|
});
|
|
|
|
// Manual
|
|
this.querySelector("#m-manual")?.addEventListener("click", () => {
|
|
const content = this.querySelector("#m-loc-content")!;
|
|
content.innerHTML = `
|
|
<div style="display:flex;gap:8px;">
|
|
<div style="flex:1;"><label style="font-size:11px;color:var(--rs-text-muted);">Latitude</label>
|
|
<input type="number" step="any" id="m-mlat" value="${this._centerLat}" style="width:100%;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:12px;"></div>
|
|
<div style="flex:1;"><label style="font-size:11px;color:var(--rs-text-muted);">Longitude</label>
|
|
<input type="number" step="any" id="m-mlng" value="${this._centerLng}" style="width:100%;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:12px;"></div>
|
|
</div>
|
|
`;
|
|
this.querySelector("#m-mlat")?.addEventListener("input", (e) => { (this.querySelector("#m-lat") as HTMLInputElement).value = (e.target as HTMLInputElement).value; });
|
|
this.querySelector("#m-mlng")?.addEventListener("input", (e) => { (this.querySelector("#m-lng") as HTMLInputElement).value = (e.target as HTMLInputElement).value; });
|
|
});
|
|
|
|
// Create
|
|
this.querySelector("#m-create")?.addEventListener("click", () => {
|
|
const name = (this.querySelector("#m-name") as HTMLInputElement).value.trim() || "Meeting point";
|
|
const lat = parseFloat((this.querySelector("#m-lat") as HTMLInputElement).value);
|
|
const lng = parseFloat((this.querySelector("#m-lng") as HTMLInputElement).value);
|
|
if (isNaN(lat) || isNaN(lng)) return;
|
|
this.dispatchEvent(new CustomEvent("meeting-create", {
|
|
detail: { name, lat, lng, emoji: this._selectedEmoji },
|
|
bubbles: true, composed: true,
|
|
}));
|
|
this.close();
|
|
});
|
|
}
|
|
}
|
|
|
|
customElements.define("map-meeting-modal", MapMeetingModal);
|
|
export { MapMeetingModal };
|