Instant Search with Twitter Bootstrap, Jekyll, JSON, and jQuery

Monday, May 12, 2014

By default, the Twitter Bootstrap Search Field will use Google to search your site. It works (assuming Google has indexed your site), but it’s not very sexy. Of course, with a little know how, the search field can be wired to do whatever you want. I wanted a search field that functioned like Google Instant Search with the following features:

  • On page load, the search field is automatically focused for quick searching
  • As more characters are typed the search results becomes more filtered
  • Be able to navigate the search results with the arrow keys
  • Hide the search results if there is a left mouse click outside of the search results drop down menu
  • If there are no search results, display “No results found”

I was able to piece all of this together using the work already done by the folks on the following websites:

Now, let’s get to work. The following sections detail everything needed to wire up the instant search field.

The JSON File

Because Jekyll is a static-site generator, you do not have a database to query. For this whole thing to work, you need some sort of file to search through. This is where JSON comes in.

Jekyll will generate a JSON file that will contain every post with its associative metadata. This associative metadata could also be incorporated into the search, but for this example only the title and href attributes are used.

Create file search.json in your root Jekyll directory with the following contents:

---
---
[
    {% for post in site.posts %}
    {
      "title"    : "{{ post.title }}",
      "href"     : "{{ post.url }}",
      "date"     : {
         "day"   : "{{ post.date | date: "%d" }}",
         "month" : "{{ post.date | date: "%B" }}",
         "year"  : "{{ post.date | date: "%Y" }}"
      }
    }
    {% unless forloop.last %},{% endunless %}
    {% endfor %}
]

Each time you run jekyll build, a search.json file will be generated in the Jekyll _site folder containing all of your posts and their associative metadata that you can search through using jQuery.

The jQuery Code

With the JSON file created, you now need the actual code in place to parse and search through the file.

First, make sure you have already loaded the jQuery library within the head tags of your website. If you are using Twitter Bootstrap this should already be in place, but if you don’t here is an example (the missing http: in the URL is not a typo):

<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>

Second, put the following jQuery code within the head tags of your website:

<script type="text/javascript">

    function getSearchJSON()
    {
        $.getJSON("/search.json", function(e) {
            console.log("[search.json loaded for instant search]");

            $("#search_results").html("");

            searchJSON = e;
        });
    }

    function doSearch(e)
    {
        results = [];

        if (e != "")
        {
            $.each(searchJSON, function(t, n) {
                var r = n.title, i = n.title.toLowerCase(), s = n.href, o = n.date;
                i.indexOf(e)!==-1 && results.push([r, s, o])
            });

            printResults();
        }
        else
        {
            $("#search_results").html();
            results = [];
            printResults();
        }
    }

    function printResults()
    {
        var e = $("#search_results");

        e.html("");

        e.html(function() {
            if (results.length == 0)
            {
                e.append('<li style="padding-top: 3px; padding-bottom: 3px"><a style="color: #999; word-wrap: break-word; white-space: normal" href="#">No results found</a></li>');
            }
            else
            {
                $.each(results, function(t, n) {
                    e.append('<li style="padding-top: 3px; padding-bottom: 3px"><a style="color: #999; word-wrap: break-word; white-space: normal" href="' + n[1] + '">' + n[0] + '</a></li>');
                });
            }
        });
    }

    // Show the dropdown menu as long as there are characters in the text field
    function checkTextField()
    {
        // If the value of id search_input is not empty show id search_results otherwise hide it
        if ($('#search_input').val() != '')
        {
            $('#search_results').show();
        }
        else
        {
            $('#search_results').hide();
        }
    }

    // Hide the dropdown menu if there is a left mouse click outside of it
    $(document).mouseup(function (e)
    {
        var container = $("#search_results");

        // if the target of the click isn't the
        // container nor a descendant of the container
        if (!container.is(e.target) && container.has(e.target).length === 0)
        {
            container.hide();
        }
    });

    $(document).ready(function() {
        // Create the search index on page load
        getSearchJSON();

        // Continually update search results as characters are typed
        $("#search_input").keyup(function() {
            // Make search inputs are case insensitive
            var e = $(this).val().toLowerCase();

            // Do the actual search
            doSearch(e);
        });
    });

</script>

The HTML Markup

With the JSON file created and the jQuery code in place, the last piece is the HTML markup.

I rely entirely on Twitter Bootstrap for the styling of the search field and the search results drop down menu. You will probably need to apply additional styling to incorporate the following HTML markup into your site.

In my particular case, I put the search field within the navigation of my website, but you could just as easily put it elsewhere (if you do be sure to remove the navigation tags and classes). I have truncated () the unnecessary parts of the HTML markup that only applies to my website.

<nav class="navbar navbar-default" role="navigation">
    <div class="container">
        ...
        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <div class="nav navbar-nav navbar-form dropdown">
                <!-- Search text box -->
                <input id="search_input" data-toggle="dropdown" type="text" class="form-control" placeholder="Search" autofocus="autofocus" autocomplete="off" onkeyup="checkTextField();" />
                <!-- Search results styled as a dropdown menu -->
                <ul id="search_results" class="dropdown-menu" role="menu">
                </ul>
            </div>
            ...
        </div><!-- /.navbar-collapse -->
    </div>
</nav>

Current Problems

All of the above code and markup accomplishes what I sought out to get working. However, there is one small problem with the current implementation which has to do with navigating the search results using the arrow keys.

When you use Twitter Bootstrap’s drop down menus it provides the functionality to navigate that drop down menu with the arrow keys. I use this functionality to navigate the search results, and it works, but the way I use it was not how it was intended to be used. The drop down menu is intended to be activated (i.e. shown) by clicking the drop down menu’s header link. It is not intended to be activated by typing in a text field.

Despite this, the functionality works with the only caveat of having to hit the down arrow twice to navigate the search results. This happens because the first hit of the down arrow is actually activating the drop down menu (which is already displayed because of the checkTextField jQuery function). The second hit of the down arrow will then navigate the search results.

At some point I hope to fix this by removing the data-toggle=”dropdown” function in the input HTML tag and writing custom jQuery code to handle navigating the search results with the arrow keys.

References

Use jQuery to hide a DIV when the user clicks outside of it



comments powered by Disqus