diff options
-rw-r--r-- | frontend/static/search.js | 31 | ||||
-rw-r--r-- | frontend/templates/blocks/options.gotmpl | 112 | ||||
-rw-r--r-- | frontend/templates/blocks/search.gotmpl | 17 | ||||
-rw-r--r-- | frontend/templates/index.gotmpl | 13 | ||||
-rw-r--r-- | internal/server/server.go | 11 | ||||
-rw-r--r-- | internal/server/templates.go | 24 |
6 files changed, 128 insertions, 80 deletions
diff --git a/frontend/static/search.js b/frontend/static/search.js index f034fe1..60fa30d 100644 --- a/frontend/static/search.js +++ b/frontend/static/search.js @@ -1,5 +1,10 @@ const search = document.getElementById("search"); -const results = document.getElementById("results"); +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: [], @@ -24,7 +29,7 @@ function detailsToggled(ev) { state.url = nextURL.toJSON(); history.replaceState(state, "", nextURL); } -function addToggleEventListeners() { +function addToggleEventListeners(results) { results.querySelectorAll("details").forEach((details) => // toggle event doesn't bubble :( details.addEventListener("toggle", detailsToggled, { passive: true }), @@ -49,18 +54,25 @@ search.addEventListener("submit", function (ev) { } }) .then(function (html) { - results.innerHTML = - escapePolicy !== null ? escapePolicy.createHTML(html) : html; - addToggleEventListeners(); + const fragment = range.createContextualFragment( + escapePolicy !== null ? escapePolicy.createHTML(html) : html, + ); + const results = fragment.firstElementChild; + range.deleteContents(); + range.insertNode(results); + addToggleEventListeners(results); }) .catch(function (error) { - results.innerText = error.message; + range.deleteContents(); + range.insertNode(new Text(error.message)); console.error("fetch failed", error); }); ev.preventDefault(); }); -addToggleEventListeners(); +if (results !== null) { + addToggleEventListeners(results); +} if (state.opened.length > 0) { state.opened.forEach((id) => @@ -71,7 +83,8 @@ if (state.opened.length > 0) { } addEventListener("popstate", function (ev) { - if (ev.state == null || ev.state.url.pathname == "/") { - results.replaceChildren(); + if (ev.state == null || ev.state.url.pathname.startsWith("/search/")) { + range.deleteContents(); + search.reset(); } }); diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl index 2f9d88a..e39d60c 100644 --- a/frontend/templates/blocks/options.gotmpl +++ b/frontend/templates/blocks/options.gotmpl @@ -1,59 +1,61 @@ -{{ template "results" . }} -{{ define "results" }} - {{- with .Results }} - {{- range .Results }} - <details id="{{ .Option }}"> - <summary> - {{ .Option }} - </summary> - <p> - {{ markdown .Description }} - </p> - <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> +{{- template "results" .Results -}} +{{- define "results" }} + <div id="results"> + {{- with . }} + {{- range .Results }} + <details id="{{ .Option }}"> + <summary> + {{ .Option }} + </summary> + <p> + {{ markdown .Description }} + </p> + <dl> + {{- with .Type }} + <dt>Type</dt> + <dd><code>{{ . }}</code></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> + {{- with .Default }} + {{- if or .Text .Markdown }} + <dt>Default</dt> + <dd> + {{- if .Markdown }} + {{ markdown .Markdown }} + {{- else }} + <pre><code>{{ .Text }}</code></pre> + {{- end }} + </dd> + {{- end }} {{- end }} - {{- end }} - {{- with .RelatedPackages }} - <dt>Related Packages</dt> - <dd>{{ . }}</dd> - {{- end }} - {{- with .Declarations }} - <dt>Declared</dt> - {{- range . }} - <dd> - <a href="{{ .URL }}">{{ .Name }}</a> - </dd> + {{- with .Example }} + {{- if or .Text .Markdown }} + <dt>Example</dt> + <dd> + {{- if .Markdown }} + {{ markdown .Markdown }} + {{- else }} + <pre><code>{{ .Text }}</code></pre> + {{- end }} + </dd> + {{- end }} {{- end }} - {{- end }} - </dl> - </details> - {{- else }} - Nothing found + {{- 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> + {{- else }} + Nothing found + {{- end }} {{- end }} - {{- end }} -{{ end }} + </div> +{{- end }} diff --git a/frontend/templates/blocks/search.gotmpl b/frontend/templates/blocks/search.gotmpl new file mode 100644 index 0000000..5482e6b --- /dev/null +++ b/frontend/templates/blocks/search.gotmpl @@ -0,0 +1,17 @@ +{{- template "main" . }} +{{- template "js" . }} + +{{- define "main" }} + <label for="query">Search</label> + <form id="search" action="/options/results"> + <input id="query" name="query" type="search" value="{{ .Query }}" /> + <button>Search</button> + </form> + {{- with .Results }} + {{ block "results" . }}{{ end }} + {{- end }} +{{- end }} + +{{- define "js" }} + <script src="/static/search.js" defer></script> +{{- end }} diff --git a/frontend/templates/index.gotmpl b/frontend/templates/index.gotmpl index d5046e1..3c2840a 100644 --- a/frontend/templates/index.gotmpl +++ b/frontend/templates/index.gotmpl @@ -13,18 +13,17 @@ <p>Search Nix Packages and options from NixOS, Darwin and Home-Manager</p> <nav> <a href="/">Home</a> + <a href="/search/nixos">NixOS Options</a> </nav> </header> <main> - <label for="query">Search </label> - <form id="search" action="/options/results"> - <input id="query" name="query" type="search" value="{{ .Query }}" /> - <button>Search</button> - </form> - <div id="results">{{ block "results" . }}{{ end }}</div> + {{ block "main" . }} + <p>Hello!</p> + {{ end }} </main> <footer>Made by <a href="https://alanpearce.eu">Alan Pearce</a></footer> - <script src="/static/search.js" defer></script> + {{ block "js" . }} + {{ end }} {{ .LiveReload }} </body> </html> diff --git a/internal/server/server.go b/internal/server/server.go index 3f87b9d..c0fbfd0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -57,6 +57,7 @@ const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203 type TemplateData struct { LiveReload template.HTML Query string + Results bool } type ResultData[T options.NixOption] struct { @@ -120,6 +121,14 @@ func New(runtimeConfig *Config) (*Server, error) { } }) + mux.HandleFunc("/search/{source}", func(w http.ResponseWriter, r *http.Request) { + log.Println(r.PathValue("source")) + err := templates["search"].Execute(w, indexData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + timeout := 1 * time.Second mux.HandleFunc("/options/results", func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeoutCause(r.Context(), timeout, errors.New("timeout")) @@ -141,7 +150,7 @@ func New(runtimeConfig *Config) (*Server, error) { } if r.Header.Get("Fetch") == "true" { w.Header().Add("Content-Type", "text/html; charset=utf-8") - err = templates["options"].Execute(w, tdata) + err = templates["options"].ExecuteTemplate(w, "options.gotmpl", tdata) } else { err = templates["options"].ExecuteTemplate(w, "index.gotmpl", tdata) } diff --git a/internal/server/templates.go b/internal/server/templates.go index 71fc5c7..8755e36 100644 --- a/internal/server/templates.go +++ b/internal/server/templates.go @@ -34,14 +34,20 @@ var templateFuncs = template.FuncMap{ }, } -func loadTemplate(filenames ...string) (*template.Template, error) { - tpl := template.New(path.Base(filenames[0])) +func loadTemplate(layoutFile string, filename string) (*template.Template, error) { + tpl := template.New("index.gotmpl") tpl.Funcs(templateFuncs) - - _, err := tpl.ParseFiles(filenames...) + _, err := tpl.ParseFiles(layoutFile) if err != nil { - return nil, errors.WithMessage(err, "could not parse template") + return nil, errors.WithMessage(err, "could not parse layout template") + } + + if filename != "" { + _, err = tpl.ParseGlob(filename) + if err != nil { + return nil, errors.WithMessage(err, "could not parse template") + } } return tpl, nil @@ -53,21 +59,23 @@ func loadTemplates() (TemplateCollection, error) { layoutFile := path.Join(templateDir, "index.gotmpl") - index, err := loadTemplate(layoutFile) + index, err := loadTemplate(layoutFile, "") if err != nil { return nil, err } templates["index"] = index - templatePaths, err := filepath.Glob(path.Join(templateDir, "blocks", "*.gotmpl")) + glob := path.Join(templateDir, "blocks", "*.gotmpl") + templatePaths, err := filepath.Glob(glob) if err != nil { return nil, errors.WithMessage(err, "could not glob block templates") } for _, fullname := range templatePaths { - tpl, err := loadTemplate(fullname, layoutFile) + tpl, err := loadTemplate(layoutFile, glob) if err != nil { return nil, err } + name, _ := strings.CutSuffix(path.Base(fullname), ".gotmpl") templates[name] = tpl } |