nitdoc: full rewrite of the nitdoc engine
[nit.git] / share / nitdoc / js / nitdoc.quicksearch.js
1 /*
2 * Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14
15 /* Nitdoc QuickSearch widget */
16
17 $.widget("nitdoc.quicksearch", {
18
19 options: {
20 list: {}, // List of raw results generated by nitdoc tool
21 fieldAttrs: {
22 autocomplete: "off",
23 },
24 maxSize: 10
25 },
26
27 _create: function() {
28 // set widget options
29 this.element.attr(this.options.fieldAttrs);
30 // event dispatch
31 this._on(this.element, {
32 "keydown": this._doKeyDown,
33 "keyup": this._doKeyUp,
34 "input": this._doInput
35 });
36 // add result table element once
37 this._popup = $("<div/>")
38 .attr("id", "nitdoc-qs-popup")
39 .css("position", "absolute")
40 .css("z-index", 10000)
41 .hide();
42 $("body").append(this._popup);
43 // make table disappear when a click occurs outside
44 $(document).click($.proxy(this.close, this));
45 this._autosizeTable();
46 },
47
48 /* events */
49
50 _doKeyDown: function(event) {
51 switch(event.keyCode) {
52 case 38: // Up
53 this._selectPrev();
54 return false;
55 case 40: // Down
56 this._selectNext();
57 return false;
58 default:
59 return true;
60 }
61 },
62
63 _doKeyUp: function(event) {
64 switch(event.keyCode) {
65 case 38: // Up
66 case 40: // Down
67 break;
68 case 13: // Enter
69 this._loadResult();
70 return false;
71 case 27: // Escape
72 this.element.blur();
73 this.close();
74 return true;
75 default: // Other keys
76 return true;
77 }
78 },
79
80 _doInput: function(event) {
81 Utils.delayEvent($.proxy(this.search, this));
82 },
83
84 /* Result lookup */
85
86 _getResults: function(query) {
87 var results = [];
88
89 // Prefix matches
90 var prefix_matches = [];
91 for(var entry in this.options.list) {
92 if(!entry.startsWith(query, true)) {
93 continue;
94 }
95 var cat = {
96 name: entry,
97 entries: this.options.list[entry]
98 };
99 prefix_matches.push(cat);
100
101 if(entry == query) {
102 cat.rank = 10;
103 } else if(entry.toUpperCase() == query.toUpperCase()) {
104 cat.rank = 5;
105 } else if(entry[0] == query[0]) {
106 cat.rank = 1.1 + query.dice(entry);
107 } else {
108 cat.rank = 1 + query.dice(entry);
109 }
110 }
111 if(prefix_matches.length > 0) {
112 prefix_matches.sort(this._rankSorter);
113 for(var i in prefix_matches) {
114 var cat = prefix_matches[i];
115 for(var j in cat.entries) {
116 var entry = cat.entries[j];
117 entry.name = cat.name;
118 results.push(entry);
119 }
120 }
121 return results;
122 }
123
124 // Partial matches
125 var partial_matches = [];
126 for(var entry in this.options.list) {
127 var cat = {
128 name: entry,
129 entries: this.options.list[entry]
130 }
131 cat.rank = query.dice(entry);
132 if(cat.rank > 0) {
133 partial_matches.push(cat);
134 }
135 }
136 if(partial_matches.length > 0) {
137 partial_matches.sort(this._rankSorter);
138 for(var i in partial_matches) {
139 var cat = partial_matches[i];
140 for(var j in cat.entries) {
141 var entry = cat.entries[j];
142 entry.name = cat.name;
143 results.push(entry);
144 }
145 }
146 }
147
148 return results;
149 },
150
151 _rankSorter: function(a, b){
152 if(a.rank < b.rank) {
153 return 1;
154 } else if(a.rank > b.rank) {
155 return -1;
156 }
157 return 0;
158 },
159
160 /* Results table */
161
162 search: function() {
163 var query = this.element.val();
164 if(query) {
165 var results = this._getResults(query);
166 this.open(query, results);
167 }
168 },
169
170 open: function(query, results) {
171 this._popup.empty();
172 this._cards = [];
173 this._index = -1;
174
175 if(results.length == 0) {
176 this.addNoResultCard();
177 }
178
179 if(results.length >= this.options.maxSize) {
180 this.addOverflowUp(false);
181 }
182
183 for(var i in results) {
184 var result = results[i];
185 this.addCard(result.name, result.txt, result.url, this.options.rowCatClass)
186 }
187
188 if(results.length >= this.options.maxSize) {
189 this.addOverflowDown(true);
190 }
191
192 if(results.length > 0) {
193 this._setIndex(0);
194 }
195
196 this._popup.show();
197 this._autosizeTable();
198 },
199
200 close: function(target) {
201 if(target != this.element && target != this._popup) {
202 this._popup.hide();
203 }
204 },
205
206 addCard: function(name, txt, url, cls) {
207 var card = $("<div/>")
208 .addClass("qs-card")
209 .addClass("qs-result")
210 .data("searchDetails", {name: name, url: url})
211 .data("index", this._cards.length)
212 .append(
213 $("<h1/>")
214 .html(name)
215 .addClass(cls)
216 )
217 .append(
218 $("<span/>")
219 .html(txt)
220 .addClass("qs-info")
221 )
222 .mouseover($.proxy(this._mouseOverRow, this))
223 .click($.proxy(this._clickRow, this))
224 this._cards.push(card);
225 if(this._cards.length >= this.options.maxSize) {
226 card.hide();
227 }
228 this._popup.append(card);
229 },
230
231 addOverflowUp: function(active) {
232 this._popup.append(
233 $("<div/>")
234 .addClass("qs-overflow")
235 .addClass("qs-overflow-up")
236 .addClass(active ? "qs-overflow-active": "")
237 .html("&#x25B2;")
238 .click($.proxy(this._clickPrev, this))
239 );
240 },
241
242 addOverflowDown: function(active) {
243 this._popup.append(
244 $("<div/>")
245 .addClass("qs-overflow")
246 .addClass("qs-overflow-down")
247 .addClass(active ? "qs-overflow-active": "")
248 .html("&#x25BC;")
249 .click($.proxy(this._clickNext, this))
250 );
251 },
252
253 addNoResultCard: function() {
254 var card = $("<div/>")
255 .addClass("qs-card qs-noresult")
256 .html("Sorry, there is no match...");
257 this._popup.append(card);
258 },
259
260 _autosizeTable: function() {
261 this._popup.position({
262 my: "left top",
263 at: "left bottom",
264 of: this.element
265 });
266 this._popup
267 .css("min-width", this.element.outerWidth())
268 .css("max-width", this.element.outerWidth());
269 },
270
271 _hasIndex: function(index) {
272 return index >= 0 && index < this._cards.length;
273 },
274
275 _hasPrev: function(index) {
276 return index - 1 >= 0;
277 },
278
279 _hasNext: function(index) {
280 return index + 1 < this._cards.length;
281 },
282
283 _setIndex: function(index) {
284 if(this._hasIndex(this._index)) {
285 this._cards[this._index].removeClass("qs-active");
286 }
287 this._index = index;
288 if(this._hasIndex(this._index)) {
289 this._cards[this._index].addClass("qs-active");
290 }
291 },
292
293 _selectPrev: function() {
294 if(this._hasPrev(this._index)) {
295 this._setIndex(this._index - 1);
296 if(!this._cards[this._index].is(":visible")) {
297 this._popup.find(".qs-result:visible").last().hide();
298 this._popup.find(".qs-overflow-down").addClass("qs-overflow-active");
299 this._cards[this._index].show();
300 if(!this._hasPrev(this._index)) {
301 this._popup.find(".qs-overflow-up").removeClass("qs-overflow-active");
302 }
303 }
304 } else {
305 }
306 },
307
308 _selectNext: function() {
309 if(this._hasNext(this._index)) {
310 this._setIndex(this._index + 1);
311 if(!this._cards[this._index].is(":visible")) {
312 this._popup.find(".qs-result:visible").first().hide();
313 this._popup.find(".qs-overflow-up").addClass("qs-overflow-active");
314 this._cards[this._index].show();
315 if(!this._hasNext(this._index)) {
316 this._popup.find(".qs-overflow-down").removeClass("qs-overflow-active");
317 }
318 }
319 }
320 },
321
322 // Load selected search result page
323 _loadResult: function() {
324 if(this._index > -1) {
325 window.location = this._cards[this._index].data("searchDetails").url;
326 return;
327 }
328 if(this.element.val().length == 0) { return; }
329
330 window.location = this.options.gotoPage + "#q=" + this.element.val();
331 if(window.location.href.indexOf(this.options.gotoPage) > -1) {
332 location.reload();
333 }
334 },
335
336 /* table events */
337
338 _clickNext: function(event) {
339 event.stopPropagation();
340 this._selectNext();
341 },
342
343 _clickPrev: function(event) {
344 event.stopPropagation();
345 this._selectPrev();
346 },
347
348 _clickRow: function(event) {
349 window.location = $(event.currentTarget).data("searchDetails")["url"];
350 },
351
352 _mouseOverRow: function(event) {
353 this._setIndex($(event.currentTarget).data("index"));
354 }
355 });
356
357 var searchField = $("<input/>")
358 .addClass("form-control search-input")
359 .attr({
360 id: "nitdoc-qs-field",
361 type: "text",
362 placeholder: "Search..."
363 })
364
365 $("#search-placeholder").append(
366 $("<form>")
367 .addClass("navbar-form navbar-right")
368 .on("submit", function() { return false; })
369 .css("margin-bottom", 0)
370 .css("margin-top", 0)
371 .append(
372 $("<div>")
373 .addClass("form-group has-icon")
374 .append(searchField)
375 .append(
376 $("<span>")
377 .addClass("glyphicon glyphicon-search form-control-icon text-muted")
378 )
379 )
380 );
381
382 searchField.quicksearch({
383 list: this.nitdocQuickSearchRawList
384 });