about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2024-06-08 20:34:37 +0200
committerAlan Pearce2024-06-08 20:42:48 +0200
commit408aed03d3454330120475ca53838a2f4fe28ea3 (patch)
tree78d0bb27634154a4ac956c851ea12b71db9ac6b9
parentd40c0e188a7fe1b36887f59c4a9958faa81b3d44 (diff)
downloadsearchix-408aed03d3454330120475ca53838a2f4fe28ea3.tar.lz
searchix-408aed03d3454330120475ca53838a2f4fe28ea3.tar.zst
searchix-408aed03d3454330120475ca53838a2f4fe28ea3.zip
feat: display results in a table, showing details on click
-rw-r--r--frontend/static/search.js118
-rw-r--r--frontend/static/style.css22
-rw-r--r--frontend/templates/blocks/options.gotmpl75
-rw-r--r--frontend/templates/blocks/packages.gotmpl99
-rw-r--r--frontend/templates/blocks/results.gotmpl28
-rw-r--r--frontend/templates/blocks/search.gotmpl11
-rw-r--r--internal/server/templates.go15
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 }}
-      <details id="{{ .Name }}">
-        <summary>
-          <h3>{{ .Name }}</h3>
-        </summary>
-        {{ markdown .Description }}
-        <dl>
-          {{- with .Type }}
-            <dt>Type</dt>
-            <dd><code>{{ . }}</code></dd>
-          {{- end }}
-          {{- with .Default }}
-            {{- if or .Text .Markdown }}
-              <dt>Default</dt>
-              <dd>
-                {{- if .Markdown }}
-                  {{ markdown .Markdown }}
-                {{- else }}
-                  <pre><code>{{ .Text }}</code></pre>
-                {{- end }}
-              </dd>
-            {{- end }}
-          {{- end }}
-          {{- with .Example }}
-            {{- if or .Text .Markdown }}
-              <dt>Example</dt>
-              <dd>
-                {{- if .Markdown }}
-                  {{ markdown .Markdown }}
-                {{- else }}
-                  <pre><code>{{ .Text }}</code></pre>
-                {{- end }}
-              </dd>
-            {{- end }}
-          {{- end }}
-          {{- with .RelatedPackages }}
-            <dt>Related Packages</dt>
-            <dd>{{ . }}</dd>
-          {{- end }}
-          {{- with .Declarations }}
-            <dt>Declared</dt>
-            {{- range . }}
-              <dd>
-                <a href="{{ .URL }}">{{ .Name }}</a>
-              </dd>
-            {{- end }}
-          {{- end }}
-        </dl>
-      </details>
-    {{- end }}
-  {{- end }}
+  <table>
+    <thead>
+      <tr>
+        <th scope="col">Title</th>
+        <th scope="col">Description</th>
+      </tr>
+    </thead>
+    <tbody>
+      {{- range . }}
+        {{- with .Data }}
+          <tr>
+            <td>
+              <a href="{{ .Name }}" class="open-dialog">{{ .Name }}</a>
+            </td>
+            <td>
+              {{ markdown (firstSentence .Description) }}
+              <dialog id="{{ .Name }}"></dialog>
+            </td>
+          </tr>
+        {{- end }}
+      {{- end }}
+    </tbody>
+  </table>
 {{- end }}
diff --git a/frontend/templates/blocks/packages.gotmpl b/frontend/templates/blocks/packages.gotmpl
index 90ba0b2..cce97a0 100644
--- a/frontend/templates/blocks/packages.gotmpl
+++ b/frontend/templates/blocks/packages.gotmpl
@@ -1,75 +1,30 @@
 {{- define "hits" }}
-  {{- range . }}
-    {{- with .Data }}
-      <details id="{{ .Name }}">
-        <summary>
-          <h3>
-            {{- if .Broken }}
-              <del>{{ .Attribute }}</del>
-            {{- else }}
-              {{ .Attribute }}
-            {{- end }}
-          </h3>
-        </summary>
-        {{- if .LongDescription }}
-          {{ markdown .LongDescription }}
-        {{- else }}
-          <p>{{ .Description }}</p>
-        {{- end }}
-        <dl>
-          {{- with .MainProgram }}
-            <dt>Main Program</dt>
-            <dd>
-              <code>{{ . }}</code>
-            </dd>
-          {{- end }}
-          {{- with .Homepages }}
-            <dt>Homepage</dt>
-            <dd>
-              {{- range . }}
-                <a href="{{ . }}">{{ . }}</a>
-              {{- end }}
-            </dd>
-          {{- end }}
-          {{- with .Version }}
-            <dt>Version</dt>
-            <dd>{{ . }}</dd>
-          {{- end }}
-          {{- with .Licenses }}
-            <dt>License</dt>
-            <dd>
-              {{- range . }}
-                {{- if .URL }}
-                  <a href="{{ .URL }}">{{ or .FullName .Name }}</a>
-                {{- else }}
-                  {{ or .FullName .Name }}
-                {{- end }}
-                {{- with .AppendixURL }}
-                  <a href="{{ . }}">Appendix</a>
-                {{- end }}
+  <table>
+    <thead>
+      <tr>
+        <th scope="col">Attribute</th>
+        <th scope="col">Name</th>
+        <th scope="col">Description</th>
+      </tr>
+    </thead>
+    <tbody>
+      {{- range . }}
+        {{- with .Data }}
+          <tr>
+            <td>
+              {{- with .Attribute }}
+                <a href="{{ . }}" class="open-dialog">{{ . }}</a>
               {{- end }}
-            </dd>
-          {{- end }}
-          {{- with .Maintainers }}
-            <dt>Maintainer{{ if gt (len .) 1 }}s{{ end }}</dt>
-            <dd>
-              {{- range . }}
-                {{- if .Github }}
-                  <a href="https://github.com/{{ .Github }}">{{ .Name }}</a>
-                {{- else }}
-                  {{ .Name }}
-                {{- end }}
-              {{- end }}
-            </dd>
-          {{- end }}
-          {{- with .Definition }}
-            <dt>Defined</dt>
-            <dd>
-              <a href="{{ . }}">Source</a>
-            </dd>
-          {{- end }}
-        </dl>
-      </details>
-    {{- end }}
-  {{- end }}
+            </td>
+            <td>
+              {{ .Name }}
+            </td>
+            <td>
+              {{ .Description }}
+            </td>
+          </tr>
+        {{- end }}
+      {{- end }}
+    </tbody>
+  </table>
 {{- end }}
diff --git a/frontend/templates/blocks/results.gotmpl b/frontend/templates/blocks/results.gotmpl
index c375156..ef6e1f1 100644
--- a/frontend/templates/blocks/results.gotmpl
+++ b/frontend/templates/blocks/results.gotmpl
@@ -1,21 +1,19 @@
 {{- define "results" }}
   {{- with .Results }}
     {{- if gt .Total 0 }}
-      <section id="results" role="list" aria-label="search results">
-        {{ block "hits" .Hits }}
-        {{ end }}
-        <footer aria-label="pagination">
-          <nav id="pagination">
-            {{- with $.Prev }}
-              <a class="button" href="{{ . }}" rel="prev">Prev</a>
-            {{- end }}
-            {{- with $.Next }}
-              <a class="button" href="{{ . }}" rel="next">Next</a>
-            {{- end }}
-          </nav>
-          <span role="status">{{ .Total }} results</span>
-        </footer>
-      </section>
+      {{ block "hits" .Hits }}
+      {{ end }}
+      <footer aria-label="pagination">
+        <nav id="pagination">
+          {{- with $.Prev }}
+            <a class="button" href="{{ . }}" rel="prev">Prev</a>
+          {{- end }}
+          {{- with $.Next }}
+            <a class="button" href="{{ . }}" rel="next">Next</a>
+          {{- end }}
+        </nav>
+        <span role="status">{{ .Total }} results</span>
+      </footer>
     {{- else }}
       <span role="status">Nothing found</span>
     {{- end }}
diff --git a/frontend/templates/blocks/search.gotmpl b/frontend/templates/blocks/search.gotmpl
index 9320376..93ae545 100644
--- a/frontend/templates/blocks/search.gotmpl
+++ b/frontend/templates/blocks/search.gotmpl
@@ -16,9 +16,14 @@
       <button>Search</button>
     </fieldset>
   </form>
-  {{- if .Results }}
-    {{ block "results" . }}{{ end }}
-  {{- end }}
+  <section id="results" role="list" aria-label="search results">
+    {{- if .Results }}
+      {{ block "results" . }}{{ end }}
+    {{- end }}
+  </section>
+  <dialog id="dialog">
+    <button autofocus>Close</button>
+  </dialog>
 {{- end }}
 
 {{- define "head" }}
diff --git a/internal/server/templates.go b/internal/server/templates.go
index 8967599..38ff5d4 100644
--- a/internal/server/templates.go
+++ b/internal/server/templates.go
@@ -7,6 +7,7 @@ import (
 	"io/fs"
 	"log/slog"
 	"path"
+	"regexp"
 	"searchix/frontend"
 	"searchix/internal/config"
 	"searchix/internal/nix"
@@ -19,10 +20,20 @@ import (
 
 type TemplateCollection map[string]*template.Template
 
-var md = goldmark.New(
-	goldmark.WithExtensions(extension.NewLinkify()),
+var (
+	md = goldmark.New(
+		goldmark.WithExtensions(extension.NewLinkify()),
+	)
+	firstSentenceRegexp = regexp.MustCompile(`^.*?\.[[:space:]]`)
 )
 var templateFuncs = template.FuncMap{
+	"firstSentence": func(input nix.Markdown) nix.Markdown {
+		if fs := firstSentenceRegexp.FindString(string(input)); fs != "" {
+			return nix.Markdown(fs)
+		}
+
+		return input
+	},
 	"markdown": func(input nix.Markdown) template.HTML {
 		var out strings.Builder
 		err := md.Convert([]byte(input), &out)