Permalink
Browse files

Added fuzzysearch & a debounce on the search method to prevent reduce…

…d responsibility when typing fast into the searchbox
  • Loading branch information...
1 parent 386ef4b commit a75b6ef5649c5fb4232a40ef2f5191d0b57e1ede @LuukvE committed Apr 7, 2012
Showing with 214 additions and 61 deletions.
  1. +214 −61 src/list.js
View
@@ -48,7 +48,7 @@ var List = function(id, options, values) {
// Check if the container exists. If not return instead of breaking the javascript
if (!this.listContainer)
return;
-
+
this.items = [];
this.visibleItems = []; // These are the items currently visible
this.matchingItems = []; // These are the items currently matching filters and search, regadlessof visible count
@@ -57,7 +57,7 @@ var List = function(id, options, values) {
this.list = null;
this.templateEngines = {};
-
+
this.page = options.page || 200;
this.i = options.i || 1;
@@ -280,69 +280,77 @@ var List = function(id, options, values) {
* The columns parameter defines if all values should be included in the search,
* defaults to undefined which means "all".
*/
- this.search = function(searchString, columns, multiSearch) {
- self.i = 1; // Reset paging
- var searchArguments,
- foundArgument,
- matching = [],
- found,
- item,
- text,
- values,
- is,
- columns = (columns === undefined) ? self.items[0].values() : columns,
- searchString = (searchString === undefined) ? "" : searchString,
- target = searchString.target || searchString.srcElement; /* IE have srcElement */
-
- searchString = (target === undefined) ? (""+searchString).toLowerCase() : ""+target.value.toLowerCase();
- is = self.items;
-
- // Substract arguments from the searchString or put searchString as only argument
- searchArguments = multiSearch ? searchString.replace(/ +$/, '').split(/ +/) : [searchString];
-
-
- // Escape regular expression characters
- for(var i = 0; i < searchArguments.length; i++) {
- searchArguments[i] = searchArguments[i].replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
- }
-
- templater.clear();
- if (searchString === "") {
- reset.search();
- self.searched = false;
- self.update();
- } else {
- self.searched = true;
-
- for (var k = 0, kl = is.length; k < kl; k++) {
- found = true;
- item = is[k];
- values = item.values();
-
- for(var i = 0; i < searchArguments.length; i++) {
- foundArgument = false;
-
- for(var j in columns) {
- if(values.hasOwnProperty(j) && columns[j] !== null) {
- text = (values[j] != null) ? values[j].toString().toLowerCase() : "";
- if ((searchArguments[i] !== "") && (text.search(searchArguments[i]) > -1)) {
- foundArgument = true;
+ this.search = (function() {
+ var func = function(searchString, columns) {
+ self.i = 1; // Reset paging
+ var searchArguments,
+ foundArgument,
+ matching = [],
+ found,
+ item,
+ text,
+ values,
+ is,
+ multiSearch = (typeof options.multiSearch !== 'boolean') ? true : options.multiSearch,
+ columns = (columns === undefined) ? self.items[0].values() : columns,
+ searchString = (searchString === undefined) ? "" : searchString,
+ target = searchString.target || searchString.srcElement; /* IE have srcElement */
+
+ searchString = (target === undefined) ? (""+searchString).toLowerCase() : ""+target.value.toLowerCase();
+ is = self.items;
+
+ // Substract arguments from the searchString or put searchString as only argument
+ searchArguments = multiSearch ? searchString.replace(/ +$/, '').split(/ +/) : [searchString];
+
+ templater.clear();
+ if (searchString === "") {
+ reset.search();
+ self.searched = false;
+ self.update();
+ } else {
+ self.searched = true;
+
+ for (var k = 0, kl = is.length; k < kl; k++) {
+ found = true;
+ item = is[k];
+ values = item.values();
+
+ for(var i = 0; i < searchArguments.length; i++) {
+ foundArgument = false;
+
+ for(var j in columns) {
+ if(values.hasOwnProperty(j) && columns[j] !== null) {
+ text = (values[j] != null) ? values[j].toString().toLowerCase() : "";
+ if (this.fuzzySearch(text, searchArguments[i], options)) {
+ foundArgument = true;
+ }
}
}
- }
- if(!foundArgument) found = false;
- }
- if (found) {
- item.found = true;
- matching.push(item);
- } else {
- item.found = false;
+ if(!foundArgument) found = false;
+ }
+ if (found) {
+ item.found = true;
+ matching.push(item);
+ } else {
+ item.found = false;
+ }
}
+ self.update();
}
- self.update();
- }
- return self.visibleItems;
- };
+ return self.visibleItems;
+ },
+ timeout;
+
+ return function() {
+ var context = this, args = arguments;
+ var later = function() {
+ timeout = null;
+ func.apply(context, args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, 100);
+ };
+ }());
/*
* Filters the list. If filterFunction() returns False hides the Item.
@@ -602,6 +610,151 @@ List.prototype.templateEngines.standard = function(list, settings) {
};
};
+/**
+ * Locate the best instance of 'pattern' in 'text' using the Bitap algorithm.
+ *
+ * This method is a based on: Diff Match and Patch
+ * http://code.google.com/p/google-diff-match-patch/
+ * Author: fraser@google.com (Neil Fraser)
+ * Modifications by: l.vanegeraat@gmail.com (Luuk van Egeraat)
+ *
+ * Licensed under the Apache License
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+
+List.prototype.fuzzySearch = function(text, pattern, options) {
+
+ // Aproximately where in the text is the pattern expected to be found?
+ var Match_Location = options.fuzzySearchLocation || 0;
+
+ //Determines how close the match must be to the fuzzy location (specified above). An exact letter match which is 'distance' characters away from the fuzzy location would score as a complete mismatch. A distance of '0' requires the match be at the exact location specified, a threshold of '1000' would require a perfect match to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
+ var Match_Distance = options.fuzzySearchDistance || 100;
+
+ // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match (of both letters and location), a threshold of '1.0' would match anything.
+ var Match_Threshold = options.fuzzySearchThreshold || 0.4;
+
+ if (pattern === text) return true; // Exact match
+ if (pattern.length > 32) return false; // This algorithm cannot be used
+
+ // Set starting location at beginning text and initialise the alphabet.
+ var loc = Match_Location,
+ s = (function() {
+ var q = {};
+
+ for (var i = 0; i < pattern.length; i++) {
+ q[pattern.charAt(i)] = 0;
+ }
+
+ for (var i = 0; i < pattern.length; i++) {
+ q[pattern.charAt(i)] |= 1 << (pattern.length - i - 1);
+ }
+
+ return q;
+ }());
+
+ // Compute and return the score for a match with e errors and x location.
+ // Accesses loc and pattern through being a closure.
+
+ function match_bitapScore_(e, x) {
@LuukvE
LuukvE Apr 9, 2012 Owner

this could be used for sorting

+
+ var accuracy = e / pattern.length,
+ proximity = Math.abs(loc - x);
+
+ if (!Match_Distance) {
+
+ // Dodge divide by zero error.
+ return proximity ? 1.0 : accuracy;
+
+ }
+
+ return accuracy + (proximity / Match_Distance);
+
+ }
+
+ var score_threshold = Match_Threshold, // Highest score beyond which we give up.
+ best_loc = text.indexOf(pattern, loc); // Is there a nearby exact match? (speedup)
+
+ if (best_loc != -1) {
+
+ score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold);
+
+ // What about in the other direction? (speedup)
+ best_loc = text.lastIndexOf(pattern, loc + pattern.length);
+
+ if (best_loc != -1) {
+
+ score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold);
+
+ }
+
+ }
+
+ // Initialise the bit arrays.
+ var matchmask = 1 << (pattern.length - 1);
+ best_loc = -1;
+
+ var bin_min, bin_mid;
+ var bin_max = pattern.length + text.length;
+ var last_rd;
+ for (var d = 0; d < pattern.length; d++) {
+ // Scan for the best match; each iteration allows for one more error.
+ // Run a binary search to determine how far from 'loc' we can stray at this
+ // error level.
+ bin_min = 0;
+ bin_mid = bin_max;
+ while (bin_min < bin_mid) {
+ if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) {
+ bin_min = bin_mid;
+ } else {
+ bin_max = bin_mid;
+ }
+ bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min);
+ }
+ // Use the result from this iteration as the maximum for the next.
+ bin_max = bin_mid;
+ var start = Math.max(1, loc - bin_mid + 1);
+ var finish = Math.min(loc + bin_mid, text.length) + pattern.length;
+
+ var rd = Array(finish + 2);
+ rd[finish + 1] = (1 << d) - 1;
+ for (var j = finish; j >= start; j--) {
+ // The alphabet (s) is a sparse hash, so the following line generates
+ // warnings.
+ var charMatch = s[text.charAt(j - 1)];
+ if (d === 0) { // First pass: exact match.
+ rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
+ } else { // Subsequent passes: fuzzy match.
+ rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) |
+ (((last_rd[j + 1] | last_rd[j]) << 1) | 1) |
+ last_rd[j + 1];
+ }
+ if (rd[j] & matchmask) {
+ var score = match_bitapScore_(d, j - 1);
+ // This match will almost certainly be better than any existing match.
+ // But check anyway.
+ if (score <= score_threshold) {
+ // Told you so.
+ score_threshold = score;
+ best_loc = j - 1;
+ if (best_loc > loc) {
+ // When passing loc, don't exceed our current distance from loc.
+ start = Math.max(1, 2 * loc - best_loc);
+ } else {
+ // Already passed loc, downhill from here on in.
+ break;
+ }
+ }
+ }
+ }
+ // No hope for a (better) match at greater error levels.
+ if (match_bitapScore_(d + 1, loc) > score_threshold) {
+ break;
+ }
+ last_rd = rd;
+ }
+ return (best_loc < 0) ? false : true;
+};
/*
* These helper functions are not written by List.js author Jonny (they may have been

0 comments on commit a75b6ef

Please sign in to comment.