aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorjohannst <johannes.stoelp@gmail.com>2021-05-11 01:48:40 +0200
committerjohannst <johannes.stoelp@gmail.com>2021-05-12 00:03:09 +0200
commit53c9ba85bf18e53a7d998c048d10e578b5a19983 (patch)
treef047de9db752900f33192b8dc76baea02a25a5f5
parentbeac9714ef693f97feb68e1793fa1ad30d8a96e4 (diff)
downloadblog-53c9ba85bf18e53a7d998c048d10e578b5a19983.tar.gz
blog-53c9ba85bf18e53a7d998c048d10e578b5a19983.zip
added search input with simple search result preview
-rw-r--r--static/search.js111
-rw-r--r--templates/index.html25
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>