a8b127c09270d6ef76bbebb3780f8a67e289bdf8
[nit.git] / share / ni_nitdoc / scripts / Nitdoc.QuickSearch.js
1 /* This file is part of NIT ( http://www.nitlanguage.org ).
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14
15 Documentation generator for the nit language.
16 Generate API documentation in HTML format from nit source code.
17 */
18
19 var Nitdoc = Nitdoc || {};
20
21 /*
22 * Nitdoc QuickSearch module
23 *
24 */
25
26 Nitdoc.QuickSearch = function() {
27 var rawList = nitdocQuickSearchRawList; // List of raw resulsts generated by nitdoc tool
28 var searchField = null; // <input:text> search field
29 var currentTable = null; // current search results <table>
30 var currentIndex = -1; // current cursor position into search results table
31
32 // Enable QuickSearch plugin
33 var enableQuickSearch = function(containerSelector) {
34 searchField = $(document.createElement("input"))
35 .attr({
36 id: "nitdoc-qs-field",
37 type: "text",
38 autocomplete: "off",
39 value: "quick search..."
40 })
41 .addClass("nitdoc-qs-notused")
42 .keyup(function(event) {
43 Nitdoc.QuickSearch.doKeyAction(event.keyCode);
44 })
45 .focusout(function() {
46 if($(this).val() == "") {
47 $(this).addClass("nitdoc-qs-notused");
48 $(this).val("quick search...");
49 }
50 })
51 .focusin(function() {
52 if($(this).val() == "quick search...") {
53 $(this).removeClass("nitdoc-qs-notused");
54 $(this).val("");
55 }
56 });
57
58 $(containerSelector).append(
59 $(document.createElement("li"))
60 .append(
61 $(document.createElement("form"))
62 .append(searchField)
63 .submit(function() {
64 return false;
65 })
66 )
67 );
68
69 // Close quicksearch list on click
70 $(document).click(function(e) {
71 Nitdoc.QuickSearch.closeResultsTable();
72 });
73 }
74
75 // Respond to key event
76 var doKeyAction = function(key) {
77 switch(key) {
78 case 38: // Up
79 selectPrevResult();
80 break;
81
82 case 40: // Down
83 selectNextResult();
84 break;
85
86 case 13: // Enter
87 goToResult();
88 return false;
89 break;
90
91 case 27: // Escape
92 $(this).blur();
93 closeResultsTable();
94 break;
95
96 default: // Other keys
97 var query = searchField.val();
98 if(!query) {
99 return false;
100 }
101 var results = rankResults(query);
102 results.sort(resultsSort);
103 displayResultsTable(query, results);
104 break;
105 }
106 }
107
108 // Rank raw list entries corresponding to query
109 var rankResults = function(query) {
110 var results = new Array();
111 for(var entry in rawList) {
112 for(var i in rawList[entry]) {
113 var result = rawList[entry][i];
114 result.entry = entry;
115 result.distance = query.dice(entry);
116 results[results.length] = result;
117 }
118 }
119 return results;
120 }
121
122 // Sort an array of results
123 var resultsSort = function(a, b){
124 if(a.distance < b.distance) {
125 return 1;
126 } else if(a.distance > b.distance) {
127 return -1;
128 }
129 return 0;
130 }
131
132 // Display results in a popup table
133 var displayResultsTable = function(query, results) {
134 // Clear results table
135 if(currentTable) currentTable.remove();
136
137 // Build results table
138 currentIndex = -1;
139 currentTable = $(document.createElement("table"));
140
141 for(var i in results) {
142 if(i > 10) {
143 break;
144 }
145 var result = results[i];
146 currentTable.append(
147 $(document.createElement("tr"))
148 .data("searchDetails", {name: result.entry, url: result.url})
149 .data("index", i)
150 .append($(document.createElement("td")).html(result.entry))
151 .append(
152 $(document.createElement("td"))
153 .addClass("nitdoc-qs-info")
154 .html(result.txt + "&nbsp;&raquo;")
155 )
156 .mouseover( function() {
157 $(currentTable.find("tr")[currentIndex]).removeClass("nitdoc-qs-active");
158 $(this).addClass("nitdoc-qs-active");
159 currentIndex = $(this).data("index");
160 })
161 .mouseout( function() {
162 $(this).removeClass("nitdoc-qs-active");
163 })
164 .click( function() {
165 window.location = $(this).data("searchDetails")["url"];
166 })
167 );
168 }
169 currentTable.append(
170 $("<tr class='nitdoc-qs-overflow'>")
171 .append(
172 $("<td colspan='2'>")
173 .html("Best results for '" + query + "'")
174 )
175 );
176
177 // Initialize table properties
178 currentTable.attr("id", "nitdoc-qs-table");
179 currentTable.css("position", "absolute");
180 currentTable.width(searchField.outerWidth());
181 $("body").append(currentTable);
182 currentTable.offset({left: searchField.offset().left + (searchField.outerWidth() - currentTable.outerWidth()), top: searchField.offset().top + searchField.outerHeight()});
183 // Preselect first entry
184 if(currentTable.find("tr").length > 0) {
185 currentIndex = 0;
186 $(currentTable.find("tr")[currentIndex]).addClass("nitdoc-qs-active");
187 searchField.focus();
188 }
189 }
190
191 // Select the previous result on current table
192 var selectPrevResult = function() {
193 // If already on first result, focus search input
194 if(currentIndex == 0) {
195 searchField.val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name);
196 searchField.focus();
197 // Else select previous result
198 } else if(currentIndex > 0) {
199 $(currentTable.find("tr")[currentIndex]).removeClass("nitdoc-qs-active");
200 currentIndex--;
201 $(currentTable.find("tr")[currentIndex]).addClass("nitdoc-qs-active");
202 searchField.val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name);
203 searchField.focus();
204 }
205 }
206
207 // Select the next result on current table
208 var selectNextResult = function() {
209 if(currentIndex < currentTable.find("tr").length - 1) {
210 if($(currentTable.find("tr")[currentIndex + 1]).hasClass("nitdoc-qs-overflow")) {
211 return;
212 }
213 $(currentTable.find("tr")[currentIndex]).removeClass("nitdoc-qs-active");
214 currentIndex++;
215 $(currentTable.find("tr")[currentIndex]).addClass("nitdoc-qs-active");
216 searchField.val($(currentTable.find("tr")[currentIndex]).data("searchDetails").name);
217 searchField.focus();
218 }
219 }
220
221 // Load selected search result page
222 var goToResult = function() {
223 if(currentIndex > -1) {
224 window.location = $(currentTable.find("tr")[currentIndex]).data("searchDetails").url;
225 return;
226 }
227
228 if(searchField.val().length == 0) { return; }
229
230 window.location = "search.html#q=" + searchField.val();
231 if(window.location.href.indexOf("search.html") > -1) {
232 location.reload();
233 }
234 }
235
236 // Close the results table
237 closeResultsTable = function(target) {
238 if(target != searchField && target != currentTable) {
239 if(currentTable != null) {
240 currentTable.remove();
241 currentTable = null;
242 }
243 }
244 }
245
246 // Public interface
247 var quicksearch = {
248 enableQuickSearch: enableQuickSearch,
249 doKeyAction: doKeyAction,
250 closeResultsTable: closeResultsTable
251 };
252
253 return quicksearch;
254 }();
255
256 $(document).ready(function() {
257 Nitdoc.QuickSearch.enableQuickSearch("nav.main ul");
258 });
259
260 /*
261 * Utils
262 */
263
264 // Calculate levenshtein distance beetween two strings
265 // see: http://en.wikipedia.org/wiki/Levenshtein_distance
266 String.prototype.levenshtein = function(other) {
267 var matrix = new Array();
268
269 for(var i = 0; i <= this.length; i++) {
270 matrix[i] = new Array();
271 matrix[i][0] = i;
272 }
273 for(var j = 0; j <= other.length; j++) {
274 matrix[0][j] = j;
275 }
276 var cost = 0;
277 for(var i = 1; i <= this.length; i++) {
278 for(var j = 1; j <= other.length; j++) {
279 if(this.charAt(i - 1) == other.charAt(j - 1)) {
280 cost = 0;
281 } else if(this.charAt(i - 1).toLowerCase() == other.charAt(j - 1).toLowerCase()) {
282 cost = 0.5;
283 } else {
284 cost = 1;
285 }
286 matrix[i][j] = Math.min(
287 matrix[i - 1][j] + 1, // deletion
288 matrix[i][j - 1] + 1, // insertion
289 matrix[i - 1][j - 1] + cost // substitution
290 );
291 }
292 }
293 return matrix[this.length][other.length]
294 }
295
296 // Compare two strings using Sorensen-Dice Coefficient
297 // see: http://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient
298 String.prototype.dice = function(other) {
299 var length1 = this.length - 1;
300 var length2 = other.length - 1;
301 if(length1 < 1 || length2 < 1) return 0;
302
303 var bigrams2 = [];
304 for(var i = 0; i < length2; i++) {
305 bigrams2.push(other.substr(i, 2));
306 }
307
308 var intersection = 0;
309 for(var i = 0; i < length1; i++) {
310 var bigram1 = this.substr(i, 2);
311 for(var j = 0; j < length2; j++) {
312 if(bigram1 == bigrams2[j]) {
313 intersection += 2;
314 bigrams2[j] = null;
315 break;
316 } else if (bigram1 && bigrams2[j] && bigram1.toLowerCase() == bigrams2[j].toLowerCase()) {
317 intersection += 1;
318 bigrams2[j] = null;
319 break;
320 }
321 }
322 }
323 return (2.0 * intersection) / (length1 + length2);
324 }
325