diff options
-rw-r--r-- | frontend/static/search.js | 118 | ||||
-rw-r--r-- | frontend/static/style.css | 22 | ||||
-rw-r--r-- | frontend/templates/blocks/options.gotmpl | 75 | ||||
-rw-r--r-- | frontend/templates/blocks/packages.gotmpl | 99 | ||||
-rw-r--r-- | frontend/templates/blocks/results.gotmpl | 28 | ||||
-rw-r--r-- | frontend/templates/blocks/search.gotmpl | 11 | ||||
-rw-r--r-- | 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 }} - <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) |