PageSourceSearch

https://hypercities.com/js/annotations-page.js?v=note-autoshow-1

js hypercities.com collected 2026-09-25 18:52:46 UTC 6,826 bytes, 121 lines download raw bytes

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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" }[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.