aboutsummaryrefslogtreecommitdiffhomepage
path: root/static/search.js
blob: be67f2d12595dd1a3014678372c79aa3a5ae3879 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 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 => word.startsWith(term))) {
            return { idx : idx, end : idx + word.length };
        } else {
            return null;
        }
    }).filter(match => 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 evaluteSearchTerm = (term, ctx) => {
    // 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 = {
        // Get HTML DOM elements.
        results : document.getElementById("search-results"),
        results_items : document.getElementById("search-results-items"),
        pages : document.getElementById("pages"),
        // Load search index.
        search_index : elasticlunr.Index.load(window.searchIndex),
        // Last search term.
        last_search : null,
    };

    if (ctx.search_index) {
        const input = document.getElementById("search");
        input.onkeyup = debounce(() => evaluteSearchTerm(input.value.trim(), ctx), 200);
    }
}