diff options
Diffstat (limited to 'static')
-rw-r--r-- | static/search.js | 111 |
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); + } +} |