summaryrefslogtreecommitdiff
path: root/client/simple/src/js/main/00_toolkit.ts
blob: 05cfc4b6bcbdc2a3a76300bdc34879ec400f33b3 (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
import type { KeyBindingLayout } from "./keyboard.ts";

type Settings = {
  theme_static_path?: string;
  method?: string;
  hotkeys?: KeyBindingLayout;
  infinite_scroll?: boolean;
  autocomplete?: boolean;
  autocomplete_min?: number;
  search_on_category_select?: boolean;
  translations?: Record<string, string>;
  [key: string]: unknown;
};

type ReadyOptions = {
  // all values must be truthy for the callback to be executed
  on?: (boolean | undefined)[];
};

const getEndpoint = (): string => {
  const endpointClass = Array.from(document.body.classList).find((className) => className.endsWith("_endpoint"));
  return endpointClass?.split("_")[0] ?? "";
};

const getSettings = (): Settings => {
  const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
  if (!settings) return {};

  try {
    return JSON.parse(atob(settings));
  } catch (error) {
    console.error("Failed to load client_settings:", error);
    return {};
  }
};

type AssertElement = (element?: Element | null) => asserts element is Element;
export const assertElement: AssertElement = (element?: Element | null): asserts element is Element => {
  if (!element) {
    throw new Error("Bad assertion: DOM element not found");
  }
};

export const searxng = {
  // dynamic functions
  closeDetail: undefined as (() => void) | undefined,
  scrollPageToSelected: undefined as (() => void) | undefined,
  selectImage: undefined as ((resultElement: Element) => void) | undefined,
  selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
  selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined,

  endpoint: getEndpoint(),

  http: async (method: string, url: string | URL, data?: BodyInit): Promise<Response> => {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 30000);

    const res = await fetch(url, {
      body: data,
      method,
      signal: controller.signal
    }).finally(() => clearTimeout(timeoutId));
    if (!res.ok) {
      throw new Error(res.statusText);
    }

    return res;
  },

  listen: <K extends keyof DocumentEventMap, E extends Element>(
    type: string | K,
    target: string | Document | E,
    listener: (this: E, event: DocumentEventMap[K]) => void,
    options?: AddEventListenerOptions
  ): void => {
    if (typeof target !== "string") {
      target.addEventListener(type, listener as EventListener, options);
      return;
    }

    document.addEventListener(
      type,
      (event: Event) => {
        for (const node of event.composedPath()) {
          if (node instanceof Element && node.matches(target)) {
            try {
              listener.call(node as E, event as DocumentEventMap[K]);
            } catch (error) {
              console.error(error);
            }
            break;
          }
        }
      },
      options
    );
  },

  ready: (callback: () => void, options?: ReadyOptions): void => {
    for (const condition of options?.on ?? []) {
      if (!condition) {
        return;
      }
    }

    if (document.readyState !== "loading") {
      callback();
    } else {
      searxng.listen("DOMContentLoaded", document, callback, { once: true });
    }
  },

  settings: getSettings()
};

searxng.listen("click", ".close", function (this: Element) {
  (this.parentNode as Element)?.classList.add("invisible");
});