// Map-page bookmark UI: a star toggle in the results "share" row, and a // compact "Saved" pill + dropdown near the Find button for jumping between // saved spots without retyping. Pure DOM glue over bookmarks.js, which is the // only place bookmark data actually lives — this module holds no state of its // own beyond references to the elements it built. // // Loaded as an extra module alongside app.js rather than merged into it: app.js // owns #results' markup (it rewrites results.innerHTML wholesale on every grade // via its own render()), so a MutationObserver on #results is what lets this // module re-attach the star after every re-render without needing a hook // app.js doesn't expose. selectLocation() is module-private to app.js, so // jumping to a saved spot goes through the same hash the app already treats as // authoritative on load (see nav.js: "a hash always wins") via a reload — // the same path opening a shared link takes, so the normal grade flow runs. import * as bookmarks from "./bookmarks.js"; const STAR_OUTLINE = ``; const STAR_FILLED = ``; const CHEV_IC = ``; const CLOSE_X = ``; const PENCIL_IC = ``; function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function injectStyle() { if (document.getElementById("tg-bookmarks-style")) return; const style = document.createElement("style"); style.id = "tg-bookmarks-style"; style.textContent = ` .bkm-star[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); } .bkm-pill-wrap { position: relative; display: inline-flex; align-self: center; } .bkm-pill { border-radius: 10px; border: 1px solid var(--border); cursor: pointer; background: var(--surface-2); color: var(--text); font-weight: 600; font-size: 13px; padding: 9px 12px; min-height: 40px; display: inline-flex; align-items: center; gap: 6px; } .bkm-pill:hover { border-color: var(--accent); } .bkm-pill[hidden] { display: none; } .bkm-drop { position: absolute; left: 0; top: calc(100% + 6px); z-index: 1100; min-width: 260px; max-width: min(340px, 90vw); max-height: 60vh; overflow-y: auto; display: flex; flex-direction: column; padding: 6px; margin: 0; list-style: none; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 16px 40px rgba(0, 0, 0, .45); } .bkm-drop[hidden] { display: none; } .bkm-item { display: flex; align-items: center; gap: 2px; border-radius: 8px; } .bkm-item:hover { background: var(--surface-2); } .bkm-item-go { flex: 1 1 auto; min-width: 0; text-align: left; background: none; border: 0; cursor: pointer; color: var(--text); font: inherit; font-size: 13.5px; padding: 9px 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bkm-item-btn { flex-shrink: 0; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 7px; border-radius: 6px; display: inline-flex; } .bkm-item-btn:hover { color: var(--text); background: var(--border); } .bkm-empty { padding: 10px 8px; font-size: 12.5px; color: var(--muted); } .bkm-import-banner { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: 12px; padding: 10px 14px; margin: 0 0 16px; font-size: 13.5px; } .bkm-import-banner .bkm-import-actions { display: flex; gap: 8px; flex-shrink: 0; } .bkm-import-banner button { border-radius: 8px; border: 1px solid var(--border); cursor: pointer; padding: 7px 12px; font-size: 12.5px; font-weight: 600; background: var(--surface); color: var(--text); } .bkm-import-banner button.bkm-import-yes { background: var(--accent); color: #1a1206; border-color: var(--accent); } @media (max-width: 480px) { .bkm-drop { left: auto; right: 0; } } `; document.head.appendChild(style); } function parseHash() { const p = new URLSearchParams(location.hash.slice(1)); const lat = parseFloat(p.get("lat")), lon = parseFloat(p.get("lon")); if (isNaN(lat) || isNaN(lon)) return null; return { lat, lon }; } function jumpTo(lat, lon) { const dateInput = document.getElementById("date-input"); const date = dateInput && dateInput.value ? dateInput.value : ""; let h = `#lat=${lat.toFixed(5)}&lon=${lon.toFixed(5)}`; if (date) h += `&date=${date}`; location.hash = h; location.reload(); } function currentLabel() { const h2 = document.querySelector("#results .loc-title h2"); return (h2 && h2.textContent.trim()) || null; } // ---- star toggle in the .share row ---- function buildStar() { const btn = document.createElement("button"); btn.type = "button"; btn.id = "btn-bookmark"; btn.className = "bkm-star"; btn.addEventListener("click", async () => { const loc = parseHash(); if (!loc) return; const existing = bookmarks.find(loc.lat, loc.lon); btn.disabled = true; try { if (existing) await bookmarks.remove(existing.id); else await bookmarks.add(loc.lat, loc.lon, currentLabel()); } finally { btn.disabled = false; } }); return btn; } function paintStar() { const share = document.querySelector("#results .share"); if (!share) return; let btn = share.querySelector("#btn-bookmark"); if (!btn) { btn = buildStar(); share.insertBefore(btn, share.firstChild); } const loc = parseHash(); const saved = loc ? !!bookmarks.find(loc.lat, loc.lon) : false; btn.setAttribute("aria-pressed", saved ? "true" : "false"); btn.title = saved ? "Remove this saved location" : "Save this location"; btn.setAttribute("aria-label", btn.title); btn.innerHTML = (saved ? STAR_FILLED : STAR_OUTLINE) + `${saved ? "Saved" : "Save"}`; } const resultsEl = document.getElementById("results"); if (resultsEl) { new MutationObserver(() => paintStar()).observe(resultsEl, { childList: true }); } // ---- "Saved" dropdown near the Find button ---- let dropEl = null, pillEl = null; function buildSavedControl() { const findBar = document.querySelector(".find-bar"); if (!findBar) return; const wrap = document.createElement("div"); wrap.className = "bkm-pill-wrap"; wrap.innerHTML = `