summaryrefslogtreecommitdiff
path: root/client/simple/src/js/main/autocomplete.ts
blob: 57788dfd56d819c0b04a27b571b4b17cf67a47ae (plain)
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
130
131
// SPDX-License-Identifier: AGPL-3.0-or-later

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 ?? "";
          }
        }
      }
    }
  });
}