1import { AnnotationStore, annotationDeepLink } from "./annotations.js?v=note-autoshow-1"; 2import { annotationSimulationRequest, simulatedAnnotations } from "./annotations-simulation.js"; 3import { loadData } from "./data.js"; 4 5const list = document.querySelector("#annotations-list"); 6const refresh = document.querySelector("#annotations-refresh"); 7const search = document.querySelector("#annotations-search"); 8const simulationNotice = document.querySelector("#annotations-simulation"); 9const expanded = new Set(); 10const shownNotes = new Map(); 11let shownMaps = 18; 12let currentStore = null; 13let currentMaps = null; 14 15function escapeHtml(value) { 16 return String(value ?? "").replace(/[&<>'"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[character])); 17} 18 19function noteDate(value) { 20 const date = new Date(value); 21 if (Number.isNaN(date.getTime())) return "Submitted"; 22 return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "numeric" }).format(date); 23} 24 25function pointLabel(point) { 26 const latitude = `${point.lat < 0 ? "S" : "N"} ${Math.abs(point.lat).toFixed(4)}`; 27 const longitude = `${point.lng < 0 ? "W" : "E"} ${Math.abs(point.lng).toFixed(4)}`; 28 return `${latitude} · ${longitude}`; 29} 30 31function render(store, maps) { 32 currentStore = store; 33 currentMaps = maps; 34 if (!store.live) { 35 list.innerHTML = `<p class="annotations-state">The public annotation stream is not open yet.</p>`; 36 refresh.hidden = true; 37 return; 38 } 39 const records = store.annotations; 40 if (!records.length) { 41 list.innerHTML = `<p class="annotations-state">No notes have been left yet. Open a historical map and choose <em>annotate</em> to begin.</p>`; 42 return; 43 } 44 const groups = new Map(); 45 for (const annotation of records) { 46 const id = annotation.context.mapId; 47 if (!groups.has(id)) groups.set(id, []); 48 groups.get(id).push(annotation); 49 } 50 const term = search.value.trim().toLocaleLowerCase(); 51 const matches = [...groups].map(([id, notes]) => { 52 const map = maps.get(id); 53 const title = map?.title || notes[0].context.title || "Historical map"; 54 const city = map?.city || notes[0].context.city || "Unplaced"; 55 const year = map?.year || notes[0].context.year || ""; 56 const mapMatch = `${title} ${city} ${year}`.toLocaleLowerCase().includes(term); 57 const filtered = !term || mapMatch ? notes : notes.filter((note) => `${note.text} ${note.tag} ${note.name || ""}`.toLocaleLowerCase().includes(term)); 58 return { id, title, city, year, notes: filtered, total: notes.length, latest: notes[0].timestampValue }; 59 }).filter((group) => group.notes.length).sort((a, b) => b.latest - a.latest || a.title.localeCompare(b.title)); 60 if (!matches.length) { 61 list.innerHTML = `<p class="annotations-state">No maps or notes match this search.</p>`; 62 return; 63 } 64 if (!expanded.size && !term) expanded.add(matches[0].id); 65 const visible = matches.slice(0, shownMaps); 66 list.innerHTML = visible.map((group) => { 67 const limit = shownNotes.get(group.id) || 8; 68 const noteRows = group.notes.slice(0, limit).map((annotation) => { 69 const tags = (annotation.tags || (annotation.tag ? annotation.tag.split(",") : [])).map((tag) => tag.trim().replace(/^#+/, "")).filter(Boolean); 70 const tagCopy = tags.map((tag) => `<span class="annotations-tag">#${escapeHtml(tag)}</span>`).join(""); 71 const byline = annotation.name ? `<span>${escapeHtml(annotation.name)}</span>` : ""; 72 return `<article class="annotation-card"><blockquote>${escapeHtml(annotation.text)}</blockquote><footer>${byline}<span>${escapeHtml(noteDate(annotation.timestamp))}</span><span>${escapeHtml(pointLabel(annotation.context.point))}</span>${tagCopy}<a href="${escapeHtml(annotationDeepLink(annotation))}">OPEN MAP <span aria-hidden="true">â</span></a></footer></article>`; 73 }).join(""); 74 const more = group.notes.length > limit ? `<button class="annotations-more" type="button" data-more-notes="${escapeHtml(group.id)}">SHOW ${Math.min(8, group.notes.length - limit)} MORE NOTES â</button>` : ""; 75 return `<section class="annotation-group"><details data-map-group="${escapeHtml(group.id)}"${expanded.has(group.id) ? " open" : ""}><summary><span class="annotation-group-identity"><small>${escapeHtml(group.city)}${group.year ? ` · ${escapeHtml(group.year)}` : ""}</small><strong>${escapeHtml(group.title)}</strong></span><span class="annotation-group-count">${group.total} NOTE${group.total === 1 ? "" : "S"} <i aria-hidden="true">â</i></span></summary><div class="annotation-group-notes">${noteRows}${more}</div></details></section>`; 76 }).join("") + (matches.length > shownMaps ? `<button class="annotations-more-maps" type="button">SHOW MORE MAPS â <span>${shownMaps} OF ${matches.length}</span></button>` : ""); 77} 78 79async function start({ force = false } = {}) { 80 refresh.disabled = true; 81 try { 82 const [store, data] = await Promise.all([AnnotationStore.load(), loadD
82ata()]); 83 const maps = new Map(data.maps.map((map) => [map.id, map])); 84 const simulation = annotationSimulationRequest(window.location.search); 85 const simulationMap = simulation && maps.get(simulation.mapId); 86 if (simulationMap) { 87 store.annotations = simulatedAnnotations(simulationMap, simulation.count).reverse(); 88 simulationNotice.innerHTML = `${simulation.count} temporary notes on ${escapeHtml(simulationMap.title)}. Nothing here has been submitted. <a href="./?map=${encodeURIComponent(simulationMap.id)}&simulateNotes=60">VIEW MAP â</a>`; 89 simulationNotice.hidden = false; 90 } else { 91 simulationNotice.hidden = true; 92 await store.refresh({ force }); 93 } 94 render(store, maps); 95 } catch (error) { 96 list.innerHTML = `<p class="annotations-state">The annotation stream could not be opened. The maps remain available.</p>`; 97 console.warn("Could not open annotations page.", error); 98 } finally { 99 refresh.disabled = false; 100 } 101} 102 103refresh.addEventListener("click", () => start({ force: true })); 104search.addEventListener("input", () => { shownMaps = 18; if (currentStore && currentMaps) render(currentStore, currentMaps); }); 105list.addEventListener("toggle", (event) => { 106 const details = event.target.closest("[data-map-group]"); 107 if (!details) return; 108 if (details.open) expanded.add(details.dataset.mapGroup); else expanded.delete(details.dataset.mapGroup); 109}, true); 110list.addEventListener("click", (event) => { 111 const noteButton = event.target.closest("[data-more-notes]"); 112 if (noteButton) { 113 shownNotes.set(noteButton.dataset.moreNotes, (shownNotes.get(noteButton.dataset.moreNotes) || 8) + 8); 114 render(currentStore, currentMaps); 115 } 116 if (event.target.closest(".annotations-more-maps")) { 117 shownMaps += 18; 118 render(currentStore, currentMaps); 119 } 120}); 121start();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.