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