snapshot of 962fc3c82c774ab371e3fc4e84c55ef26e61268d Annotations about the code that implements koment.

internal/ui/assets/koment.js

1 (function () {
2 "use strict";
3
4 var switcher = document.querySelector("[data-switcher]");
5 if (switcher) {
6 switcher.addEventListener("change", function () {
7 var destination = sameOriginDestination(switcher.value);
8 if (destination) {
9 window.location.assign(destination);
10 }
11 });
12 }
13
14 setupSearch();
15 alignNotes();
16
17 function sameOriginDestination(value) {
18 if (typeof value !== "string" || value.length === 0) {
19 return null;
20 }
21 var target;
22 try {
23 target = new URL(value, document.baseURI);
24 } catch (error) {
25 return null;
26 }
27 if (target.origin !== window.location.origin) {
28 return null;
29 }
30 if (target.protocol !== "http:" && target.protocol !== "https:") {
31 return null;
32 }
33 return target.href;
34 }
35
36 function setupSearch() {
37 var dialog = document.querySelector("[data-search-dialog]");
38 var trigger = document.querySelector("[data-search-open]");
39 var close = document.querySelector("[data-search-close]");
40 var input = document.querySelector("[data-search-input]");
41 var results = document.querySelector("[data-search-results]");
42 var tree = document.querySelector("[data-tree]");
43 if (!dialog || !trigger || !close || !input || !results || !tree) return;
44
45 var files = Array.prototype.slice.call(tree.querySelectorAll(".file"));
46 var shortcut = document.querySelector("[data-search-shortcut]");
47 var platform = navigator.userAgentData && navigator.userAgentData.platform || navigator.platform || navigator.userAgent;
48 var apple = /Mac|iPhone|iPad|iPod/i.test(platform);
49 var selected = 0;
50 var rendered = [];
51 var returnFocus = null;
52
53 shortcut.textContent = apple ? "⌘K" : "Ctrl K";
54 trigger.hidden = false;
55
56 function editing(target) {
57 if (!target) return false;
58 var name = target.tagName;
59 return target.isContentEditable || name === "INPUT" || name === "TEXTAREA" || name === "SELECT";
60 }
61
62 function matches() {
63 var needle = input.value.trim().toLowerCase();
64 var found = files.filter(function (file) {
65 return !needle || file.dataset.search.indexOf(needle) !== -1;
66 }).slice(0, 50);
67
68 results.replaceChildren();
69 rendered = found.map(function (file, index) {
70 var result = document.createElement("a");
71 var dot = document.createElement("span");
72 var path = document.createElement("span");
73 var meta = document.createElement("span");
74
75 result.className = "search-result";
76 result.href = file.href;
77 result.setAttribute("role", "option");
78 dot.className = "dot " + file.dataset.status;
79 path.className = "search-result-path";
80 path.textContent = file.dataset.path;
81 meta.className = "search-result-meta";
82 meta.textContent = file.dataset.count + (file.dataset.count === "1" ? " annotation" : " annotations");
83 result.append(dot, path, meta);
84 results.appendChild(result);
85 if (index === selected) result.classList.add("selected");
86 result.addEventListener("mousemove", function () { select(index); });
87 return result;
88 });
89
90 if (!rendered.length) {
91 var empty = document.createElement("p");
92 empty.className = "search-empty";
93 empty.textContent = "No matching annotations";
94 results.appendChild(empty);
95 }
96 selected = Math.min(selected, Math.max(rendered.length - 1, 0));
97 select(selected);
98 }
99
100 function select(index) {
101 if (!rendered.length) return;
102 selected = (index + rendered.length) % rendered.length;
103 rendered.forEach(function (result, candidate) {
104 var current = candidate === selected;
105 result.classList.toggle("selected", current);
106 result.setAttribute("aria-selected", current ? "true" : "false");
107 });
108 rendered[selected].scrollIntoView({ block: "nearest" });
109 }
110
111 function openSearch() {
112 if (dialog.open) return;
113 returnFocus = document.activeElement;
114 selected = 0;
115 input.value = "";
116 matches();
117 if (typeof dialog.showModal === "function") dialog.showModal();
118 else dialog.setAttribute("open", "");
119 input.focus();
120 }
121
122 function closeSearch() {
123 if (!dialog.open) return;
124 if (typeof dialog.close === "function") dialog.close();
125 else dialog.removeAttribute("open");
126 if (returnFocus && typeof returnFocus.focus === "function") returnFocus.focus();
127 }
128
129 trigger.addEventListener("click", openSearch);
130 close.addEventListener("click", closeSearch);
131 input.addEventListener("input", function () { selected = 0; matches(); });
132 input.addEventListener("keydown", function (event) {
133 if (event.key === "ArrowDown" || event.key === "ArrowUp") {
134 event.preventDefault();
135 select(selected + (event.key === "ArrowDown" ? 1 : -1));
136 }
137 if (event.key === "Enter" && rendered[selected]) {
138 event.preventDefault();
139 rendered[selected].click();
140 }
141 });
142 dialog.addEventListener("click", function (event) {
143 if (event.target === dialog) closeSearch();
144 });
145 dialog.addEventListener("close", function () {
146 if (returnFocus && typeof returnFocus.focus === "function") returnFocus.focus();
147 });
148 document.addEventListener("keydown", function (event) {
149 var modifier = apple ? event.metaKey : event.ctrlKey;
150 if ((modifier && event.key.toLowerCase() === "k") || (event.key === "/" && !editing(event.target))) {
151 event.preventDefault();
152 openSearch();
153 } else if (event.key === "Escape" && dialog.open) {
154 event.preventDefault();
155 closeSearch();
156 }
157 });
158
159 matches();
160 }
161
162 function alignNotes() {
163 var reading = document.querySelector("[data-reading]");
164 var gloss = document.querySelector("[data-gloss]");
165 if (!reading || !gloss) return;
166
167 var notes = Array.prototype.slice.call(gloss.querySelectorAll(".note[data-for]"));
168 if (!notes.length) return;
169
170 var narrow = window.matchMedia("(max-width: 900px)");
171 var scheduled = null;
172
173 function place() {
174 scheduled = null;
175
176 if (narrow.matches) {
177 notes.forEach(function (note) { note.style.transform = ""; });
178 gloss.classList.remove("aligned");
179 return;
180 }
181
182 var top = gloss.getBoundingClientRect().top;
183 var floor = 0;
184
185 notes.forEach(function (note) {
186 note.style.transform = "";
187 });
188
189 notes.forEach(function (note) {
190 var row = document.getElementById("L" + note.dataset.for);
191 if (!row) return;
192
193 var wanted = row.getBoundingClientRect().top - top;
194 var resting = note.offsetTop;
195 var placed = Math.max(wanted, floor);
196
197 note.style.transform = "translateY(" + (placed - resting) + "px)";
198 floor = placed + note.offsetHeight + 12;
199 });
200
201 gloss.classList.add("aligned");
202 }
203
204 function schedule() {
205 if (scheduled) return;
206 scheduled = window.requestAnimationFrame(place);
207 }
208
209 place();
210 window.addEventListener("resize", schedule);
211 if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);
212
213 notes.forEach(function (note) {
214 var row = document.getElementById("L" + note.dataset.for);
215 if (!row) return;
216 note.addEventListener("mouseenter", function () { row.classList.add("lit"); });
217 note.addEventListener("mouseleave", function () { row.classList.remove("lit"); });
218 row.addEventListener("mouseenter", function () { note.classList.add("lit"); });
219 row.addEventListener("mouseleave", function () { note.classList.remove("lit"); });
220 });
221 }
222 })();

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close