internal/ui/assets/koment.js
1
(function () {
2
"use strict";
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
}
14
setupSearch();
15
alignNotes();
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
}
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;
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;
53
shortcut.textContent = apple ? "⌘K" : "Ctrl K";
54
trigger.hidden = false;
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
}
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);
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");
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
});
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
}
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
}
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
}
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
}
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
});
159
matches();
160
}
162
function alignNotes() {
163
var reading = document.querySelector("[data-reading]");
164
var gloss = document.querySelector("[data-gloss]");
165
if (!reading || !gloss) return;
167
var notes = Array.prototype.slice.call(gloss.querySelectorAll(".note[data-for]"));
168
if (!notes.length) return;
170
var narrow = window.matchMedia("(max-width: 900px)");
171
var scheduled = null;
173
function place() {
174
scheduled = null;
176
if (narrow.matches) {
177
notes.forEach(function (note) { note.style.transform = ""; });
178
gloss.classList.remove("aligned");
179
return;
180
}
182
var top = gloss.getBoundingClientRect().top;
183
var floor = 0;
185
notes.forEach(function (note) {
186
note.style.transform = "";
187
});
189
notes.forEach(function (note) {
190
var row = document.getElementById("L" + note.dataset.for);
191
if (!row) return;
193
var wanted = row.getBoundingClientRect().top - top;
194
var resting = note.offsetTop;
195
var placed = Math.max(wanted, floor);
197
note.style.transform = "translateY(" + (placed - resting) + "px)";
198
floor = placed + note.offsetHeight + 12;
199
});
201
gloss.classList.add("aligned");
202
}
204
function schedule() {
205
if (scheduled) return;
206
scheduled = window.requestAnimationFrame(place);
207
}
209
place();
210
window.addEventListener("resize", schedule);
211
if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);
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
})();