nitdoc: improves quicksearch
authorAlexandre Terrasa <alexandre@moz-code.org>
Thu, 16 Jan 2014 02:44:23 +0000 (21:44 -0500)
committerAlexandre Terrasa <alexandre@moz-code.org>
Thu, 16 Jan 2014 02:44:23 +0000 (21:44 -0500)
Basic match is made on the begining of the string:
* first we select results with startsWith(substring)
* then we apply ranking using Dice coefficient
* rank bonus is given to exact match

If there is no match found:
* we rank all the entries and sort them
* entries with a Dice coefficient = 0 are trimmed

Also made some improvements on the GUI:
* results are grouped by name
* user can scroll the results list
* better handling of arrows in the search text field

Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

share/nitdoc/scripts/Nitdoc.QuickSearch.js
share/nitdoc/styles/Nitdoc.QuickSearch.css

index 25e3b94..3d818e7 100644 (file)
@@ -39,8 +39,11 @@ Nitdoc.QuickSearch = function() {
                        value: "quick search..."
                })
                .addClass("nitdoc-qs-field-notused")
+               .keydown(function(event) {
+                       return Nitdoc.QuickSearch.doKeyDownAction(event.keyCode);
+               })
                .keyup(function(event) {
-                       Nitdoc.QuickSearch.doKeyAction(event.keyCode);
+                       Nitdoc.QuickSearch.doKeyUpAction(event.keyCode);
                })
                .focusout(function() {
                        if($(this).val() == "") {
@@ -63,19 +66,28 @@ Nitdoc.QuickSearch = function() {
 
                // Close quicksearch list on click
                $(document).click(function(e) {
+                       console.log(e);
                        Nitdoc.QuickSearch.closeResultsTable();
                });
        }
 
-       // Respond to key event
-       var doKeyAction = function(key) {
+       var doKeyDownAction = function(key) {
                switch(key) {
                        case 38: // Up
                                selectPrevResult();
-                       break;
-
+                               return false;
                        case 40: // Down
                                selectNextResult();
+                               return false;
+                       default:
+                               return true;
+                }
+       }
+
+       var doKeyUpAction = function(key) {
+               switch(key) {
+                       case 38: // Up
+                       case 40: // Down
                        break;
 
                        case 13: // Enter
@@ -93,32 +105,55 @@ Nitdoc.QuickSearch = function() {
                                if(!query) {
                                        return false;
                                }
-                               var results = rankResults(query);
-                               results.sort(resultsSort);
+                               var results = getResults(query);
                                displayResultsTable(query, results);
                        break;
                }
        }
 
-       // Rank raw list entries corresponding to query
-       var rankResults = function(query) {
-               var results = new Array();
+       // Get results corresponding to search query
+       var getResults = function(query) {
+               var results = {};
+               results.matches = new Array();
                for(var entry in rawList) {
-                       for(var i in rawList[entry]) {
-                               var result = rawList[entry][i];
-                               result.entry = entry;
-                               result.distance = query.dice(entry);
-                               results[results.length] = result;
+                       if(!entry.startsWith(query, true)) {
+                               continue;
+                       }
+                       var cat = new Object();
+                       cat.name = entry;
+                       cat.entries = rawList[entry];
+                       results.matches[results.matches.length] = cat;
+
+                       if(entry == query) {
+                               cat.rank = 3;
+                       } else if(entry.toUpperCase() == query.toUpperCase()) {
+                               cat.rank = 2;
+                       } else {
+                               cat.rank = 1 + query.dice(entry);
                        }
                }
+               results.matches.sort(rankSorter);
+               results.partials = new Array();
+               if(results.matches.length == 0) {
+                       for(var entry in rawList) {
+                               var cat = new Object();
+                               cat.name = entry;
+                               cat.entries = rawList[entry];
+                               cat.rank = query.dice(entry);
+                               if(cat.rank > 0) {
+                                       results.partials[results.partials.length] = cat;
+                               }
+                       }
+                       results.partials.sort(rankSorter);
+               }
                return results;
        }
 
-       // Sort an array of results
-       var resultsSort = function(a, b){
-               if(a.distance < b.distance) {
+       // Sort an array of results by rank
+       var rankSorter = function(a, b){
+               if(a.rank < b.rank) {
                        return 1;
-               } else if(a.distance > b.distance) {
+               } else if(a.rank > b.rank) {
                        return -1;
                }
                return 0;
@@ -132,91 +167,173 @@ Nitdoc.QuickSearch = function() {
                // Build results table
                currentIndex = -1;
                currentTable = $(document.createElement("table"));
+               currentTable.attr("id", "nitdoc-qs-table");
+               currentTable.css("position", "absolute");
+               currentTable.width(searchField.outerWidth());
 
-               for(var i in results) {
-                       if(i > 10) {
-                               break;
+               var maxSize = 10;
+               var count = 0;
+               var resultSet;
+               if(results.matches.length == 0) {
+                       resultSet = results.partials
+               } else {
+                       resultSet = results.matches
+               }
+               for(var i in resultSet) {
+                       var cat = resultSet[i];
+                       var result = cat.entries[0];
+
+                       addResultRow(count, cat.name, result.txt, result.url, "nitdoc-qs-cat")
+                       if(count >= maxSize) {
+                               currentTable.find("tbody").children().last().hide();
                        }
-                       var result = results[i];
-                       currentTable.append(
+                       count++;
+
+                       for(var j = 1; j < cat.entries.length; j++) {
+                               var result = cat.entries[j];
+                               addResultRow(count, cat.name, result.txt, result.url, "nitdoc-qs-sub")
+                               if(count >= maxSize) {
+                                       currentTable.find("tr.nitdoc-qs-row").last().hide();
+                               }
+                               count++;
+                       }
+               }
+               if(count >= maxSize) {
+                       currentTable.prepend(
                                $(document.createElement("tr"))
-                               .data("searchDetails", {name: result.entry, url: result.url})
-                               .data("index", i)
-                               .append($(document.createElement("td")).html(result.entry))
+                               .addClass("nitdoc-qs-overflow-up")
+                               .addClass("nitdoc-qs-overflow-inactive")
                                .append(
                                        $(document.createElement("td"))
-                                               .addClass("nitdoc-qs-info")
-                                               .html(result.txt + "&nbsp;&raquo;")
+                                       .attr("colspan", 2)
+                                       .html("&#x25B2;")
                                )
-                               .mouseover( function() {
-                                       $(currentTable.find("tr")[currentIndex]).removeClass("nitdoc-qs-active");
-                                       $(this).addClass("nitdoc-qs-active");
-                                       currentIndex = $(this).data("index");
+                               .click( function(e) {
+                                       e.stopPropagation();
+                                       selectPrevResult();
                                })
-                               .mouseout( function() {
-                                       $(this).removeClass("nitdoc-qs-active");
-                                })
-                               .click( function() {
-                                       window.location = $(this).data("searchDetails")["url"];
+                       );
+                       currentTable.append(
+                               $(document.createElement("tr"))
+                               .addClass("nitdoc-qs-overflow-down")
+                               .addClass(count >= maxSize ? "nitdoc-qs-overflow-active" : "nitdoc-qs-overflow-inactive")
+                               .append(
+                                       $(document.createElement("td"))
+                                       .attr("colspan", 2)
+                                       .html("&#x25BC;")
+                               )
+                               .click( function(e) {
+                                       e.stopPropagation();
+                                       console.log("nest");
+                                       selectNextResult();
                                })
                        );
                }
+               if(results.matches.length == 0) {
+                       currentTable.prepend(
+                               $("<tr class='nitdoc-qs-noresult'>")
+                               .append(
+                                       $("<td colspan='2'>")
+                                       .html("Sorry, there is no match, best results are:")
+                               )
+                       );
+               }
+
+               // Initialize table
+               $("body").append(currentTable);
+               resizeResultsTable();
+               if(currentTable.find("tr").length > 0) {
+                       setIndex(0);
+               }
+       }
+
+       // adds a result row to the current result table
+       var addResultRow = function(index, name, txt, url, cls) {
                currentTable.append(
-                       $("<tr class='nitdoc-qs-overflow'>")
+                       $(document.createElement("tr"))
+                       .addClass("nitdoc-qs-row")
+                       .data("searchDetails", {name: name, url: url})
+                       .data("index", index)
+                       .append(
+                               $(document.createElement("td")).html(name)
+                               .addClass(cls)
+                       )
                        .append(
-                               $("<td colspan='2'>")
-                               .html("Best results for '" + query + "'")
+                               $(document.createElement("td"))
+                                       .addClass("nitdoc-qs-info")
+                                       .html(txt + "&nbsp;&raquo;")
                        )
+                       .mouseover( function() {
+                               setIndex($(this).data("index"));
+                       })
+                       .mouseout( function() {
+                               $(this).removeClass("nitdoc-qs-active");
+                        })
+                       .click( function() {
+                               window.location = $(this).data("searchDetails")["url"];
+                       })
                );
+       }
 
-               // Initialize table properties
-               currentTable.attr("id", "nitdoc-qs-table");
-               currentTable.css("position", "absolute");
-               currentTable.width(searchField.outerWidth());
-               $("body").append(currentTable);
-               currentTable.offset({left: searchField.offset().left + (searchField.outerWidth() - currentTable.outerWidth()), top: searchField.offset().top + searchField.outerHeight()});
-               // Preselect first entry
-               if(currentTable.find("tr").length > 0) {
-                       currentIndex = 0;
-                       $(currentTable.find("tr")[currentIndex]).addClass("nitdoc-qs-active");
-                       searchField.focus();
-               }
+       // adapts result table to content
+       var resizeResultsTable = function() {
+               currentTable.offset({
+                       left: searchField.offset().left + (searchField.outerWidth() - currentTable.outerWidth()),
+                       top: searchField.offset().top + searchField.outerHeight()
+               });
+       }
+
+       // select row at index
+       var setIndex = function(index) {
+               $(currentTable.find("tr.nitdoc-qs-row")[currentIndex]).removeClass("nitdoc-qs-active");
+               currentIndex = index;
+               var currentRow = $(currentTable.find("tr.nitdoc-qs-row")[currentIndex]);
+               currentRow.addClass("nitdoc-qs-active");
+               //searchField.val(currentRow.data("searchDetails").name);
+       }
+
+       var hasPrev = function(index) {
+               return index - 1 >= 0;
+       }
+
+       var hasNext = function(index) {
+               return index + 1 < currentTable.find("tr.nitdoc-qs-row").length;
        }
 
-       // Select the previous result on current table
        var selectPrevResult = function() {
-               // If already on first result, focus search input
-               if(currentIndex == 0) {
-                       searchField.val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name);
-                       searchField.focus();
-               // Else select previous result
-               } else if(currentIndex > 0) {
-                       $(currentTable.find("tr")[currentIndex]).removeClass("nitdoc-qs-active");
-                       currentIndex--;
-                       $(currentTable.find("tr")[currentIndex]).addClass("nitdoc-qs-active");
-                       searchField.val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name);
-                       searchField.focus();
+               if(hasPrev(currentIndex)) {
+                       setIndex(currentIndex - 1);
+                       if(!$(currentTable.find("tr.nitdoc-qs-row")[currentIndex]).is(":visible")) {
+                               currentTable.find("tr.nitdoc-qs-row:visible").last().hide();
+                               currentTable.find("tr.nitdoc-qs-overflow-down").addClass("nitdoc-qs-overflow-active");
+                               $(currentTable.find("tr.nitdoc-qs-row")[currentIndex]).show();
+                               if(!hasPrev(currentIndex)) {
+                                       currentTable.find("tr.nitdoc-qs-overflow-up").removeClass("nitdoc-qs-overflow-active");
+                               }
+                               resizeResultsTable();
+                       }
                }
        }
 
-       // Select the next result on current table
        var selectNextResult = function() {
-               if(currentIndex < currentTable.find("tr").length - 1) {
-                       if($(currentTable.find("tr")[currentIndex + 1]).hasClass("nitdoc-qs-overflow")) {
-                               return;
+               if(hasNext(currentIndex)) {
+                       setIndex(currentIndex + 1);
+                       if(!$(currentTable.find("tr.nitdoc-qs-row")[currentIndex]).is(":visible")) {
+                               currentTable.find("tr.nitdoc-qs-row:visible").first().hide();
+                               currentTable.find("tr.nitdoc-qs-overflow-up").addClass("nitdoc-qs-overflow-active");
+                               $(currentTable.find("tr.nitdoc-qs-row")[currentIndex]).show();
+                               if(!hasNext(currentIndex)) {
+                                       currentTable.find("tr.nitdoc-qs-overflow-down").removeClass("nitdoc-qs-overflow-active");
+                               }
+                               resizeResultsTable();
                        }
-                       $(currentTable.find("tr")[currentIndex]).removeClass("nitdoc-qs-active");
-                       currentIndex++;
-                       $(currentTable.find("tr")[currentIndex]).addClass("nitdoc-qs-active");
-                       searchField.val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name);
-                       searchField.focus();
                }
        }
 
        // Load selected search result page
        var goToResult = function() {
                if(currentIndex > -1) {
-                       window.location = $(currentTable.find("tr")[currentIndex]).data("searchDetails").url;
+                       window.location = $(currentTable.find("tr.nitdoc-qs-row")[currentIndex]).data("searchDetails").url;
                        return;
                }
 
@@ -241,7 +358,8 @@ Nitdoc.QuickSearch = function() {
        // Public interface
        var quicksearch = {
                enableQuickSearch: enableQuickSearch,
-               doKeyAction: doKeyAction,
+               doKeyUpAction: doKeyUpAction,
+               doKeyDownAction: doKeyDownAction,
                closeResultsTable: closeResultsTable
        };
 
@@ -256,36 +374,11 @@ $(document).ready(function() {
  * Utils
  */
 
-// Calculate levenshtein distance beetween two strings
-// see: http://en.wikipedia.org/wiki/Levenshtein_distance
-String.prototype.levenshtein = function(other) {
-       var matrix = new Array();
-
-       for(var i = 0; i <= this.length; i++) {
-               matrix[i] = new Array();
-               matrix[i][0] = i;
+String.prototype.startsWith = function(prefix, caseSensitive) {
+       if(caseSensitive) {
+               return this.toUpperCase().indexOf(prefix.toUpperCase()) === 0;
        }
-       for(var j = 0; j <= other.length; j++) {
-               matrix[0][j] = j;
-       }
-       var cost = 0;
-       for(var i = 1; i <= this.length; i++) {
-               for(var j = 1; j <= other.length; j++) {
-                       if(this.charAt(i - 1) == other.charAt(j - 1)) {
-                               cost = 0;
-                       } else if(this.charAt(i - 1).toLowerCase() == other.charAt(j - 1).toLowerCase()) {
-                               cost = 0.5;
-                       } else {
-                               cost = 1;
-                       }
-                       matrix[i][j] = Math.min(
-                               matrix[i - 1][j] + 1, // deletion
-                               matrix[i][j - 1] + 1, // insertion
-                               matrix[i - 1][j - 1] + cost // substitution
-                       );
-               }
-       }
-       return matrix[this.length][other.length]
+    return this.indexOf(prefix) === 0;
 }
 
 // Compare two strings using Sorensen-Dice Coefficient
@@ -305,11 +398,7 @@ String.prototype.dice = function(other) {
                var bigram1 = this.substr(i, 2);
                for(var j = 0; j < length2; j++) {
                        if(bigram1 == bigrams2[j]) {
-                               intersection += 2;
-                               bigrams2[j] = null;
-                               break;
-                       } else if (bigram1 && bigrams2[j] && bigram1.toLowerCase() == bigrams2[j].toLowerCase()) {
-                               intersection += 1;
+                               intersection++;
                                bigrams2[j] = null;
                                break;
                        }
index 98f5728..b0a434c 100644 (file)
@@ -40,6 +40,9 @@
        border: 1px solid #E0E0E0;\r
        border-spacing: 0px;\r
        z-index: 1000;\r
+       -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
+       -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
+       box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\r
 }\r
 \r
 #nitdoc-qs-table .nitdoc-qs-active {\r
@@ -47,7 +50,7 @@
        background: #EEE;\r
 }\r
 \r
-#nitdoc-qs-table td {\r
+#nitdoc-qs-table td, th {\r
        white-space: nowrap;\r
        overflow: hidden;\r
        line-height: 22px;\r
        width: 25%;\r
 }\r
 \r
+#nitdoc-qs-table td.nitdoc-qs-sub {\r
+       color: #6C6C6C;\r
+       padding-left: 12px;\r
+}\r
+\r
 #nitdoc-qs-table td.nitdoc-qs-info {\r
        color: #0D8921;\r
        font-size: small;\r
        text-align: right;\r
 }\r
 \r
-#nitdoc-qs-table tr.nitdoc-qs-overflow td {\r
+#nitdoc-qs-table tr.nitdoc-qs-noresult td {\r
+       color: #6C6C6C;\r
+       font-size: small;\r
+       line-height: 15px;\r
+}\r
+\r
+#nitdoc-qs-table tr.nitdoc-qs-overflow-up td,\r
+#nitdoc-qs-table tr.nitdoc-qs-overflow-down td {\r
        text-align: center;\r
-       background-color: #E0E0E0;\r
+       font-size:      x-small;\r
+       line-height: 10px;\r
+       color: #FFF;\r
+       -webkit-touch-callout: none;\r
+       -webkit-user-select: none;\r
+       -khtml-user-select: none;\r
+       -moz-user-select: none;\r
+       -ms-user-select: none;\r
+       user-select: none;\r
 }\r
 \r
+#nitdoc-qs-table tr.nitdoc-qs-overflow-active td {\r
+       color: #0D8921;\r
+       cursor: pointer;\r
+}\r
+\r
+#nitdoc-qs-table tr.nitdoc-qs-overflow-active td:hover {\r
+       background-color: #E0E0E0;\r
+}\r