diff options
-rw-r--r-- | static/search.js | 111 | ||||
-rw-r--r-- | templates/index.html | 25 |
2 files changed, 134 insertions, 2 deletions
diff --git a/static/search.js b/static/search.js new file mode 100644 index 0000000..02f1814 --- /dev/null +++ b/static/search.js @@ -0,0 +1,111 @@ +// Copyright (c) 2021 Johannes Stoelp +// file: search.js + +const formatResultPreview = (body, terms) => { + let body_idx = 0; + + // Collect indexes into `body` that match any term. + let matches = body.toLowerCase().split(" ").map(word => { + let idx = body_idx; + + // Update global index into body. + body_idx += word.length + 1 /* for the ' ' split char */; + + if (terms.some(term => { return word.startsWith(term); })) { + return { idx : idx, end : idx + word.length }; + } else { + return null; + } + }).filter(match => { return match !== null; }); + + // Format preview, by making each term bold and adding a short slice + // following after the term. + let preview = []; + matches.forEach(match => { + let end = Math.min(match.end + 40, body.length - 1); + preview.push(`<b>${body.substring(match.idx, match.end)}</b>` + + body.substring(match.end, end) + + "..."); + }); + + return preview.join(" "); +} + +const formatResult = (result, terms) => { + return "<div>" + + `<a href="${result.ref}">${result.doc.title}</a>` + + `<div>${formatResultPreview(result.doc.body, terms)}</div>` + + "</div>"; +} + +const evaluteSearch = (ctx) => { + let term = ctx.input.value.trim(); + + // Nothing todo if current search term is the same as last one. + if (term === ctx.last_search) { + return; + } + + // Empty search term, show list of blog pages again. + if (term === "") { + ctx.pages.style.display = "block"; + ctx.results.style.display = "none"; + return; + } + + // Search term entered, show result list and hide list with blog posts. + ctx.pages.style.display = "none"; + ctx.results.style.display = "block"; + + // Perform actual search. + let results = ctx.search_index.search(term, { + bool: "AND", + expand: true, // Also match if words starts with search query. + fields: { + title: {boost: 2}, + body : {boost: 1}, + } + }); + + if (results.length !== 0) { + // Update last search term. + ctx.last_search = term; + + // Reset HTML of results items (previous search results). + ctx.results_items.innerHTML = ""; + // Generate HTML for search results. + results.forEach(result => { + let item = document.createElement("li"); + item.innerHTML = formatResult(result, term.split(" ")); + ctx.results_items.appendChild(item); + }); + } +}; + +const debounce = (func, wait) => { + let timeout = null; + return () => { + clearTimeout(timeout); + timeout = setTimeout(func , wait); + }; +} + +window.onload = () => { + let ctx = new class { + constructor() { + // Get HTML DOM elements. + this.input = document.getElementById("search"); + this.results = document.getElementById("search-results"); + this.results_items = document.getElementById("search-results-items"); + this.pages = document.getElementById("pages"); + // Load search index. + this.search_index = elasticlunr.Index.load(window.searchIndex); + // Last search term. + this.last_search = ""; + } + }; + + if (ctx.search_index) { + ctx.input.onkeyup = debounce(() => { evaluteSearch(ctx); }, 200); + } +} diff --git a/templates/index.html b/templates/index.html index bc5cca2..ad0d413 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,3 +1,5 @@ +<!-- Override derived from hyde theme: https://github.com/getzola/hyde.git --> + <!DOCTYPE html> <html lang="en"> <head> @@ -44,14 +46,24 @@ {% endfor %} {% endblock sidebar_nav %} </ul> + + <!-- johannst START --> + <div class="search-container"> + <input id="search" type="search" placeholder="🔎 Search"> + </div> + <!-- johannst END --> </div> </div> {% endblock sidebar %} - <div class="content container"> + <!-- johannst: START | add id --> + <div class="content container" id="pages"> + <!-- johannst: END --> {% block content %} <div class="posts"> - {% for page in section.pages | reverse %} + <!-- johannst: START | remove `reverse` --> + {% for page in section.pages %} + <!-- johannst: END --> <div class="post"> <h1 class="post-title"> <a href="{{ page.permalink }}"> @@ -66,6 +78,15 @@ {% endblock content %} </div> + <!-- johannst START --> + <div class="content container" id="search-results"> + <div id="search-results-items"></div> + </div> + + <script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script> + <script type="text/javascript" src="{{ get_url(path="search_index.en.js") }}"></script> + <script type="text/javascript" src="{{ get_url(path="search.js") }}"></script> + <!-- johannst END --> </body> </html> |