aboutsummaryrefslogtreecommitdiffhomepage
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/search.js111
1 files changed, 111 insertions, 0 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);
+ }
+}