about summary refs log tree commit diff stats
path: root/frontend/static/search.js
blob: 60fa30dac281c2da0a1659e778225ad292bb411a (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
const search = document.getElementById("search");
let results = document.getElementById("results");

const range = new Range();
range.setStartAfter(search);
range.setEndAfter(search.parentNode.lastChild);

let state = history.state || {
  url: new URL(location).toJSON(),
  opened: [],
};

let escapePolicy = null;
if (window.trustedTypes && trustedTypes.createPolicy) {
  escapePolicy = trustedTypes.createPolicy("fetch", {
    createHTML: (string) => string,
  });
}

function detailsToggled(ev) {
  const nextURL = new URL(location);
  if (ev.newState == "open" || ev.target.open === true) {
    state.opened.push(this.id);
    nextURL.hash = this.id;
  } else {
    state.opened = state.opened.filter((x) => x != this.id);
    nextURL.hash = "";
  }
  state.url = nextURL.toJSON();
  history.replaceState(state, "", nextURL);
}
function addToggleEventListeners(results) {
  results.querySelectorAll("details").forEach((details) =>
    // toggle event doesn't bubble :(
    details.addEventListener("toggle", detailsToggled, { passive: true }),
  );
}
search.addEventListener("submit", function (ev) {
  const url = new URL(this.action);
  url.search = new URLSearchParams(new FormData(this)).toString();
  fetch(url, {
    headers: {
      fetch: "true",
    },
  })
    .then(async function (res) {
      state.url = url.toJSON();
      state.opened = [];
      if (res.ok) {
        history.pushState(state, null, url);
        return res.text();
      } else {
        throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
      }
    })
    .then(function (html) {
      const fragment = range.createContextualFragment(
        escapePolicy !== null ? escapePolicy.createHTML(html) : html,
      );
      const results = fragment.firstElementChild;
      range.deleteContents();
      range.insertNode(results);
      addToggleEventListeners(results);
    })
    .catch(function (error) {
      range.deleteContents();
      range.insertNode(new Text(error.message));
      console.error("fetch failed", error);
    });
  ev.preventDefault();
});

if (results !== null) {
  addToggleEventListeners(results);
}

if (state.opened.length > 0) {
  state.opened.forEach((id) =>
    document.getElementById(id).setAttribute("open", "open"),
  );
} else if (location.hash) {
  document.getElementById(location.hash.slice(1)).setAttribute("open", "open");
}

addEventListener("popstate", function (ev) {
  if (ev.state == null || ev.state.url.pathname.startsWith("/search/")) {
    range.deleteContents();
    search.reset();
  }
});