scratch – Blame information for rev

Subversion Repositories:
Rev:
Rev Author Line No. Line
115 office 1 /*
2 Copyright (c) 2009 Vladimir Kolesnikov
3  
4 Permission is hereby granted, free of charge, to any person obtaining
5 a copy of this software and associated documentation files (the
6 "Software"), to deal in the Software without restriction, including
7 without limitation the rights to use, copy, modify, merge, publish,
8 distribute, sublicense, and/or sell copies of the Software, and to
9 permit persons to whom the Software is furnished to do so, subject to
10 the following conditions:
11  
12 The above copyright notice and this permission notice shall be
13 included in all copies or substantial portions of the Software.
14  
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 */
23  
24 Searchdoc = {};
25  
26 // navigation.js ------------------------------------------
27  
28 Searchdoc.Navigation = new function() {
29 this.initNavigation = function() {
30 var _this = this;
31  
32 $(document).keydown(function(e) {
33 _this.onkeydown(e);
34 }).keyup(function(e) {
35 _this.onkeyup(e);
36 });
37  
38 this.navigationActive = true;
39 }
40  
41 this.setNavigationActive = function(state) {
42 this.navigationActive = state;
43 this.clearMoveTimeout();
44 }
45  
46  
47 this.onkeyup = function(e) {
48 if (!this.navigationActive) return;
49 switch(e.keyCode) {
50 case 37: //Event.KEY_LEFT:
51 case 38: //Event.KEY_UP:
52 case 39: //Event.KEY_RIGHT:
53 case 40: //Event.KEY_DOWN:
54 case 73: // i - qwerty
55 case 74: // j
56 case 75: // k
57 case 76: // l
58 case 67: // c - dvorak
59 case 72: // h
60 case 84: // t
61 case 78: // n
62 this.clearMoveTimeout();
63 break;
64 }
65 }
66  
67 this.onkeydown = function(e) {
68 if (!this.navigationActive) return;
69 switch(e.keyCode) {
70 case 37: //Event.KEY_LEFT:
71 case 74: // j (qwerty)
72 case 72: // h (dvorak)
73 if (this.moveLeft()) e.preventDefault();
74 break;
75 case 38: //Event.KEY_UP:
76 case 73: // i (qwerty)
77 case 67: // c (dvorak)
78 if (e.keyCode == 38 || e.ctrlKey) {
79 if (this.moveUp()) e.preventDefault();
80 this.startMoveTimeout(false);
81 }
82 break;
83 case 39: //Event.KEY_RIGHT:
84 case 76: // l (qwerty)
85 case 78: // n (dvorak)
86 if (this.moveRight()) e.preventDefault();
87 break;
88 case 40: //Event.KEY_DOWN:
89 case 75: // k (qwerty)
90 case 84: // t (dvorak)
91 if (e.keyCode == 40 || e.ctrlKey) {
92 if (this.moveDown()) e.preventDefault();
93 this.startMoveTimeout(true);
94 }
95 break;
96 case 9: //Event.KEY_TAB:
97 case 13: //Event.KEY_RETURN:
98 if (this.$current) this.select(this.$current);
99 break;
100 }
101 if (e.ctrlKey && e.shiftKey) this.select(this.$current);
102 }
103  
104 this.clearMoveTimeout = function() {
105 clearTimeout(this.moveTimeout);
106 this.moveTimeout = null;
107 }
108  
109 this.startMoveTimeout = function(isDown) {
110 if (!$.browser.mozilla && !$.browser.opera) return;
111 if (this.moveTimeout) this.clearMoveTimeout();
112 var _this = this;
113  
114 var go = function() {
115 if (!_this.moveTimeout) return;
116 _this[isDown ? 'moveDown' : 'moveUp']();
117 _this.moveTimout = setTimeout(go, 100);
118 }
119 this.moveTimeout = setTimeout(go, 200);
120 }
121  
122 this.moveRight = function() {
123 }
124  
125 this.moveLeft = function() {
126 }
127  
128 this.move = function(isDown) {
129 }
130  
131 this.moveUp = function() {
132 return this.move(false);
133 }
134  
135 this.moveDown = function() {
136 return this.move(true);
137 }
138 }
139  
140  
141 // scrollIntoView.js --------------------------------------
142  
143 function scrollIntoView(element, view) {
144 var offset, viewHeight, viewScroll, height;
145 offset = element.offsetTop;
146 height = element.offsetHeight;
147 viewHeight = view.offsetHeight;
148 viewScroll = view.scrollTop;
149 if (offset - viewScroll + height > viewHeight) {
150 view.scrollTop = offset - viewHeight + height;
151 }
152 if (offset < viewScroll) {
153 view.scrollTop = offset;
154 }
155 }
156  
157  
158 // searcher.js --------------------------------------------
159  
160 Searchdoc.Searcher = function(data) {
161 this.data = data;
162 this.handlers = [];
163 }
164  
165 Searchdoc.Searcher.prototype = new function() {
166 var CHUNK_SIZE = 1000, // search is performed in chunks of 1000 for non-bloking user input
167 MAX_RESULTS = 100, // do not try to find more than 100 results
168 huid = 1, suid = 1,
169 runs = 0;
170  
171  
172 this.find = function(query) {
173 var queries = splitQuery(query),
174 regexps = buildRegexps(queries),
175 highlighters = buildHilighters(queries),
176 state = { from: 0, pass: 0, limit: MAX_RESULTS, n: suid++},
177 _this = this;
178 this.currentSuid = state.n;
179  
180 if (!query) return;
181  
182 var run = function() {
183 // stop current search thread if new search started
184 if (state.n != _this.currentSuid) return;
185  
186 var results = performSearch(_this.data, regexps, queries, highlighters, state),
187 hasMore = (state.limit > 0 && state.pass < 3);
188  
189 triggerResults.call(_this, results, !hasMore);
190 if (hasMore) {
191 setTimeout(run, 2);
192 }
193 runs++;
194 };
195 runs = 0;
196  
197 // start search thread
198 run();
199 }
200  
201 /* ----- Events ------ */
202 this.ready = function(fn) {
203 fn.huid = huid;
204 this.handlers.push(fn);
205 }
206  
207 /* ----- Utilities ------ */
208 function splitQuery(query) {
209 return jQuery.grep(query.split(/(\s+|\(\)?)/), function(string) { return string.match(/\S/) });
210 }
211  
212 function buildRegexps(queries) {
213 return jQuery.map(queries, function(query) { return new RegExp(query.replace(/(.)/g, '([$1])([^$1]*?)'), 'i') });
214 }
215  
216 function buildHilighters(queries) {
217 return jQuery.map(queries, function(query) {
218 return jQuery.map( query.split(''), function(l, i){ return '\u0001$' + (i*2+1) + '\u0002$' + (i*2+2) } ).join('')
219 });
220 }
221  
222 // function longMatchRegexp(index, longIndex, regexps) {
223 // for (var i = regexps.length - 1; i >= 0; i--){
224 // if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false;
225 // };
226 // return true;
227 // }
228  
229  
230 /* ----- Mathchers ------ */
231 function matchPass1(index, longIndex, queries, regexps) {
232 if (index.indexOf(queries[0]) != 0) return false;
233 for (var i=1, l = regexps.length; i < l; i++) {
234 if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false;
235 };
236 return true;
237 }
238  
239 function matchPass2(index, longIndex, queries, regexps) {
240 if (index.indexOf(queries[0]) == -1) return false;
241 for (var i=1, l = regexps.length; i < l; i++) {
242 if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false;
243 };
244 return true;
245 }
246  
247 function matchPassRegexp(index, longIndex, queries, regexps) {
248 if (!index.match(regexps[0])) return false;
249 for (var i=1, l = regexps.length; i < l; i++) {
250 if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false;
251 };
252 return true;
253 }
254  
255  
256 /* ----- Highlighters ------ */
257 function highlightRegexp(info, queries, regexps, highlighters) {
258 var result = createResult(info);
259 for (var i=0, l = regexps.length; i < l; i++) {
260 result.title = result.title.replace(regexps[i], highlighters[i]);
261 if (i > 0)
262 result.namespace = result.namespace.replace(regexps[i], highlighters[i]);
263 };
264 return result;
265 }
266  
267 function hltSubstring(string, pos, length) {
268 return string.substring(0, pos) + '\u0001' + string.substring(pos, pos + length) + '\u0002' + string.substring(pos + length);
269 }
270  
271 function highlightQuery(info, queries, regexps, highlighters) {
272 var result = createResult(info), pos = 0, lcTitle = result.title.toLowerCase();
273 pos = lcTitle.indexOf(queries[0]);
274 if (pos != -1) {
275 result.title = hltSubstring(result.title, pos, queries[0].length);
276 }
277 for (var i=1, l = regexps.length; i < l; i++) {
278 result.title = result.title.replace(regexps[i], highlighters[i]);
279 result.namespace = result.namespace.replace(regexps[i], highlighters[i]);
280 };
281 return result;
282 }
283  
284 function createResult(info) {
285 var result = {};
286 result.title = info[0];
287 result.namespace = info[1];
288 result.path = info[2];
289 result.params = info[3];
290 result.snippet = info[4];
291 result.badge = info[6];
292 return result;
293 }
294  
295 /* ----- Searching ------ */
296 function performSearch(data, regexps, queries, highlighters, state) {
297 var searchIndex = data.searchIndex, // search by title first and then by source
298 longSearchIndex = data.longSearchIndex,
299 info = data.info,
300 result = [],
301 i = state.from,
302 l = searchIndex.length,
303 togo = CHUNK_SIZE,
304 matchFunc, hltFunc;
305  
306 while (state.pass < 3 && state.limit > 0 && togo > 0) {
307 if (state.pass == 0) {
308 matchFunc = matchPass1;
309 hltFunc = highlightQuery;
310 } else if (state.pass == 1) {
311 matchFunc = matchPass2;
312 hltFunc = highlightQuery;
313 } else if (state.pass == 2) {
314 matchFunc = matchPassRegexp;
315 hltFunc = highlightRegexp;
316 }
317  
318 for (; togo > 0 && i < l && state.limit > 0; i++, togo--) {
319 if (info[i].n == state.n) continue;
320 if (matchFunc(searchIndex[i], longSearchIndex[i], queries, regexps)) {
321 info[i].n = state.n;
322 result.push(hltFunc(info[i], queries, regexps, highlighters));
323 state.limit--;
324 }
325 };
326 if (searchIndex.length <= i) {
327 state.pass++;
328 i = state.from = 0;
329 } else {
330 state.from = i;
331 }
332 }
333 return result;
334 }
335  
336 function triggerResults(results, isLast) {
337 jQuery.each(this.handlers, function(i, fn) { fn.call(this, results, isLast) })
338 }
339 }
340  
341  
342  
343  
344 // panel.js -----------------------------------------------
345  
346 Searchdoc.Panel = function(element, data, tree, frame) {
347 this.$element = $(element);
348 this.$input = $('input', element).eq(0);
349 this.$result = $('.result ul', element).eq(0);
350 this.frame = frame;
351 this.$current = null;
352 this.$view = this.$result.parent();
353 this.data = data;
354 this.searcher = new Searchdoc.Searcher(data.index);
355 this.tree = new Searchdoc.Tree($('.tree', element), tree, this);
356 this.init();
357 }
358  
359 Searchdoc.Panel.prototype = $.extend({}, Searchdoc.Navigation, new function() {
360 var suid = 1;
361  
362 this.init = function() {
363 var _this = this;
364 var observer = function() {
365 _this.search(_this.$input[0].value);
366 };
367 this.$input.keyup(observer);
368 this.$input.click(observer); // mac's clear field
369  
370 this.searcher.ready(function(results, isLast) {
371 _this.addResults(results, isLast);
372 })
373  
374 this.$result.click(function(e) {
375 _this.$current.removeClass('current');
376 _this.$current = $(e.target).closest('li').addClass('current');
377 _this.select();
378 _this.$input.focus();
379 });
380  
381 this.initNavigation();
382 this.setNavigationActive(false);
383 }
384  
385 this.search = function(value, selectFirstMatch) {
386 value = jQuery.trim(value).toLowerCase();
387 this.selectFirstMatch = selectFirstMatch;
388 if (value) {
389 this.$element.removeClass('panel_tree').addClass('panel_results');
390 this.tree.setNavigationActive(false);
391 this.setNavigationActive(true);
392 } else {
393 this.$element.addClass('panel_tree').removeClass('panel_results');
394 this.tree.setNavigationActive(true);
395 this.setNavigationActive(false);
396 }
397 if (value != this.lastQuery) {
398 this.lastQuery = value;
399 this.firstRun = true;
400 this.searcher.find(value);
401 }
402 }
403  
404 this.addResults = function(results, isLast) {
405 var target = this.$result.get(0);
406 if (this.firstRun && (results.length > 0 || isLast)) {
407 this.$current = null;
408 this.$result.empty();
409 }
410 for (var i=0, l = results.length; i < l; i++) {
411 target.appendChild(renderItem.call(this, results[i]));
412 };
413 if (this.firstRun && results.length > 0) {
414 this.firstRun = false;
415 this.$current = $(target.firstChild);
416 this.$current.addClass('current');
417 if (this.selectFirstMatch) this.select();
418 scrollIntoView(this.$current[0], this.$view[0])
419 }
420 if (jQuery.browser.msie) this.$element[0].className += '';
421 }
422  
423 this.open = function(src) {
424 this.frame.location.href = src;
425 if (this.frame.highlight) this.frame.highlight(src);
426 }
427  
428 this.select = function() {
429 this.open(this.$current.data('path'));
430 }
431  
432 this.move = function(isDown) {
433 if (!this.$current) return;
434 var $next = this.$current[isDown ? 'next' : 'prev']();
435 if ($next.length) {
436 this.$current.removeClass('current');
437 $next.addClass('current');
438 scrollIntoView($next[0], this.$view[0]);
439 this.$current = $next;
440 }
441 return true;
442 }
443  
444 function renderItem(result) {
445 var li = document.createElement('li'),
446 html = '', badge = result.badge;
447 html += '<h1>' + hlt(result.title);
448 if (result.params) html += '<i>' + result.params + '</i>';
449 html += '</h1>';
450 html += '<p>';
451 if (typeof badge != 'undefined') {
452 html += '<span class="badge badge_' + (badge % 6 + 1) + '">' + escapeHTML(this.data.badges[badge] || 'unknown') + '</span>';
453 }
454 html += hlt(result.namespace) + '</p>';
455 if (result.snippet) html += '<p class="snippet">' + escapeHTML(result.snippet) + '</p>';
456 li.innerHTML = html;
457 jQuery.data(li, 'path', result.path);
458 return li;
459 }
460  
461 function hlt(html) {
462 return escapeHTML(html).replace(/\u0001/g, '<b>').replace(/\u0002/g, '</b>')
463 }
464  
465 function escapeHTML(html) {
466 return html.replace(/[&<>]/g, function(c) {
467 <> return '&#' + c.charCodeAt(0) + ';';
468 <> });
469 <> }
470  
471 <>});
472  
473 <>// tree.js ------------------------------------------------
474  
475 <>Searchdoc.Tree = function(element, tree, panel) {
476 <> this.$element = $(element);
477 <> this.$list = $('ul', element);
478 <> this.tree = tree;
479 <> this.panel = panel;
480 <> this.init();
481 <>}
482  
483 <>Searchdoc.Tree.prototype = $.extend({}, Searchdoc.Navigation, new function() {
484 <> this.init = function() {
485 <> var stopper = document.createElement('li');
486 <> stopper.className = 'stopper';
487 <> this.$list[0].appendChild(stopper);
488 <> for (var i=0, l = this.tree.length; i < l; i++) {
489 <> buildAndAppendItem.call(this, this.tree[i], 0, stopper);
490 <> };
491 <> var _this = this;
492 <> this.$list.click(function(e) {
493 <> var $target = $(e.target),
494 <> $li = $target.closest('li');
495 <> if ($target.hasClass('icon')) {
496 <> _this.toggle($li);
497 <> } else {
498 <> _this.select($li);
499 <> }
500 <> })
501  
502 <> this.initNavigation();
503 <> if (jQuery.browser.msie) document.body.className += '';
504 <> }
505  
506 <> this.select = function($li) {
507 <> this.highlight($li);
508 <> var path = $li[0].searchdoc_tree_data.path;
509 <> if (path) this.panel.open(path);
510 <> }
511  
512 <> this.highlight = function($li) {
513 <> if (this.$current) this.$current.removeClass('current');
514 <> this.$current = $li.addClass('current');
515 <> }
516  
517 <> this.toggle = function($li) {
518 <> var closed = !$li.hasClass('closed'),
519 <> children = $li[0].searchdoc_tree_data.children;
520 <> $li.toggleClass('closed');
521 <> for (var i=0, l = children.length; i < l; i++) {
522 <> toggleVis.call(this, $(children[i].li), !closed);
523 <> };
524 <> }
525  
526 <> this.moveRight = function() {
527 <> if (!this.$current) {
528 <> this.highlight(this.$list.find('li:first'));
529 <> return;
530 <> }
531 <> if (this.$current.hasClass('closed')) {
532 <> this.toggle(this.$current);
533 <> }
534 <> }
535  
536 <> this.moveLeft = function() {
537 <> if (!this.$current) {
538 <> this.highlight(this.$list.find('li:first'));
539 <> return;
540 <> }
541 <> if (!this.$current.hasClass('closed')) {
542 <> this.toggle(this.$current);
543 <> } else {
544 <> var level = this.$current[0].searchdoc_tree_data.level;
545 <> if (level == 0) return;
546 <> var $next = this.$current.prevAll('li.level_' + (level - 1) + ':visible:first');
547 <> this.$current.removeClass('current');
548 <> $next.addClass('current');
549 <> scrollIntoView($next[0], this.$element[0]);
550 <> this.$current = $next;
551 <> }
552 <> }
553  
554 <> this.move = function(isDown) {
555 <> if (!this.$current) {
556 <> this.highlight(this.$list.find('li:first'));
557 <> return true;
558 <> }
559 <> var next = this.$current[0];
560 <> if (isDown) {
561 <> do {
562 <> next = next.nextSibling;
563 <> if (next && next.style && next.style.display != 'none') break;
564 <> } while(next);
565 <> } else {
566 <> do {
567 <> next = next.previousSibling;
568 <> if (next && next.style && next.style.display != 'none') break;
569 <> } while(next);
570 <> }
571 <> if (next && next.className.indexOf('stopper') == -1) {
572 <> this.$current.removeClass('current');
573 <> $(next).addClass('current');
574 <> scrollIntoView(next, this.$element[0]);
575 <> this.$current = $(next);
576 <> }
577 <> return true;
578 <> }
579  
580 <> function toggleVis($li, show) {
581 <> var closed = $li.hasClass('closed'),
582 <> children = $li[0].searchdoc_tree_data.children;
583 <> $li.css('display', show ? '' : 'none')
584 <> if (!show && this.$current && $li[0] == this.$current[0]) {
585 <> this.$current.removeClass('current');
586 <> this.$current = null;
587 <> }
588 <> for (var i=0, l = children.length; i < l; i++) {
589 <> toggleVis.call(this, $(children[i].li), show && !closed);
590 <> };
591 <> }
592  
593 <> function buildAndAppendItem(item, level, before) {
594 <> var li = renderItem(item, level),
595 <> list = this.$list[0];
596 <> item.li = li;
597 <> list.insertBefore(li, before);
598 <> for (var i=0, l = item[3].length; i < l; i++) {
599 <> buildAndAppendItem.call(this, item[3][i], level + 1, before);
600 <> };
601 <> return li;
602 <> }
603  
604 <> function renderItem(item, level) {
605 <> var li = document.createElement('li'),
606 <> cnt = document.createElement('div'),
607 <> h1 = document.createElement('h1'),
608 <> p = document.createElement('p'),
609 <> icon, i;
610  
611 <> li.appendChild(cnt);
612 <> li.style.paddingLeft = getOffset(level);
613 <> cnt.className = 'content';
614 <> if (!item[1]) li.className = 'empty ';
615 <> cnt.appendChild(h1);
616 <> // cnt.appendChild(p);
617 <> h1.appendChild(document.createTextNode(item[0]));
618 <> // p.appendChild(document.createTextNode(item[4]));
619 <> if (item[2]) {
620 <> i = document.createElement('i');
621 <> i.appendChild(document.createTextNode(item[2]));
622 <> h1.appendChild(i);
623 <> }
624 <> if (item[3].length > 0) {
625 <> icon = document.createElement('div');
626 <> icon.className = 'icon';
627 <> cnt.appendChild(icon);
628 <> }
629  
630 <> // user direct assignement instead of $()
631 <> // it's 8x faster
632 <> // $(li).data('path', item[1])
633 <> // .data('children', item[3])
634 <> // .data('level', level)
635 <> // .css('display', level == 0 ? '' : 'none')
636 <> // .addClass('level_' + level)
637 <> // .addClass('closed');
638 <> li.searchdoc_tree_data = {
639 <> path: item[1],
640 <> children: item[3],
641 <> level: level
642 <> }
643 <> li.style.display = level == 0 ? '' : 'none';
644 <> li.className += 'level_' + level + ' closed';
645 <> return li;
646 <> }
647  
648 <> function getOffset(level) {
649 <> return 5 + 18*level + 'px';
650 <> }
651 <>});