Changeset - 8fe010d0a605
[Not reviewed]
default
0 4 1
Branko Majic (branko) - 12 years ago 2013-10-20 23:12:29
branko@majic.rs
CONNT-23: Implemented as-you-type search suggestions, including keyboard navigation for selecting one of the options.
5 files changed with 250 insertions and 4 deletions:
0 comments (0 inline, 0 general)
conntrackt/static/conntrackt.js
Show inline comments
 
new file 100644
 
/**
 
 * Copyright (C) 2013 Branko Majic
 
 *
 
 * This file is part of Django Conntrackt.
 
 *
 
 * Django Conntrackt is free software: you can redistribute it and/or modify it
 
 * under the terms of the GNU General Public License as published by the Free
 
 * Software Foundation, either version 3 of the License, or (at your option) any
 
 * later version.
 
 *
 
 * Django Conntrackt is distributed in the hope that it will be useful, but
 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
 * details.
 
 *
 
 * You should have received a copy of the GNU General Public License along with
 
 * Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 

	
 
$(document).ready(function(){
 

	
 
    /**
 
     * BEGIN (search suggestions)
 
     */
 

	
 
    // Search input field in navigation bar.
 
    var searchField = $("#search");
 

	
 
    // Set-up the search suggestions as the user types-in the search query.
 
    searchField.keyup(function(){
 
        // Fetch the needed elements. The search suggestions element is kept
 
        // invisble by default via CSS.
 
        var searchField = $("#search");
 
        var searchSuggestions = $("#search-suggestions");
 
        var searchSuggestionsList = searchSuggestions.find("ul");
 

	
 
        // Show the search suggestions only if the user has provided 2 or more
 
        // characters.
 
        if (searchField.val().length >= 2) {
 
            request = $.getJSON("/conntrackt/api/search/" + searchField.val())
 
                .done(function(data) {
 

	
 
                    // Process the retrieved JSON response, setting-up list of
 
                    // items that will be offered as suggestion.
 
                    var items = [];
 
                    $.each(data, function(index, item) {
 
                        if (item.type == "project") {
 
                            items.push('<li><a class="search-option" href="' + item.url + '">' + item.name + '<br/><small>Project</small></a></li>');
 
                        } else if (item.type == "entity") {
 
                            items.push('<li><a class="search-option" href="' + item.url + '">' + item.name + '<br/><small>from ' + item.project + '<small></a></li>');
 
                        }
 
                    });
 

	
 
                    // Add the suggestions (if any) to correct HTML element.
 
                    if (items.length) {
 
                        searchSuggestions.find("ul").empty().append(items);
 
                        searchSuggestions.fadeIn(200);
 

	
 
                        // Set-up up/down arrow navigation through suggested
 
                        // elements.  Since we empty and add new elements on
 
                        // each change of list, this has to be done here (and
 
                        // not globally).
 
                        $(".search-option").keydown(function(e){
 
                            // Down arrow was pressed.
 
                            if(e.which == 40) {
 
                                $(this).parent().next().find(".search-option").focus();
 
                                // Stops the page from scrolling.
 
                                return false;
 
                            }
 
                            if(e.which == 38) {
 
                                // Up arrow was pressed. Focus the search box if
 
                                // there's no more search items up in the list.
 
                                var prev = $(this).parent().prev().find(".search-option");
 
                                if (prev.length) {
 
                                    prev.focus();
 
                                } else {
 
                                    $("#search").focus();
 
                                }
 
                                // Stops the page from scrolling
 
                                return false;
 
                            }
 
                        });
 

	
 
                        // Hide the suggestions if search options lose focus,
 
                        // and search field doesn't have the focus
 
                        // either. Timeout has to be used since focus change
 
                        // takes some time.
 
                        //
 
                        // @TODO: Figure out if this can be done without timeout, since it feels
 
                        // like race condition issue.
 
                        $(".search-option").blur(function(){
 
                            setTimeout(function() {
 
                                if ($(document.activeElement).attr('class') != "search") {
 
                                    $("#search-suggestions").fadeOut(200);
 
                                }
 
                            }, 10);
 
                        });
 
                    } else {
 
                        // No search results were returned (nothing matched).
 
                        searchSuggestions.find("ul").empty().append("<li class='disabled'><a href='#'>No matches</a></li>");
 
                        searchSuggestions.fadeIn(200);
 
                    }
 

	
 
                })
 
                .fail(function() {
 
                    // Show error message if search query failed.
 
                    searchSuggestions.find("ul").append("<li class='disabled'><a href='#'>Search error.</a></li>");
 
                });
 
        } else {
 
            // Hide the search suggestions if less than two characters were provided.
 
            searchSuggestions.fadeOut(200);
 
        }
 
    });
 

	
 
    // Focus the first search suggestion if arrow done is pressed while within
 
    // the search field.
 
    searchField.keydown(function(event){
 
        if (event.which == 40) {
 
            $(".search-option:first").focus();
 
            return false;
 
        }
 
    });
 

	
 
    // Hide the suggestions if search box loses focus, and no search suggestions
 
    // have the focus either. Timeout has to be used since focus change takes
 
    // some time.
 
    //
 
    // @TODO: Figure out if this can be done without timeout, since it feels
 
    // like race condition issue.
 
    searchField.blur(function(){
 
        setTimeout(function() {
 
            if ($(document.activeElement).attr('class') != "search-option") {
 
                $("#search-suggestions").fadeOut(200);
 
            }
 
        }, 10);
 
    });
 

	
 
    // Show the suggestions if search box gains focus, and more than two
 
    // characters were entered as search term previously.
 
    searchField.focus(function(){
 
        if ($("#search").val().length >= 2) {
 
            $("#search-suggestions").fadeIn(200);
 
        }
 
    });
 

	
 
    /**
 
     * END (search suggestions)
 
     */
 

	
 
});
 
\ No newline at end of file
conntrackt/static/custom.css
Show inline comments
 
@@ -15,4 +15,30 @@
 
 *
 
 * You should have received a copy of the GNU General Public License along with
 
 * Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 
\ No newline at end of file
 
 */
 

	
 
/**
 
 * BEGIN (search suggestions)
 
 */
 

	
 
/* Do not show search suggestions by default. */
 
#search-suggestions {
 
    display: none;
 
}
 
/* Make sure that the dropdown menu is visible by default (we hide it through
 
parent div instead). */
 
#search-suggestions .dropdown-menu {
 
    display: block;
 
    width: 100%;
 
}
 

	
 
/* Cut off long links/strings, using three periods at the end to mark it. */
 
#search-suggestions .dropdown-menu li a {
 
    text-overflow:ellipsis;
 
    overflow: hidden;
 
}
 

	
 
/**
 
 * END (search suggestions)
 
 */
 

	
conntrackt/templates/conntrackt/base.html
Show inline comments
 
@@ -38,7 +38,12 @@
 
                <form action="{% url "search" %}" class="navbar-search pull-left" method="GET">
 
                  <div class="input-prepend">
 
                    <button type="submit" class="btn btn-link"><span class="icon-search icon-white"></span></button>
 
                    <input class="span2 search-query" type="text" name="q"  placeholder="Search">
 
                    <input id="search" class="search-query" type="text" autocomplete="off" name="q"  placeholder="Search">
 
                    <div id="search-suggestions" class="dropdown">
 
                      <ul class="dropdown-menu">
 
                      </ul>
 
                    </div>
 

	
 
                  </div>
 
                </form>
 
              </li>
 
@@ -68,6 +73,7 @@
 
    </div>
 

	
 
    <script src="/static/jquery-min.js"></script>
 
    <script src="/static/conntrackt.js"></script>
 
    <script src="/static/bootstrap/js/bootstrap.js"></script>
 
  </body>
 
</html>
conntrackt/urls.py
Show inline comments
 
@@ -30,7 +30,7 @@ from .views import LocationCreateView, L
 
from .views import EntityCreateView, EntityUpdateView, EntityDeleteView
 
from .views import InterfaceCreateView, InterfaceUpdateView, InterfaceDeleteView
 
from .views import CommunicationCreateView, CommunicationUpdateView, CommunicationDeleteView
 
from .views import SearchView
 
from .views import SearchView, APISearchView
 

	
 

	
 
urlpatterns = patterns(
 
@@ -95,4 +95,7 @@ urlpatterns = patterns(
 

	
 
    # View for displaying the search page.
 
    url(r'^search/$', SearchView.as_view(), name="search"),
 

	
 
    # View for getting the search results in JSON format.
 
    url(r'^api/search/(?P<search_term>.*)$', APISearchView.as_view(), name="api_search"),
 
)
conntrackt/views.py
Show inline comments
 
@@ -22,6 +22,7 @@
 
# Standard library imports.
 
from StringIO import StringIO
 
from zipfile import ZipFile, ZIP_DEFLATED
 
import json
 

	
 
# Django imports.
 
from django.contrib.auth.decorators import permission_required
 
@@ -30,7 +31,7 @@ from django.core.urlresolvers import rev
 
from django.db.models import Q
 
from django.http import HttpResponse
 
from django.shortcuts import render_to_response, get_object_or_404
 
from django.views.generic import TemplateView, DetailView, CreateView, UpdateView, DeleteView
 
from django.views.generic import TemplateView, DetailView, CreateView, UpdateView, DeleteView, View
 

	
 
# Third-party application imports.
 
from braces.views import MultiplePermissionsRequiredMixin, SetHeadlineMixin
 
@@ -1017,3 +1018,63 @@ class SearchView(MultiplePermissionsRequ
 
            context['projects'] = Project.objects.search(search_term)
 

	
 
        return context
 

	
 

	
 
class APISearchView(MultiplePermissionsRequiredMixin, View):
 
    """
 
    API view implementing search for entities and projects that match the
 
    provided search term.
 

	
 
    The output generated by the view uses JSON. The result will include a list
 
    of matched items, where each item is a dictionary with the following keys:
 

	
 
      - name (name of the matched item)
 
      - project (project to which the item belongs)
 
      - type (type of the matched item)
 
      - url (URL towards the matched item)
 
    """
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.view",),
 
        }
 

	
 
    def get(self, request, search_term):
 
        """
 
        Implements response handling for a GET request.
 
        """
 

	
 
        # Retrieve the search term, and strip it if it was provided.
 
        if search_term:
 
            search_term = search_term.strip()
 

	
 
        # Set-up a list that will contain found items.
 
        items = []
 

	
 
        # Don't perform search with empty search term.
 
        if search_term != "":
 

	
 
            # Run the search on entities and projects.
 
            entities = Entity.objects.search(search_term).select_related("project")
 
            projects = Project.objects.search(search_term)
 

	
 
            # Add found entities.
 
            for entity in entities:
 
                items.append({"name": entity.name,
 
                              "project": entity.project.name,
 
                              "type": "entity",
 
                              "url": entity.get_absolute_url(),})
 

	
 
            # Add found projects.
 
            for project in projects:
 
                items.append({"name": project.name,
 
                              "project": project.name,
 
                              "type": "project",
 
                              "url": project.get_absolute_url(),})
 

	
 
        # Generate the JSON response.
 
        content = json.dumps(items)
 
        response = HttpResponse(content, mimetype="application/json")
 

	
 
        # Return the response.
 
        return response
0 comments (0 inline, 0 general)