From 408aed03d3454330120475ca53838a2f4fe28ea3 Mon Sep 17 00:00:00 2001
From: Alan Pearce
Date: Sat, 8 Jun 2024 20:34:37 +0200
Subject: feat: display results in a table, showing details on click
---
frontend/static/search.js | 118 +++++++++++++++++++-----------
frontend/static/style.css | 22 +++++-
frontend/templates/blocks/options.gotmpl | 75 ++++++-------------
frontend/templates/blocks/packages.gotmpl | 99 +++++++------------------
frontend/templates/blocks/results.gotmpl | 28 ++++---
frontend/templates/blocks/search.gotmpl | 11 ++-
internal/server/templates.go | 15 +++-
7 files changed, 180 insertions(+), 188 deletions(-)
diff --git a/frontend/static/search.js b/frontend/static/search.js
index 8d30d89..e3777d7 100644
--- a/frontend/static/search.js
+++ b/frontend/static/search.js
@@ -1,18 +1,23 @@
const search = document.getElementById("search");
const nav = document.querySelectorAll("body > header > nav")[0];
const queryInput = document.getElementById("query");
-let results = document.getElementById("results");
+const dialog = document.getElementById("dialog");
+const results = document.getElementById("results");
let pagination = document.getElementById("pagination");
-const range = new Range();
-range.setStartAfter(search);
-range.setEndAfter(search.parentNode.lastChild);
+const resultsRange = new Range();
+resultsRange.setStartBefore(results.firstChild);
+resultsRange.setEndAfter(results.lastChild);
+
+const detailsRange = new Range();
+detailsRange.setStartAfter(dialog.firstElementChild);
+detailsRange.setEndAfter(dialog.lastElementChild);
let urlLocation = new URL(location);
let state = history.state || {
url: urlLocation.toString(),
input: urlLocation.searchParams.get("query"),
- fragment: range.cloneContents().innerHTML || null,
+ results: resultsRange.cloneContents().innerHTML || null,
opened: [],
};
@@ -23,23 +28,10 @@ if (window.trustedTypes && trustedTypes.createPolicy) {
});
}
-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 }),
- );
+function addOpenDialogListeners(results) {
+ results.querySelectorAll("a.open-dialog").forEach(function (element) {
+ element.addEventListener("click", handleDialogOpen);
+ });
}
function paginationLinkClicked(ev) {
@@ -54,17 +46,14 @@ function addPaginationEventListeners(pagination) {
);
}
-function renderFragmentHTML(html) {
- const fragment = range.createContextualFragment(
+function renderResults(html) {
+ const fragment = resultsRange.createContextualFragment(
escapePolicy !== null ? escapePolicy.createHTML(html) : html,
);
- results = fragment.querySelector("#results");
pagination = fragment.querySelector("#pagination");
- range.deleteContents();
- range.insertNode(fragment);
- if (results !== null) {
- addToggleEventListeners(results);
- }
+ resultsRange.deleteContents();
+ resultsRange.insertNode(fragment);
+ addOpenDialogListeners(results);
if (pagination !== null) {
addPaginationEventListeners(pagination);
}
@@ -82,16 +71,16 @@ async function getResults(url) {
// render errors sent as HTML as well as OK responses
if (res.headers.get("content-type").startsWith("text/html")) {
- state.fragment = await res.text();
+ state.results = await res.text();
state.opened = [];
history.replaceState(state, null, url);
- renderFragmentHTML(state.fragment);
+ renderResults(state.results);
} else {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
} catch (error) {
- range.deleteContents();
- range.insertNode(new Text(error.message));
+ resultsRange.deleteContents();
+ resultsRange.insertNode(new Text(error.message));
console.error("fetch failed", error);
}
}
@@ -120,7 +109,7 @@ search.addEventListener("submit", function (ev) {
});
if (results !== null) {
- addToggleEventListeners(results);
+ addOpenDialogListeners(results);
}
if (pagination !== null) {
addPaginationEventListeners(pagination);
@@ -129,13 +118,55 @@ if (pagination !== null) {
document.querySelector("a.current").addEventListener("click", function (ev) {
search.reset();
state.input = null;
- range.deleteContents();
- state.fragment = "";
+ resultsRange.deleteContents();
+ state.results = "";
history.pushState(state, null, ev.target.href);
ev.preventDefault();
queryInput.value = "";
});
+function renderDetails(html) {
+ const fragment = detailsRange.createContextualFragment(
+ escapePolicy !== null ? escapePolicy.createHTML(html) : html,
+ );
+ detailsRange.insertNode(fragment);
+ dialog.showModal();
+}
+
+dialog.addEventListener("close", function (event) {
+ detailsRange.deleteContents();
+});
+
+dialog.querySelector("button").addEventListener("click", function () {
+ dialog.close();
+});
+
+async function getDetail(url) {
+ try {
+ state.url = url.toJSON();
+ const res = await fetch(url, {
+ headers: {
+ fetch: "true",
+ },
+ });
+
+ // render errors sent as HTML as well as OK responses
+ if (res.headers.get("content-type").startsWith("text/html")) {
+ renderDetails(await res.text());
+ } else {
+ throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
+ }
+ } catch (error) {
+ console.error("fetch failed", error);
+ renderDetails(new Text(error.message));
+ }
+}
+
+function handleDialogOpen(ev) {
+ getDetail(new URL(ev.target.href));
+ ev.preventDefault();
+}
+
if (state.opened.length > 0) {
state.opened.forEach((id) =>
document.getElementById(id).setAttribute("open", "open"),
@@ -147,12 +178,15 @@ if (state.opened.length > 0) {
addEventListener("popstate", function (ev) {
if (ev.state != null) {
url = new URL(ev.state.url);
- if (ev.state.fragment !== null) {
+ if (ev.state.results !== null) {
queryInput.value = ev.state.input;
- renderFragmentHTML(ev.state.fragment);
- return;
+ renderResults(ev.state.results);
+ }
+ if (ev.state.details !== null) {
+ renderDetails(ev.state.details);
}
+ } else {
+ resultsRange.deleteContents();
+ search.reset();
}
- range.deleteContents();
- search.reset();
});
diff --git a/frontend/static/style.css b/frontend/static/style.css
index 8d5977f..379c60e 100644
--- a/frontend/static/style.css
+++ b/frontend/static/style.css
@@ -58,7 +58,7 @@ fieldset {
}
section {
- border-top: none;
+ border: none;
margin: unset;
padding: unset;
}
@@ -75,7 +75,8 @@ dd {
margin-inline-start: 1rem;
}
-dd > p {
+dd > p,
+td > p {
margin: unset;
}
@@ -125,3 +126,20 @@ h3 {
blockquote > p {
margin: unset;
}
+
+dialog {
+ max-width: unset;
+ width: min(var(--min-width), 90%);
+}
+
+dialog > button {
+ float: right;
+}
+
+dialog > h2 {
+ margin-top: 0.5rem;
+}
+
+table {
+ width: 100%;
+}
diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl
index de31696..5a08bae 100644
--- a/frontend/templates/blocks/options.gotmpl
+++ b/frontend/templates/blocks/options.gotmpl
@@ -1,54 +1,25 @@
{{- define "hits" }}
- {{- range . }}
- {{- with .Data }}
-
-
- {{ markdown .Description }}
- {{ .Name }}
-
- {{- with .Type }}
-
- {{ . }}
- {{- end }}
- {{ .Text }}
- {{- end }}
- {{ .Text }}
Title | +Description | +
---|---|
+ {{ .Name }} + | ++ {{ markdown (firstSentence .Description) }} + + | +
{{ .Description }}
- {{- end }} -{{ . }}
- Attribute | +Name | +Description | +
---|---|---|
+ {{- with .Attribute }}
+ {{ . }}
{{- end }}
-
- {{- end }}
- {{- with .Maintainers }}
- |
+ + {{ .Name }} + | ++ {{ .Description }} + | +