1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
import { assertElement, http, listen, settings } from "../core/toolkit.ts";
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
try {
let res: Response;
if (settings.method === "GET") {
res = await http("GET", `./autocompleter?q=${query}`);
} else {
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
}
const results = await res.json();
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
assertElement(autocomplete);
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
assertElement(autocompleteList);
autocomplete.classList.add("open");
autocompleteList.replaceChildren();
// show an error message that no result was found
if (results?.[1]?.length === 0) {
const noItemFoundMessage = Object.assign(document.createElement("li"), {
className: "no-item-found",
textContent: settings.translations?.no_item_found ?? "No results found"
});
autocompleteList.append(noItemFoundMessage);
return;
}
const fragment = new DocumentFragment();
for (const result of results[1]) {
const li = Object.assign(document.createElement("li"), { textContent: result });
listen("mousedown", li, () => {
qInput.value = result;
const form = document.querySelector<HTMLFormElement>("#search");
form?.submit();
autocomplete.classList.remove("open");
});
fragment.append(li);
}
autocompleteList.append(fragment);
} catch (error) {
console.error("Error fetching autocomplete results:", error);
}
};
const qInput = document.getElementById("q") as HTMLInputElement | null;
assertElement(qInput);
let timeoutId: number;
listen("input", qInput, () => {
clearTimeout(timeoutId);
const query = qInput.value;
const minLength = settings.autocomplete_min ?? 2;
if (query.length < minLength) return;
timeoutId = window.setTimeout(async () => {
if (query === qInput.value) {
await fetchResults(qInput, query);
}
}, 300);
});
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
if (autocompleteList) {
listen("keyup", qInput, (event: KeyboardEvent) => {
const listItems = [...autocompleteList.children] as HTMLElement[];
const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
let newCurrentIndex = -1;
switch (event.key) {
case "ArrowUp": {
const currentItem = listItems[currentIndex];
if (currentItem && currentIndex >= 0) {
currentItem.classList.remove("active");
}
// we need to add listItems.length to the index calculation here because the JavaScript modulos
// operator doesn't work with negative numbers
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
break;
}
case "ArrowDown": {
const currentItem = listItems[currentIndex];
if (currentItem && currentIndex >= 0) {
currentItem.classList.remove("active");
}
newCurrentIndex = (currentIndex + 1) % listItems.length;
break;
}
case "Tab":
case "Enter":
if (autocomplete) {
autocomplete.classList.remove("open");
}
break;
default:
break;
}
if (newCurrentIndex !== -1) {
const selectedItem = listItems[newCurrentIndex];
if (selectedItem) {
selectedItem.classList.add("active");
if (!selectedItem.classList.contains("no-item-found")) {
const qInput = document.getElementById("q") as HTMLInputElement | null;
if (qInput) {
qInput.value = selectedItem.textContent ?? "";
}
}
}
}
});
}
|