Fixed as per Tim's comments on r60665:
[mediawiki.git] / skins / common / mwsuggest.js
blob5cda6c9d0363eaabad601eb2bc57b266ef842359
1 /*
2  * OpenSearch ajax suggestion engine for MediaWiki
3  *
4  * uses core MediaWiki open search support to fetch suggestions
5  * and show them below search boxes and other inputs
6  *
7  * by Robert Stojnic (April 2008)
8  */
10 // search_box_id -> Results object
11 var os_map = {};
12 // cached data, url -> json_text
13 var os_cache = {};
14 // global variables for suggest_keypress
15 var os_cur_keypressed = 0;
16 var os_keypressed_count = 0;
17 // type: Timer
18 var os_timer = null;
19 // tie mousedown/up events
20 var os_mouse_pressed = false;
21 var os_mouse_num = -1;
22 // if true, the last change was made by mouse (and not keyboard)
23 var os_mouse_moved = false;
24 // delay between keypress and suggestion (in ms)
25 var os_search_timeout = 250;
26 // these pairs of inputs/forms will be autoloaded at startup
27 var os_autoload_inputs = new Array('searchInput', 'searchInput2', 'powerSearchText', 'searchText');
28 var os_autoload_forms = new Array('searchform', 'searchform2', 'powersearch', 'search' );
29 // if we stopped the service
30 var os_is_stopped = false;
31 // max lines to show in suggest table
32 var os_max_lines_per_suggest = 7;
33 // number of steps to animate expansion/contraction of container width
34 var os_animation_steps = 6;
35 // num of pixels of smallest step
36 var os_animation_min_step = 2;
37 // delay between steps (in ms)
38 var os_animation_delay = 30;
39 // max width of container in percent of normal size (1 == 100%)
40 var os_container_max_width = 2;
41 // currently active animation timer
42 var os_animation_timer = null;
44 /** Timeout timer class that will fetch the results */
45 function os_Timer( id, r, query ) {
46         this.id = id;
47         this.r = r;
48         this.query = query;
51 /** Timer user to animate expansion/contraction of container width */
52 function os_AnimationTimer( r, target ) {
53         this.r = r;
54         var current = document.getElementById(r.container).offsetWidth;
55         this.inc = Math.round( ( target - current ) / os_animation_steps );
56         if( this.inc < os_animation_min_step && this.inc >=0 ) {
57                 this.inc = os_animation_min_step; // minimal animation step
58         }
59         if( this.inc > -os_animation_min_step && this.inc < 0 ) {
60                 this.inc = -os_animation_min_step;
61         }
62         this.target = target;
65 /** Property class for single search box */
66 function os_Results( name, formname ) {
67         this.searchform = formname; // id of the searchform
68         this.searchbox = name; // id of the searchbox
69         this.container = name + 'Suggest'; // div that holds results
70         this.resultTable = name + 'Result'; // id base for the result table (+num = table row)
71         this.resultText = name + 'ResultText'; // id base for the spans within result tables (+num)
72         this.toggle = name + 'Toggle'; // div that has the toggle (enable/disable) link
73         this.query = null; // last processed query
74         this.results = null;  // parsed titles
75         this.resultCount = 0; // number of results
76         this.original = null; // query that user entered
77         this.selected = -1; // which result is selected
78         this.containerCount = 0; // number of results visible in container
79         this.containerRow = 0; // height of result field in the container
80         this.containerTotal = 0; // total height of the container will all results
81         this.visible = false; // if container is visible
82         this.stayHidden = false; // don't try to show if lost focus
85 /** Hide results div */
86 function os_hideResults( r ) {
87         var c = document.getElementById( r.container );
88         if( c !== null ) {
89                 c.style.visibility = 'hidden';
90         }
91         r.visible = false;
92         r.selected = -1;
95 /** Show results div */
96 function os_showResults( r ) {
97         if( os_is_stopped ) {
98                 return;
99         }
100         if( r.stayHidden ) {
101                 return;
102         }
103         os_fitContainer( r );
104         var c = document.getElementById( r.container );
105         r.selected = -1;
106         if( c !== null ) {
107                 c.scrollTop = 0;
108                 c.style.visibility = 'visible';
109                 r.visible = true;
110         }
113 function os_operaWidthFix( x ) {
114         // For browsers that don't understand overflow-x, estimate scrollbar width
115         if( typeof document.body.style.overflowX != 'string' ) {
116                 return 30;
117         }
118         return 0;
121 function os_encodeQuery( value ) {
122         if ( encodeURIComponent ) {
123                 return encodeURIComponent( value );
124         }
125         if( escape ) {
126                 return escape( value );
127         }
128         return null;
131 function os_decodeValue( value ) {
132         if ( decodeURIComponent ) {
133                 return decodeURIComponent( value );
134         }
135         if( unescape ) {
136                 return unescape( value );
137         }
138         return null;
141 /** Brower-dependent functions to find window inner size, and scroll status */
142 function f_clientWidth() {
143         return f_filterResults(
144                 window.innerWidth ? window.innerWidth : 0,
145                 document.documentElement ? document.documentElement.clientWidth : 0,
146                 document.body ? document.body.clientWidth : 0
147         );
150 function f_clientHeight() {
151         return f_filterResults(
152                 window.innerHeight ? window.innerHeight : 0,
153                 document.documentElement ? document.documentElement.clientHeight : 0,
154                 document.body ? document.body.clientHeight : 0
155         );
158 function f_scrollLeft() {
159         return f_filterResults(
160                 window.pageXOffset ? window.pageXOffset : 0,
161                 document.documentElement ? document.documentElement.scrollLeft : 0,
162                 document.body ? document.body.scrollLeft : 0
163         );
166 function f_scrollTop() {
167         return f_filterResults(
168                 window.pageYOffset ? window.pageYOffset : 0,
169                 document.documentElement ? document.documentElement.scrollTop : 0,
170                 document.body ? document.body.scrollTop : 0
171         );
174 function f_filterResults( n_win, n_docel, n_body ) {
175         var n_result = n_win ? n_win : 0;
176         if ( n_docel && ( !n_result || ( n_result > n_docel ) ) ) {
177                 n_result = n_docel;
178         }
179         return n_body && ( !n_result || ( n_result > n_body ) ) ? n_body : n_result;
182 /** Get the height available for the results container */
183 function os_availableHeight( r ) {
184         var absTop = document.getElementById( r.container ).style.top;
185         var px = absTop.lastIndexOf( 'px' );
186         if( px > 0 ) {
187                 absTop = absTop.substring( 0, px );
188         }
189         return f_clientHeight() - ( absTop - f_scrollTop() );
192 /** Get element absolute position {left,top} */
193 function os_getElementPosition( elemID ) {
194         var offsetTrail = document.getElementById( elemID );
195         var offsetLeft = 0;
196         var offsetTop = 0;
197         while ( offsetTrail ) {
198                 offsetLeft += offsetTrail.offsetLeft;
199                 offsetTop += offsetTrail.offsetTop;
200                 offsetTrail = offsetTrail.offsetParent;
201         }
202         if ( navigator.userAgent.indexOf('Mac') != -1 && typeof document.body.leftMargin != 'undefined' ) {
203                 offsetLeft += document.body.leftMargin;
204                 offsetTop += document.body.topMargin;
205         }
206         return { left:offsetLeft, top:offsetTop };
209 /** Create the container div that will hold the suggested titles */
210 function os_createContainer( r ) {
211         var c = document.createElement( 'div' );
212         var s = document.getElementById( r.searchbox );
213         var pos = os_getElementPosition( r.searchbox );
214         var left = pos.left;
215         var top = pos.top + s.offsetHeight;
216         c.className = 'os-suggest';
217         c.setAttribute( 'id', r.container );
218         document.body.appendChild( c );
220         // dynamically generated style params
221         // IE workaround, cannot explicitely set "style" attribute
222         c = document.getElementById( r.container );
223         c.style.top = top + 'px';
224         c.style.left = left + 'px';
225         c.style.width = s.offsetWidth + 'px';
227         // mouse event handlers
228         c.onmouseover = function( event ) { os_eventMouseover( r.searchbox, event ); };
229         c.onmousemove = function( event ) { os_eventMousemove( r.searchbox, event ); };
230         c.onmousedown = function( event ) { return os_eventMousedown( r.searchbox, event ); };
231         c.onmouseup = function( event ) { os_eventMouseup( r.searchbox, event ); };
232         return c;
235 /** change container height to fit to screen */
236 function os_fitContainer( r ) {
237         var c = document.getElementById( r.container );
238         var h = os_availableHeight( r ) - 20;
239         var inc = r.containerRow;
240         h = parseInt( h / inc ) * inc;
241         if( h < ( 2 * inc ) && r.resultCount > 1 ) { // min: two results
242                 h = 2 * inc;
243         }
244         if( ( h / inc ) > os_max_lines_per_suggest ) {
245                 h = inc * os_max_lines_per_suggest;
246         }
247         if( h < r.containerTotal ) {
248                 c.style.height = h + 'px';
249                 r.containerCount = parseInt( Math.round( h / inc ) );
250         } else {
251                 c.style.height = r.containerTotal + 'px';
252                 r.containerCount = r.resultCount;
253         }
256 /** If some entries are longer than the box, replace text with "..." */
257 function os_trimResultText( r ) {
258         // find max width, first see if we could expand the container to fit it
259         var maxW = 0;
260         for( var i = 0; i < r.resultCount; i++ ) {
261                 var e = document.getElementById( r.resultText + i );
262                 if( e.offsetWidth > maxW ) {
263                         maxW = e.offsetWidth;
264                 }
265         }
266         var w = document.getElementById( r.container ).offsetWidth;
267         var fix = 0;
268         if( r.containerCount < r.resultCount ) {
269                 fix = 20; // give 20px for scrollbar
270         } else {
271                 fix = os_operaWidthFix( w );
272         }
273         if( fix < 4 ) {
274                 fix = 4; // basic padding
275         }
276         maxW += fix;
278         // resize container to fit more data if permitted
279         var normW = document.getElementById( r.searchbox ).offsetWidth;
280         var prop = maxW / normW;
281         if( prop > os_container_max_width ) {
282                 prop = os_container_max_width;
283         } else if( prop < 1 ) {
284                 prop = 1;
285         }
286         var newW = Math.round( normW * prop );
287         if( w != newW ) {
288                 w = newW;
289                 if( os_animation_timer !== null ) {
290                         clearInterval( os_animation_timer.id );
291                 }
292                 os_animation_timer = new os_AnimationTimer( r, w );
293                 os_animation_timer.id = setInterval( "os_animateChangeWidth()", os_animation_delay );
294                 w -= fix; // this much is reserved
295         }
297         // trim results
298         if( w < 10 ) {
299                 return;
300         }
301         for( var i = 0; i < r.resultCount; i++ ) {
302                 var e = document.getElementById( r.resultText + i );
303                 var replace = 1;
304                 var lastW = e.offsetWidth + 1;
305                 var iteration = 0;
306                 var changedText = false;
307                 while( e.offsetWidth > w && ( e.offsetWidth < lastW || iteration < 2 ) ) {
308                         changedText = true;
309                         lastW = e.offsetWidth;
310                         var l = e.innerHTML;
311                         e.innerHTML = l.substring( 0, l.length - replace ) + '...';
312                         iteration++;
313                         replace = 4; // how many chars to replace
314                 }
315                 if( changedText ) {
316                         // show hint for trimmed titles
317                         document.getElementById( r.resultTable + i ).setAttribute( 'title', r.results[i] );
318                 }
319         }
322 /** Invoked on timer to animate change in container width */
323 function os_animateChangeWidth() {
324         var r = os_animation_timer.r;
325         var c = document.getElementById( r.container );
326         var w = c.offsetWidth;
327         var normW = document.getElementById( r.searchbox ).offsetWidth;
328         var normL = os_getElementPosition( r.searchbox ).left;
329         var inc = os_animation_timer.inc;
330         var target = os_animation_timer.target;
331         var nw = w + inc;
332         if( ( inc > 0 && nw >= target ) || ( inc <= 0 && nw <= target ) ) {
333                 // finished !
334                 c.style.width = target + 'px';
335                 clearInterval( os_animation_timer.id );
336                 os_animation_timer = null;
337         } else {
338                 // in-progress
339                 c.style.width = nw + 'px';
340                 if( document.documentElement.dir == 'rtl' ) {
341                         c.style.left = ( normL + normW + ( target - nw ) - os_animation_timer.target - 1 ) + 'px';
342                 }
343         }
346 /** Handles data from XMLHttpRequest, and updates the suggest results */
347 function os_updateResults( r, query, text, cacheKey ) {
348         os_cache[cacheKey] = text;
349         r.query = query;
350         r.original = query;
351         if( text == '' ) {
352                 r.results = null;
353                 r.resultCount = 0;
354                 os_hideResults( r );
355         } else {
356                 try {
357                         var p = eval( '(' + text + ')' ); // simple json parse, could do a safer one
358                         if( p.length < 2 || p[1].length === 0 ) {
359                                 r.results = null;
360                                 r.resultCount = 0;
361                                 os_hideResults( r );
362                                 return;
363                         }
364                         var c = document.getElementById( r.container );
365                         if( c === null ) {
366                                 c = os_createContainer( r );
367                         }
368                         c.innerHTML = os_createResultTable( r, p[1] );
369                         // init container table sizes
370                         var t = document.getElementById( r.resultTable );
371                         r.containerTotal = t.offsetHeight;
372                         r.containerRow = t.offsetHeight / r.resultCount;
373                         os_fitContainer( r );
374                         os_trimResultText( r );
375                         os_showResults( r );
376                 } catch( e ) {
377                         // bad response from server or such
378                         os_hideResults( r );
379                         os_cache[cacheKey] = null;
380                 }
381         }
384 /** Create the result table to be placed in the container div */
385 function os_createResultTable( r, results ) {
386         var c = document.getElementById( r.container );
387         var width = c.offsetWidth - os_operaWidthFix( c.offsetWidth );
388         var html = '<table class="os-suggest-results" id="' + r.resultTable + '" style="width: ' + width + 'px;">';
389         r.results = new Array();
390         r.resultCount = results.length;
391         for( i = 0; i < results.length; i++ ) {
392                 var title = os_decodeValue( results[i] );
393                 r.results[i] = title;
394                 html += '<tr><td class="os-suggest-result" id="' + r.resultTable + i + '"><span id="' + r.resultText + i + '">' + title + '</span></td></tr>';
395         }
396         html += '</table>';
397         return html;
400 /** Fetch namespaces from checkboxes or hidden fields in the search form,
401     if none defined use wgSearchNamespaces global */
402 function os_getNamespaces( r ) {
403         var namespaces = '';
404         var elements = document.forms[r.searchform].elements;
405         for( i = 0; i < elements.length; i++ ) {
406                 var name = elements[i].name;
407                 if( typeof name != 'undefined' && name.length > 2 && name[0] == 'n' &&
408                         name[1] == 's' && (
409                                 ( elements[i].type == 'checkbox' && elements[i].checked ) ||
410                                 ( elements[i].type == 'hidden' && elements[i].value == '1' )
411                         )
412                 ) {
413                         if( namespaces != '' ) {
414                                 namespaces += '|';
415                         }
416                         namespaces += name.substring( 2 );
417                 }
418         }
419         if( namespaces == '' ) {
420                 namespaces = wgSearchNamespaces.join('|');
421         }
422         return namespaces;
425 /** Update results if user hasn't already typed something else */
426 function os_updateIfRelevant( r, query, text, cacheKey ) {
427         var t = document.getElementById( r.searchbox );
428         if( t !== null && t.value == query ) { // check if response is still relevant
429                 os_updateResults( r, query, text, cacheKey );
430         }
431         r.query = query;
434 /** Fetch results after some timeout */
435 function os_delayedFetch() {
436         if( os_timer === null ) {
437                 return;
438         }
439         var r = os_timer.r;
440         var query = os_timer.query;
441         os_timer = null;
442         var path = wgMWSuggestTemplate.replace( "{namespaces}", os_getNamespaces( r ) )
443                                                                         .replace( "{dbname}", wgDBname )
444                                                                         .replace( "{searchTerms}", os_encodeQuery( query ) );
446         // try to get from cache, if not fetch using ajax
447         var cached = os_cache[path];
448         if( cached !== null ) {
449                 os_updateIfRelevant( r, query, cached, path );
450         } else {
451                 var xmlhttp = sajax_init_object();
452                 if( xmlhttp ) {
453                         try {
454                                 xmlhttp.open( 'GET', path, true );
455                                 xmlhttp.onreadystatechange = function() {
456                                         if ( xmlhttp.readyState == 4 && typeof os_updateIfRelevant == 'function' ) {
457                                                 os_updateIfRelevant( r, query, xmlhttp.responseText, path );
458                                         }
459                                 };
460                                 xmlhttp.send( null );
461                         } catch ( e ) {
462                                 if ( window.location.hostname == 'localhost' ) {
463                                         alert( "Your browser blocks XMLHttpRequest to 'localhost', try using a real hostname for development/testing." );
464                                 }
465                                 throw e;
466                         }
467                 }
468         }
471 /** Init timed update via os_delayedUpdate() */
472 function os_fetchResults( r, query, timeout ) {
473         if( query == '' ) {
474                 r.query = '';
475                 os_hideResults( r );
476                 return;
477         } else if( query == r.query ) {
478                 return; // no change
479         }
481         os_is_stopped = false; // make sure we're running
483         /* var cacheKey = wgDBname + ':' + query;
484         var cached = os_cache[cacheKey];
485         if( cached != null ) {
486                 os_updateResults( r, wgDBname, query, cached );
487                 return;
488         } */
490         // cancel any pending fetches
491         if( os_timer !== null && os_timer.id !== null ) {
492                 clearTimeout( os_timer.id );
493         }
494         // schedule delayed fetching of results
495         if( timeout !== 0 ) {
496                 os_timer = new os_Timer( setTimeout( "os_delayedFetch()", timeout ), r, query );
497         } else {
498                 os_timer = new os_Timer( null, r, query );
499                 os_delayedFetch(); // do it now!
500         }
504 /** Change the highlighted row (i.e. suggestion), from position cur to next */
505 function os_changeHighlight( r, cur, next, updateSearchBox ) {
506         if ( next >= r.resultCount ) {
507                 next = r.resultCount - 1;
508         }
509         if ( next < -1 ) {
510                 next = -1;
511         }
512         r.selected = next;
513         if ( cur == next ) {
514                 return; // nothing to do.
515         }
517         if( cur >= 0 ) {
518                 var curRow = document.getElementById( r.resultTable + cur );
519                 if( curRow !== null ) {
520                         curRow.className = 'os-suggest-result';
521                 }
522         }
523         var newText;
524         if( next >= 0 ) {
525                 var nextRow = document.getElementById( r.resultTable + next );
526                 if( nextRow !== null ) {
527                         nextRow.className = os_HighlightClass();
528                 }
529                 newText = r.results[next];
530         } else {
531                 newText = r.original;
532         }
534         // adjust the scrollbar if any
535         if( r.containerCount < r.resultCount ) {
536                 var c = document.getElementById( r.container );
537                 var vStart = c.scrollTop / r.containerRow;
538                 var vEnd = vStart + r.containerCount;
539                 if( next < vStart ) {
540                         c.scrollTop = next * r.containerRow;
541                 } else if( next >= vEnd ) {
542                         c.scrollTop = ( next - r.containerCount + 1 ) * r.containerRow;
543                 }
544         }
546         // update the contents of the search box
547         if( updateSearchBox ) {
548                 os_updateSearchQuery( r, newText );
549         }
552 function os_HighlightClass() {
553         var match = navigator.userAgent.match(/AppleWebKit\/(\d+)/);
554         if ( match ) {
555                 var webKitVersion = parseInt( match[1] );
556                 if ( webKitVersion < 523 ) {
557                         // CSS system highlight colors broken on old Safari
558                         // https://bugs.webkit.org/show_bug.cgi?id=6129
559                         // Safari 3.0.4, 3.1 known ok
560                         return 'os-suggest-result-hl-webkit';
561                 }
562         }
563         return 'os-suggest-result-hl';
566 function os_updateSearchQuery( r, newText ) {
567         document.getElementById( r.searchbox ).value = newText;
568         r.query = newText;
571 /** Find event target */
572 function os_getTarget( e ) {
573         if ( !e ) {
574                 e = window.event;
575         }
576         if ( e.target ) {
577                 return e.target;
578         } else if ( e.srcElement ) {
579                 return e.srcElement;
580         } else {
581                 return null;
582         }
586 /********************
587  *  Keyboard events
588  ********************/
590 /** Event handler that will fetch results on keyup */
591 function os_eventKeyup( e ) {
592         var targ = os_getTarget( e );
593         var r = os_map[targ.id];
594         if( r === null ) {
595                 return; // not our event
596         }
598         // some browsers won't generate keypressed for arrow keys, catch it
599         if( os_keypressed_count === 0 ) {
600                 os_processKey( r, os_cur_keypressed, targ );
601         }
602         var query = targ.value;
603         os_fetchResults( r, query, os_search_timeout );
606 /** catch arrows up/down and escape to hide the suggestions */
607 function os_processKey( r, keypressed, targ ) {
608         if ( keypressed == 40 ) { // Arrow Down
609                 if ( r.visible ) {
610                         os_changeHighlight( r, r.selected, r.selected + 1, true );
611                 } else if( os_timer === null ) {
612                         // user wants to get suggestions now
613                         r.query = '';
614                         os_fetchResults( r, targ.value, 0 );
615                 }
616         } else if ( keypressed == 38 ) { // Arrow Up
617                 if ( r.visible ) {
618                         os_changeHighlight( r, r.selected, r.selected - 1, true );
619                 }
620         } else if( keypressed == 27 ) { // Escape
621                 document.getElementById( r.searchbox ).value = r.original;
622                 r.query = r.original;
623                 os_hideResults( r );
624         } else if( r.query != document.getElementById( r.searchbox ).value ) {
625                 // os_hideResults( r ); // don't show old suggestions
626         }
629 /** When keys is held down use a timer to output regular events */
630 function os_eventKeypress( e ) {
631         var targ = os_getTarget( e );
632         var r = os_map[targ.id];
633         if( r === null ) {
634                 return; // not our event
635         }
637         var keypressed = os_cur_keypressed;
639         os_keypressed_count++;
640         os_processKey( r, keypressed, targ );
643 /** Catch the key code (Firefox bug) */
644 function os_eventKeydown( e ) {
645         if ( !e ) {
646                 e = window.event;
647         }
648         var targ = os_getTarget( e );
649         var r = os_map[targ.id];
650         if( r === null ) {
651                 return; // not our event
652         }
654         os_mouse_moved = false;
656         os_cur_keypressed = ( e.keyCode === undefined ) ? e.which : e.keyCode;
657         os_keypressed_count = 0;
660 /** Event: loss of focus of input box */
661 function os_eventBlur( e ) {
662         var targ = os_getTarget( e );
663         var r = os_map[targ.id];
664         if( r === null ) {
665                 return; // not our event
666         }
667         if( !os_mouse_pressed ) {
668                 os_hideResults( r );
669                 // force canvas to stay hidden
670                 r.stayHidden = true;
671                 // cancel any pending fetches
672                 if( os_timer !== null && os_timer.id !== null ) {
673                         clearTimeout( os_timer.id );
674                 }
675                 os_timer = null;
676         }
679 /** Event: focus (catch only when stopped) */
680 function os_eventFocus( e ) {
681         var targ = os_getTarget( e );
682         var r = os_map[targ.id];
683         if( r === null ) {
684                 return; // not our event
685         }
686         r.stayHidden = false;
690 /********************
691  *  Mouse events
692  ********************/
694 /** Mouse over the container */
695 function os_eventMouseover( srcId, e ) {
696         var targ = os_getTarget( e );
697         var r = os_map[srcId];
698         if( r === null || !os_mouse_moved ) {
699                 return; // not our event
700         }
701         var num = os_getNumberSuffix( targ.id );
702         if( num >= 0 ) {
703                 os_changeHighlight( r, r.selected, num, false );
704         }
707 /* Get row where the event occured (from its id) */
708 function os_getNumberSuffix( id ) {
709         var num = id.substring( id.length - 2 );
710         if( !( num.charAt( 0 ) >= '0' && num.charAt( 0 ) <= '9' ) ) {
711                 num = num.substring( 1 );
712         }
713         if( os_isNumber( num ) ) {
714                 return parseInt( num );
715         } else {
716                 return -1;
717         }
720 /** Save mouse move as last action */
721 function os_eventMousemove( srcId, e ) {
722         os_mouse_moved = true;
725 /** Mouse button held down, register possible click */
726 function os_eventMousedown( srcId, e ) {
727         var targ = os_getTarget( e );
728         var r = os_map[srcId];
729         if( r === null ) {
730                 return; // not our event
731         }
732         var num = os_getNumberSuffix( targ.id );
734         os_mouse_pressed = true;
735         if( num >= 0 ) {
736                 os_mouse_num = num;
737                 // os_updateSearchQuery( r, r.results[num] );
738         }
739         // keep the focus on the search field
740         document.getElementById( r.searchbox ).focus();
742         return false; // prevents selection
745 /** Mouse button released, check for click on some row */
746 function os_eventMouseup( srcId, e ) {
747         var targ = os_getTarget( e );
748         var r = os_map[srcId];
749         if( r === null ) {
750                 return; // not our event
751         }
752         var num = os_getNumberSuffix( targ.id );
754         if( num >= 0 && os_mouse_num == num ) {
755                 os_updateSearchQuery( r, r.results[num] );
756                 os_hideResults( r );
757                 document.getElementById( r.searchform ).submit();
758         }
759         os_mouse_pressed = false;
760         // keep the focus on the search field
761         document.getElementById( r.searchbox ).focus();
764 /** Check if x is a valid integer */
765 function os_isNumber( x ) {
766         if( x == '' || isNaN( x ) ) {
767                 return false;
768         }
769         for( var i = 0; i < x.length; i++ ) {
770                 var c = x.charAt( i );
771                 if( !( c >= '0' && c <= '9' ) ) {
772                         return false;
773                 }
774         }
775         return true;
778 /** When the form is submitted hide everything, cancel updates... */
779 function os_eventOnsubmit( e ) {
780         var targ = os_getTarget( e );
782         os_is_stopped = true;
783         // kill timed requests
784         if( os_timer !== null && os_timer.id !== null ) {
785                 clearTimeout( os_timer.id );
786                 os_timer = null;
787         }
788         // Hide all suggestions
789         for( i = 0; i < os_autoload_inputs.length; i++ ) {
790                 var r = os_map[os_autoload_inputs[i]];
791                 if( r !== null ) {
792                         var b = document.getElementById( r.searchform );
793                         if( b !== null && b == targ ) {
794                                 // set query value so the handler won't try to fetch additional results
795                                 r.query = document.getElementById( r.searchbox ).value;
796                         }
797                         os_hideResults( r );
798                 }
799         }
800         return true;
803 function os_hookEvent( element, hookName, hookFunct ) {
804         if ( element.addEventListener ) {
805                 element.addEventListener( hookName, hookFunct, false );
806         } else if ( window.attachEvent ) {
807                 element.attachEvent( 'on' + hookName, hookFunct );
808         }
811 /** Init Result objects and event handlers */
812 function os_initHandlers( name, formname, element ) {
813         var r = new os_Results( name, formname );
814         // event handler
815         os_hookEvent( element, 'keyup', function( event ) { os_eventKeyup( event ); } );
816         os_hookEvent( element, 'keydown', function( event ) { os_eventKeydown( event ); } );
817         os_hookEvent( element, 'keypress', function( event ) { os_eventKeypress( event ); } );
818         os_hookEvent( element, 'blur', function( event ) { os_eventBlur( event ); } );
819         os_hookEvent( element, 'focus', function( event ) { os_eventFocus( event ); } );
820         element.setAttribute( 'autocomplete', 'off' );
821         // stopping handler
822         os_hookEvent( document.getElementById( formname ), 'submit', function( event ) { return os_eventOnsubmit( event ); } );
823         os_map[name] = r;
824         // toggle link
825         if( document.getElementById( r.toggle ) === null ) {
826                 // TODO: disable this while we figure out a way for this to work in all browsers
827                 /* if( name == 'searchInput' ) {
828                         // special case: place above the main search box
829                         var t = os_createToggle( r, 'os-suggest-toggle' );
830                         var searchBody = document.getElementById( 'searchBody' );
831                         var first = searchBody.parentNode.firstChild.nextSibling.appendChild(t);
832                 } else {
833                         // default: place below search box to the right
834                         var t = os_createToggle( r, 'os-suggest-toggle-def' );
835                         var top = element.offsetTop + element.offsetHeight;
836                         var left = element.offsetLeft + element.offsetWidth;
837                         t.style.position = 'absolute';
838                         t.style.top = top + 'px';
839                         t.style.left = left + 'px';
840                         element.parentNode.appendChild( t );
841                         // only now width gets calculated, shift right
842                         left -= t.offsetWidth;
843                         t.style.left = left + 'px';
844                         t.style.visibility = 'visible';
845                 } */
846         }
850 /** Return the span element that contains the toggle link */
851 function os_createToggle( r, className ) {
852         var t = document.createElement( 'span' );
853         t.className = className;
854         t.setAttribute( 'id', r.toggle );
855         var link = document.createElement( 'a' );
856         link.setAttribute( 'href', 'javascript:void(0);' );
857         link.onclick = function() { os_toggle( r.searchbox, r.searchform ); };
858         var msg = document.createTextNode( wgMWSuggestMessages[0] );
859         link.appendChild( msg );
860         t.appendChild( link );
861         return t;
864 /** Call when user clicks on some of the toggle links */
865 function os_toggle( inputId, formName ) {
866         r = os_map[inputId];
867         var msg = '';
868         if( r === null ) {
869                 os_enableSuggestionsOn( inputId, formName );
870                 r = os_map[inputId];
871                 msg = wgMWSuggestMessages[0];
872         } else{
873                 os_disableSuggestionsOn( inputId, formName );
874                 msg = wgMWSuggestMessages[1];
875         }
876         // change message
877         var link = document.getElementById( r.toggle ).firstChild;
878         link.replaceChild( document.createTextNode( msg ), link.firstChild );
881 /** Call this to enable suggestions on input (id=inputId), on a form (name=formName) */
882 function os_enableSuggestionsOn( inputId, formName ) {
883         os_initHandlers( inputId, formName, document.getElementById( inputId ) );
886 /** Call this to disable suggestios on input box (id=inputId) */
887 function os_disableSuggestionsOn( inputId ) {
888         r = os_map[inputId];
889         if( r !== null ) {
890                 // cancel/hide results
891                 os_timer = null;
892                 os_hideResults( r );
893                 // turn autocomplete on !
894                 document.getElementById( inputId ).setAttribute( 'autocomplete', 'on' );
895                 // remove descriptor
896                 os_map[inputId] = null;
897         }
899         // Remove the element from the os_autoload_* arrays
900         var index = os_autoload_inputs.indexOf( inputId );
901         if ( index >= 0 ) {
902                 os_autoload_inputs[index] = os_autoload_forms[index] = '';
903         }
906 /** Initialization, call upon page onload */
907 function os_MWSuggestInit() {
908         for( i = 0; i < os_autoload_inputs.length; i++ ) {
909                 var id = os_autoload_inputs[i];
910                 var form = os_autoload_forms[i];
911                 element = document.getElementById( id );
912                 if( element !== null ) {
913                         os_initHandlers( id, form, element );
914                 }
915         }
918 hookEvent( 'load', os_MWSuggestInit );