nitdoc: makes quick-search field aware of paste from mouse.
[nit.git] / share / nitdoc / js / plugins / 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 /*
20 * Nitdoc QuickSearch widget
21 */
22 define([
23 "jquery",
24 "jQueryUI",
25 "utils",
26 "quicksearchList",
27 ], function($, ui, utils) {
28 $.widget("nitdoc.quicksearch", {
29
30 options: {
31 list: {}, // List of raw results generated by nitdoc tool
32 fieldAttrs: {
33 autocomplete: "off",
34 },
35 tableID: "nitdoc-qs-table",
36 tableCSS: {
37 "position": "absolute"
38 },
39 rowClass: "nitdoc-qs-row",
40 rowCatClass: "nitdoc-qs-cat",
41 rowSubClass: "nitdoc-qs-sub",
42 rowActiveClass: "nitdoc-qs-active",
43 rowOverflowClass: "nitdoc-qs-overflow",
44 rowOverflowActive: "nitdoc-qs-overflow-active",
45 rowNoResultClass: "nitdoc-qs-noresult",
46 overflowUpHtml: "▲",
47 overflowDownHtml: "▼",
48 noresultText: "Sorry, there is no match, best results are:",
49 infoClass: "nitdoc-qs-info",
50 gotoPage: "search.html",
51 maxSize: 10
52 },
53
54 _create: function() {
55 // set widget options
56 this.element.attr(this.options.fieldAttrs);
57 // event dispatch
58 this._on(this.element, {
59 "keydown": this._doKeyDown,
60 "keyup": this._doKeyUp,
61 "input": this._doInput
62 });
63 // add result table element once
64 this._table = $("<table/>")
65 .attr("id", this.options.tableID)
66 .css(this.options.tableCSS)
67 .css("min-width", this.element.outerWidth());
68 $("body").append(this._table);
69 // make table disappear when a click occurs outside
70 $(document).click($.proxy(this.closeTable, this));
71 },
72
73 /* events */
74
75 _doKeyDown: function(event) {
76 switch(event.keyCode) {
77 case 38: // Up
78 this._selectPrev();
79 return false;
80 case 40: // Down
81 this._selectNext();
82 return false;
83 default:
84 return true;
85 }
86 },
87
88 _doKeyUp: function(event) {
89 switch(event.keyCode) {
90 case 38: // Up
91 case 40: // Down
92 break;
93 case 13: // Enter
94 this._loadResult();
95 return false;
96 case 27: // Escape
97 this.element.blur();
98 this.closeTable();
99 return true;
100 default: // Other keys
101 return true;
102 }
103 },
104
105 _doInput: function(event) {
106 utils.delayEvent($.proxy(this.search, this));
107 },
108
109 /* Result lookup */
110
111 _getResults: function(query) {
112 var results = {};
113 results.matches = [];
114 for(var entry in this.options.list) {
115 if(!entry.startsWith(query, true)) {
116 continue;
117 }
118 var cat = {
119 name: entry,
120 entries: this.options.list[entry]
121 };
122 results.matches[results.matches.length] = cat;
123
124 if(entry == query) {
125 cat.rank = 3;
126 } else if(entry.toUpperCase() == query.toUpperCase()) {
127 cat.rank = 2;
128 } else {
129 cat.rank = 1 + query.dice(entry);
130 }
131 }
132 results.matches.sort(this._rankSorter);
133 results.partials = new Array();
134 if(results.matches.length == 0) {
135 for(var entry in this.options.list) {
136 var cat = {
137 name: entry,
138 entries: this.options.list[entry]
139 }
140 cat.rank = query.dice(entry);
141 if(cat.rank > 0) {
142 results.partials[results.partials.length] = cat;
143 }
144 }
145 results.partials.sort(this._rankSorter);
146 }
147 return results;
148 },
149
150 _rankSorter: function(a, b){
151 if(a.rank < b.rank) {
152 return 1;
153 } else if(a.rank > b.rank) {
154 return -1;
155 }
156 return 0;
157 },
158
159 /* Results table */
160
161 search: function() {
162 var query = this.element.val();
163 if(query) {
164 var results = this._getResults(query);
165 this.openTable(query, results);
166 }
167 },
168
169 openTable: function(query, results) {
170 this._table.empty();
171 this._rows = [];
172 this._index = -1;
173
174 var resultSet = results.matches;
175 if(resultSet.length == 0) {
176 resultSet = results.partials
177 }
178
179 for(var i in resultSet) {
180 var cat = resultSet[i];
181 var result = cat.entries[0];
182 this.addRow(cat.name, result.txt, result.url, this.options.rowCatClass)
183 for(var j = 1; j < cat.entries.length; j++) {
184 var result = cat.entries[j];
185 this.addRow(cat.name, result.txt, result.url, this.options.rowSubClass)
186 }
187 }
188
189 if(this._rows.length >= this.options.maxSize) {
190 this.addOverflowUp();
191 this.addOverflowDown();
192 }
193 if(results.matches.length == 0) {
194 this.addNoResultRow();
195 }
196
197 if(resultSet.length > 0) {
198 this._setIndex(0);
199 }
200 this._table.show();
201 this._autosizeTable();
202 },
203
204 closeTable: function(target) {
205 if(target != this.element && target != this._table) {
206 this._table.hide();
207 }
208 },
209
210 addRow: function(name, txt, url, cls) {
211 var row = $("<tr/>")
212 .addClass(this.options.rowClass)
213 .data("searchDetails", {name: name, url: url})
214 .data("index", this._rows.length)
215 .append(
216 $("<td/>")
217 .html(name)
218 .addClass(cls)
219 )
220 .append(
221 $("<td/>")
222 .html(txt + "&nbsp;&raquo;")
223 .addClass(this.options.infoClass)
224 )
225 .mouseover($.proxy(this._mouseOverRow, this))
226 .click($.proxy(this._clickRow, this))
227 this._rows.push(row);
228 if(this._rows.length >= this.options.maxSize) {
229 row.hide();
230 }
231 this._table.append(row);
232 },
233
234 addOverflowUp: function() {
235 this._table.prepend(
236 $("<tr/>")
237 .addClass(this.options.rowOverflowClass)
238 .append(
239 $("<td/>")
240 .attr("colspan", 2)
241 .html(this.options.overflowUpHtml)
242 )
243 .click($.proxy(this._clickPrev, this))
244 );
245 },
246
247 addOverflowDown: function() {
248 this._table.append(
249 $("<tr/>")
250 .addClass(this.options.rowOverflowClass)
251 .addClass(this.options.rowOverflowActive)
252 .append(
253 $("<td/>")
254 .attr("colspan", 2)
255 .html(this.options.overflowDownHtml)
256 )
257 .click($.proxy(this._clickNext, this))
258 );
259 },
260
261 addNoResultRow: function() {
262 this._table.prepend(
263 $("<tr/>")
264 .addClass(this.options.rowNoResultClass)
265 .append(
266 $("<td/>")
267 .attr("colspan", "2")
268 .text(this.options.noresultText)
269 )
270 );
271 },
272
273 _autosizeTable: function() {
274 this._table.position({
275 my: "right top",
276 at: "right bottom",
277 of: this.element
278 });
279 },
280
281 _hasIndex: function(index) {
282 return index >= 0 && index < this._rows.length;
283 },
284
285 _hasPrev: function(index) {
286 return index - 1 >= 0;
287 },
288
289 _hasNext: function(index) {
290 return index + 1 < this._rows.length;
291 },
292
293 _setIndex: function(index) {
294 if(this._hasIndex(this._index)) {
295 this._rows[this._index].removeClass(this.options.rowActiveClass);
296 }
297 this._index = index;
298 if(this._hasIndex(this._index)) {
299 this._rows[this._index].addClass(this.options.rowActiveClass);
300 }
301 },
302
303 _selectPrev: function() {
304 if(this._hasPrev(this._index)) {
305 this._setIndex(this._index - 1);
306 if(!this._rows[this._index].is(":visible")) {
307 this._table.find("tr." + this.options.rowClass + ":visible").last().hide();
308 this._table.find("tr." + this.options.rowOverflowClass).addClass(this.options.rowOverflowActive);
309 this._rows[this._index].show();
310 if(!this._hasPrev(this._index)) {
311 this._table.find("tr." + this.options.rowOverflowClass).removeClass(this.options.rowOverflowActive);
312 }
313 this._autosizeTable();
314 }
315 }
316 },
317
318 _selectNext: function() {
319 if(this._hasNext(this._index)) {
320 this._setIndex(this._index + 1);
321 if(!this._rows[this._index].is(":visible")) {
322 this._table.find("tr." + this.options.rowClass + ":visible").first().hide();
323 this._table.find("tr." + this.options.rowOverflowClass).addClass(this.options.rowOverflowActive);
324 this._rows[this._index].show();
325 if(!this._hasNext(this._index)) {
326 this._table.find("tr." + this.options.rowOverflowClass).removeClass(this.options.rowOverflowActive);
327 }
328 this._autosizeTable();
329 }
330 }
331 },
332
333 // Load selected search result page
334 _loadResult: function() {
335 if(this._index > -1) {
336 window.location = this._rows[this._index].data("searchDetails").url;
337 return;
338 }
339 if(this.element.val().length == 0) { return; }
340
341 window.location = this.options.gotoPage + "#q=" + this.element.val();
342 if(window.location.href.indexOf(this.options.gotoPage) > -1) {
343 location.reload();
344 }
345 },
346
347 /* table events */
348
349 _clickNext: function(event) {
350 event.stopPropagation();
351 this._selectNext();
352 },
353
354 _clickPrev: function(event) {
355 event.stopPropagation();
356 this._selectPrev();
357 },
358
359 _clickRow: function(event) {
360 window.location = $(event.currentTarget).data("searchDetails")["url"];
361 },
362
363 _mouseOverRow: function(event) {
364 this._setIndex($(event.currentTarget).data("index"));
365 }
366 });
367
368 var searchField = $("<input/>")
369 .addClass("form-control input-sm")
370 .attr({
371 id: "nitdoc-qs-field",
372 type: "text",
373 placeholder: "Search..."
374 })
375
376 $("#topmenu-collapse").append(
377 $("<div>")
378 .addClass("navbar-form navbar-right")
379 .append(
380 $("<div>")
381 .addClass("form-group")
382 .append(searchField)
383 )
384 );
385
386 searchField.quicksearch({
387 list: this.nitdocQuickSearchRawList
388 });
389 });