]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - skins/common/mwsuggest.js
MediaWiki 1.17.4
[autoinstalls/mediawiki.git] / skins / common / mwsuggest.js
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  */
9
10 // Make sure wgMWSuggestTemplate is defined
11 if ( !mw.config.exists( 'wgMWSuggestTemplate' ) ) {
12         mw.config.set( 'wgMWSuggestTemplate', mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' )
13                  + "/api.php?action=opensearch\x26search={searchTerms}\x26namespace={namespaces}\x26suggest" );
14 }
15
16 // search_box_id -> Results object
17 window.os_map = {};
18 // cached data, url -> json_text
19 window.os_cache = {};
20 // global variables for suggest_keypress
21 window.os_cur_keypressed = 0;
22 window.os_keypressed_count = 0;
23 // type: Timer
24 window.os_timer = null;
25 // tie mousedown/up events
26 window.os_mouse_pressed = false;
27 window.os_mouse_num = -1;
28 // if true, the last change was made by mouse (and not keyboard)
29 window.os_mouse_moved = false;
30 // delay between keypress and suggestion (in ms)
31 window.os_search_timeout = 250;
32 // these pairs of inputs/forms will be autoloaded at startup
33 window.os_autoload_inputs = new Array('searchInput', 'searchInput2', 'powerSearchText', 'searchText');
34 window.os_autoload_forms = new Array('searchform', 'searchform2', 'powersearch', 'search' );
35 // if we stopped the service
36 window.os_is_stopped = false;
37 // max lines to show in suggest table
38 window.os_max_lines_per_suggest = 7;
39 // number of steps to animate expansion/contraction of container width
40 window.os_animation_steps = 6;
41 // num of pixels of smallest step
42 window.os_animation_min_step = 2;
43 // delay between steps (in ms)
44 window.os_animation_delay = 30;
45 // max width of container in percent of normal size (1 == 100%)
46 window.os_container_max_width = 2;
47 // currently active animation timer
48 window.os_animation_timer = null;
49 // whether MWSuggest is enabled. Set to false when os_MWSuggestDisable() is called
50 window.os_enabled = true;
51
52 /**
53  * <datalist> is a new HTML5 element that allows you to manually supply
54  * suggestion lists and have them rendered according to the right platform
55  * conventions.  However, the only shipping browser as of early 2010 is Opera,
56  * and that has a fatal problem: the suggestion lags behind what the user types
57  * by one keypress.  (Reported as DSK-276870 to Opera's secret bug tracker.)
58  * The code here otherwise seems to work, though, so this can be flipped on
59  * (maybe with a UA check) when some browser has a better implementation.
60  */
61 // var os_use_datalist = 'list' in document.createElement( 'input' );
62 window.os_use_datalist = false;
63
64 /** Timeout timer class that will fetch the results */
65 window.os_Timer = function( id, r, query ) {
66         this.id = id;
67         this.r = r;
68         this.query = query;
69 };
70
71 /** Property class for single search box */
72 window.os_Results = function( name, formname ) {
73         this.searchform = formname; // id of the searchform
74         this.searchbox = name; // id of the searchbox
75         this.container = name + 'Suggest'; // div that holds results
76         this.resultTable = name + 'Result'; // id base for the result table (+num = table row)
77         this.resultText = name + 'ResultText'; // id base for the spans within result tables (+num)
78         this.toggle = name + 'Toggle'; // div that has the toggle (enable/disable) link
79         this.query = null; // last processed query
80         this.results = null;  // parsed titles
81         this.resultCount = 0; // number of results
82         this.original = null; // query that user entered
83         this.selected = -1; // which result is selected
84         this.containerCount = 0; // number of results visible in container
85         this.containerRow = 0; // height of result field in the container
86         this.containerTotal = 0; // total height of the container will all results
87         this.visible = false; // if container is visible
88         this.stayHidden = false; // don't try to show if lost focus
89 };
90
91 /** Timer user to animate expansion/contraction of container width */
92 window.os_AnimationTimer = function( r, target ) {
93         this.r = r;
94         var current = document.getElementById(r.container).offsetWidth;
95         this.inc = Math.round( ( target - current ) / os_animation_steps );
96         if( this.inc < os_animation_min_step && this.inc >=0 ) {
97                 this.inc = os_animation_min_step; // minimal animation step
98         }
99         if( this.inc > -os_animation_min_step && this.inc < 0 ) {
100                 this.inc = -os_animation_min_step;
101         }
102         this.target = target;
103 };
104
105 /******************
106  * Initialization
107  ******************/
108
109 /** Initialization, call upon page onload */
110 window.os_MWSuggestInit = function() {
111         if ( !window.os_enabled ) {
112                 return;
113         }
114         
115         for( var i = 0; i < os_autoload_inputs.length; i++ ) {
116                 var id = os_autoload_inputs[i];
117                 var form = os_autoload_forms[i];
118                 element = document.getElementById( id );
119                 if( element != null ) {
120                         os_initHandlers( id, form, element );
121                 }
122         }
123 };
124
125 /* Teardown, called when things like SimpleSearch need to disable MWSuggest */
126 window.os_MWSuggestTeardown = function() {
127         for( var i = 0; i < os_autoload_inputs.length; i++ ) {
128                 var id = os_autoload_inputs[i];
129                 var form = os_autoload_forms[i];
130                 element = document.getElementById( id );
131                 if( element != null ) {
132                         os_teardownHandlers( id, form, element );
133                 }
134         }
135 };
136
137 /* Call this to disable MWSuggest. Works regardless of whether MWSuggest has been initialized already. */
138 window.os_MWSuggestDisable = function() {
139         window.os_MWSuggestTeardown();
140         window.os_enabled = false;
141 }
142         
143
144 /** Init Result objects and event handlers */
145 window.os_initHandlers = function( name, formname, element ) {
146         var r = new os_Results( name, formname );
147         var formElement = document.getElementById( formname );
148         if( !formElement ) {
149                 // Older browsers (Opera 8) cannot get form elements
150                 return;
151         }
152         // event handler
153         os_hookEvent( element, 'keyup', os_eventKeyup );
154         os_hookEvent( element, 'keydown', os_eventKeydown );
155         os_hookEvent( element, 'keypress', os_eventKeypress );
156         if ( !os_use_datalist ) {
157                 // These are needed for the div hack to hide it if the user blurs.
158                 os_hookEvent( element, 'blur', os_eventBlur );
159                 os_hookEvent( element, 'focus', os_eventFocus );
160                 // We don't want browser auto-suggestions interfering with our div, but
161                 // autocomplete must be on for datalist to work (at least in Opera
162                 // 10.10).
163                 element.setAttribute( 'autocomplete', 'off' );
164         }
165         // stopping handler
166         os_hookEvent( formElement, 'submit', os_eventOnsubmit );
167         os_map[name] = r;
168         // toggle link
169         if( document.getElementById( r.toggle ) == null ) {
170                 // TODO: disable this while we figure out a way for this to work in all browsers
171                 /* if( name == 'searchInput' ) {
172                         // special case: place above the main search box
173                         var t = os_createToggle( r, 'os-suggest-toggle' );
174                         var searchBody = document.getElementById( 'searchBody' );
175                         var first = searchBody.parentNode.firstChild.nextSibling.appendChild(t);
176                 } else {
177                         // default: place below search box to the right
178                         var t = os_createToggle( r, 'os-suggest-toggle-def' );
179                         var top = element.offsetTop + element.offsetHeight;
180                         var left = element.offsetLeft + element.offsetWidth;
181                         t.style.position = 'absolute';
182                         t.style.top = top + 'px';
183                         t.style.left = left + 'px';
184                         element.parentNode.appendChild( t );
185                         // only now width gets calculated, shift right
186                         left -= t.offsetWidth;
187                         t.style.left = left + 'px';
188                         t.style.visibility = 'visible';
189                 } */
190         }
191
192 };
193
194 window.os_teardownHandlers = function( name, formname, element ) {
195         var formElement = document.getElementById( formname );
196         if( !formElement ) {
197                 // Older browsers (Opera 8) cannot get form elements
198                 return;
199         }
200
201         os_unhookEvent( element, 'keyup', os_eventKeyup );
202         os_unhookEvent( element, 'keydown', os_eventKeydown );
203         os_unhookEvent( element, 'keypress', os_eventKeypress );
204         if ( !os_use_datalist ) {
205                 // These are needed for the div hack to hide it if the user blurs.
206                 os_unhookEvent( element, 'blur', os_eventBlur );
207                 os_unhookEvent( element, 'focus', os_eventFocus );
208                 // We don't want browser auto-suggestions interfering with our div, but
209                 // autocomplete must be on for datalist to work (at least in Opera
210                 // 10.10).
211                 element.removeAttribute( 'autocomplete' );
212         }
213         // stopping handler
214         os_unhookEvent( formElement, 'submit', os_eventOnsubmit );
215 };
216
217
218 window.os_hookEvent = function( element, hookName, hookFunct ) {
219         if ( element.addEventListener ) {
220                 element.addEventListener( hookName, hookFunct, false );
221         } else if ( window.attachEvent ) {
222                 element.attachEvent( 'on' + hookName, hookFunct );
223         }
224 };
225
226 window.os_unhookEvent = function( element, hookName, hookFunct ) {
227         if ( element.removeEventListener ) {
228                 element.removeEventListener( hookName, hookFunct, false );
229         } else if ( element.detachEvent ) {
230                 element.detachEvent( 'on' + hookName, hookFunct );
231         }
232 }
233
234 /********************
235  *  Keyboard events
236  ********************/
237
238 /** Event handler that will fetch results on keyup */
239 window.os_eventKeyup = function( e ) {
240         var targ = os_getTarget( e );
241         var r = os_map[targ.id];
242         if( r == null ) {
243                 return; // not our event
244         }
245
246         // some browsers won't generate keypressed for arrow keys, catch it
247         if( os_keypressed_count == 0 ) {
248                 os_processKey( r, os_cur_keypressed, targ );
249         }
250         var query = targ.value;
251         os_fetchResults( r, query, os_search_timeout );
252 };
253
254 /** catch arrows up/down and escape to hide the suggestions */
255 window.os_processKey = function( r, keypressed, targ ) {
256         if ( keypressed == 40 && !r.visible && os_timer == null ) {
257                 // If the user hits the down arrow, fetch results immediately if none
258                 // are already displayed.
259                 r.query = '';
260                 os_fetchResults( r, targ.value, 0 );
261         }
262         // Otherwise, if we're not using datalist, we need to handle scrolling and
263         // so on.
264         if ( os_use_datalist ) {
265                 return;
266         }
267         if ( keypressed == 40 ) { // Arrow Down
268                 if ( r.visible ) {
269                         os_changeHighlight( r, r.selected, r.selected + 1, true );
270                 }
271         } else if ( keypressed == 38 ) { // Arrow Up
272                 if ( r.visible ) {
273                         os_changeHighlight( r, r.selected, r.selected - 1, true );
274                 }
275         } else if( keypressed == 27 ) { // Escape
276                 document.getElementById( r.searchbox ).value = r.original;
277                 r.query = r.original;
278                 os_hideResults( r );
279         } else if( r.query != document.getElementById( r.searchbox ).value ) {
280                 // os_hideResults( r ); // don't show old suggestions
281         }
282 };
283
284 /** When keys is held down use a timer to output regular events */
285 window.os_eventKeypress = function( e ) {
286         var targ = os_getTarget( e );
287         var r = os_map[targ.id];
288         if( r == null ) {
289                 return; // not our event
290         }
291
292         var keypressed = os_cur_keypressed;
293
294         os_keypressed_count++;
295         os_processKey( r, keypressed, targ );
296 };
297
298 /** Catch the key code (Firefox bug) */
299 window.os_eventKeydown = function( e ) {
300         if ( !e ) {
301                 e = window.event;
302         }
303         var targ = os_getTarget( e );
304         var r = os_map[targ.id];
305         if( r == null ) {
306                 return; // not our event
307         }
308
309         os_mouse_moved = false;
310
311         os_cur_keypressed = ( e.keyCode == undefined ) ? e.which : e.keyCode;
312         os_keypressed_count = 0;
313 };
314
315
316 /** When the form is submitted hide everything, cancel updates... */
317 window.os_eventOnsubmit = function( e ) {
318         var targ = os_getTarget( e );
319
320         os_is_stopped = true;
321         // kill timed requests
322         if( os_timer != null && os_timer.id != null ) {
323                 clearTimeout( os_timer.id );
324                 os_timer = null;
325         }
326         // Hide all suggestions
327         for( i = 0; i < os_autoload_inputs.length; i++ ) {
328                 var r = os_map[os_autoload_inputs[i]];
329                 if( r != null ) {
330                         var b = document.getElementById( r.searchform );
331                         if( b != null && b == targ ) {
332                                 // set query value so the handler won't try to fetch additional results
333                                 r.query = document.getElementById( r.searchbox ).value;
334                         }
335                         os_hideResults( r );
336                 }
337         }
338         return true;
339 };
340
341
342
343 /** Hide results from the user, either making the div visibility=hidden or
344  * detaching the datalist from the input. */
345 window.os_hideResults = function( r ) {
346         if ( os_use_datalist ) {
347                 document.getElementById( r.searchbox ).setAttribute( 'list', '' );
348         } else {
349                 var c = document.getElementById( r.container );
350                 if ( c != null ) {
351                         c.style.visibility = 'hidden';
352                 }
353         }
354         r.visible = false;
355         r.selected = -1;
356 };
357
358 window.os_decodeValue = function( value ) {
359         if ( decodeURIComponent ) {
360                 return decodeURIComponent( value );
361         }
362         if( unescape ) {
363                 return unescape( value );
364         }
365         return null;
366 };
367
368 window.os_encodeQuery = function( value ) {
369         if ( encodeURIComponent ) {
370                 return encodeURIComponent( value );
371         }
372         if( escape ) {
373                 return escape( value );
374         }
375         return null;
376 };
377
378 /** Handles data from XMLHttpRequest, and updates the suggest results */
379 window.os_updateResults = function( r, query, text, cacheKey ) {
380         os_cache[cacheKey] = text;
381         r.query = query;
382         r.original = query;
383         if( text == '' ) {
384                 r.results = null;
385                 r.resultCount = 0;
386                 os_hideResults( r );
387         } else {
388                 try {
389                         var p = eval( '(' + text + ')' ); // simple json parse, could do a safer one
390                         if( p.length < 2 || p[1].length == 0 ) {
391                                 r.results = null;
392                                 r.resultCount = 0;
393                                 os_hideResults( r );
394                                 return;
395                         }
396                         if ( os_use_datalist ) {
397                                 os_setupDatalist( r, p[1] );
398                         } else {
399                                 os_setupDiv( r, p[1] );
400                         }
401                 } catch( e ) {
402                         // bad response from server or such
403                         os_hideResults( r );
404                         os_cache[cacheKey] = null;
405                 }
406         }
407 };
408
409 /**
410  * Create and populate a <datalist>.
411  *
412  * @param r       os_Result object
413  * @param results Array of the new results to replace existing ones
414  */
415 window.os_setupDatalist = function( r, results ) {
416         var s = document.getElementById( r.searchbox );
417         var c = document.getElementById( r.container );
418         if ( c == null ) {
419                 c = document.createElement( 'datalist' );
420                 c.setAttribute( 'id', r.container );
421                 document.body.appendChild( c );
422         } else {
423                 c.innerHTML = '';
424         }
425         s.setAttribute( 'list', r.container );
426
427         r.results = new Array();
428         r.resultCount = results.length;
429         r.visible = true;
430         for ( i = 0; i < results.length; i++ ) {
431                 var title = os_decodeValue( results[i] );
432                 var opt = document.createElement( 'option' );
433                 opt.value = title;
434                 r.results[i] = title;
435                 c.appendChild( opt );
436         }
437 };
438
439 /** Fetch namespaces from checkboxes or hidden fields in the search form,
440     if none defined use wgSearchNamespaces global */
441 window.os_getNamespaces = function( r ) {
442         var namespaces = '';
443         var elements = document.forms[r.searchform].elements;
444         for( i = 0; i < elements.length; i++ ) {
445                 var name = elements[i].name;
446                 if( typeof name != 'undefined' && name.length > 2 && name[0] == 'n' &&
447                         name[1] == 's' && (
448                                 ( elements[i].type == 'checkbox' && elements[i].checked ) ||
449                                 ( elements[i].type == 'hidden' && elements[i].value == '1' )
450                         )
451                 ) {
452                         if( namespaces != '' ) {
453                                 namespaces += '|';
454                         }
455                         namespaces += name.substring( 2 );
456                 }
457         }
458         if( namespaces == '' ) {
459                 namespaces = wgSearchNamespaces.join('|');
460         }
461         return namespaces;
462 };
463
464 /** Update results if user hasn't already typed something else */
465 window.os_updateIfRelevant = function( r, query, text, cacheKey ) {
466         var t = document.getElementById( r.searchbox );
467         if( t != null && t.value == query ) { // check if response is still relevant
468                 os_updateResults( r, query, text, cacheKey );
469         }
470         r.query = query;
471 };
472
473 /** Fetch results after some timeout */
474 window.os_delayedFetch = function() {
475         if( os_timer == null ) {
476                 return;
477         }
478         var r = os_timer.r;
479         var query = os_timer.query;
480         os_timer = null;
481         var path = mw.config.get( 'wgMWSuggestTemplate' ).replace( "{namespaces}", os_getNamespaces( r ) )
482                                                                         .replace( "{dbname}", wgDBname )
483                                                                         .replace( "{searchTerms}", os_encodeQuery( query ) );
484
485         // try to get from cache, if not fetch using ajax
486         var cached = os_cache[path];
487         if( cached != null && cached != undefined ) {
488                 os_updateIfRelevant( r, query, cached, path );
489         } else {
490                 var xmlhttp = sajax_init_object();
491                 if( xmlhttp ) {
492                         try {
493                                 xmlhttp.open( 'GET', path, true );
494                                 xmlhttp.onreadystatechange = function() {
495                                         if ( xmlhttp.readyState == 4 && typeof os_updateIfRelevant == 'function' ) {
496                                                 os_updateIfRelevant( r, query, xmlhttp.responseText, path );
497                                         }
498                                 };
499                                 xmlhttp.send( null );
500                         } catch ( e ) {
501                                 if ( window.location.hostname == 'localhost' ) {
502                                         alert( "Your browser blocks XMLHttpRequest to 'localhost', try using a real hostname for development/testing." );
503                                 }
504                                 throw e;
505                         }
506                 }
507         }
508 };
509
510 /** Init timed update via os_delayedUpdate() */
511 window.os_fetchResults = function( r, query, timeout ) {
512         if( query == '' ) {
513                 r.query = '';
514                 os_hideResults( r );
515                 return;
516         } else if( query == r.query ) {
517                 return; // no change
518         }
519
520         os_is_stopped = false; // make sure we're running
521
522         // cancel any pending fetches
523         if( os_timer != null && os_timer.id != null ) {
524                 clearTimeout( os_timer.id );
525         }
526         // schedule delayed fetching of results
527         if( timeout != 0 ) {
528                 os_timer = new os_Timer( setTimeout( "os_delayedFetch()", timeout ), r, query );
529         } else {
530                 os_timer = new os_Timer( null, r, query );
531                 os_delayedFetch(); // do it now!
532         }
533 };
534
535 /** Find event target */
536 window.os_getTarget = function( e ) {
537         if ( !e ) {
538                 e = window.event;
539         }
540         if ( e.target ) {
541                 return e.target;
542         } else if ( e.srcElement ) {
543                 return e.srcElement;
544         } else {
545                 return null;
546         }
547 };
548
549 /** Check if x is a valid integer */
550 window.os_isNumber = function( x ) {
551         if( x == '' || isNaN( x ) ) {
552                 return false;
553         }
554         for( var i = 0; i < x.length; i++ ) {
555                 var c = x.charAt( i );
556                 if( !( c >= '0' && c <= '9' ) ) {
557                         return false;
558                 }
559         }
560         return true;
561 };
562
563 /** Call this to enable suggestions on input (id=inputId), on a form (name=formName) */
564 window.os_enableSuggestionsOn = function( inputId, formName ) {
565         os_initHandlers( inputId, formName, document.getElementById( inputId ) );
566 };
567
568 /** Call this to disable suggestios on input box (id=inputId) */
569 window.os_disableSuggestionsOn = function( inputId ) {
570         r = os_map[inputId];
571         if( r != null ) {
572                 // cancel/hide results
573                 os_timer = null;
574                 os_hideResults( r );
575                 // turn autocomplete on !
576                 document.getElementById( inputId ).setAttribute( 'autocomplete', 'on' );
577                 // remove descriptor
578                 os_map[inputId] = null;
579         }
580
581         // Remove the element from the os_autoload_* arrays
582         var index = os_autoload_inputs.indexOf( inputId );
583         if ( index >= 0 ) {
584                 os_autoload_inputs[index] = os_autoload_forms[index] = '';
585         }
586 };
587
588 /************************************************
589  * Div-only functions (irrelevant for datalist)
590  ************************************************/
591
592 /** Event: loss of focus of input box */
593 window.os_eventBlur = function( e ) {
594         var targ = os_getTarget( e );
595         var r = os_map[targ.id];
596         if( r == null ) {
597                 return; // not our event
598         }
599         if( !os_mouse_pressed ) {
600                 os_hideResults( r );
601                 // force canvas to stay hidden
602                 r.stayHidden = true;
603                 // cancel any pending fetches
604                 if( os_timer != null && os_timer.id != null ) {
605                         clearTimeout( os_timer.id );
606                 }
607                 os_timer = null;
608         }
609 };
610
611 /** Event: focus (catch only when stopped) */
612 window.os_eventFocus = function( e ) {
613         var targ = os_getTarget( e );
614         var r = os_map[targ.id];
615         if( r == null ) {
616                 return; // not our event
617         }
618         r.stayHidden = false;
619 };
620
621 /**
622  * Create and populate a <div>, for non-<datalist>-supporting browsers.
623  *
624  * @param r       os_Result object
625  * @param results Array of the new results to replace existing ones
626  */
627 window.os_setupDiv = function( r, results ) {
628         var c = document.getElementById( r.container );
629         if ( c == null ) {
630                 c = os_createContainer( r );
631         }
632         c.innerHTML = os_createResultTable( r, results );
633         // init container table sizes
634         var t = document.getElementById( r.resultTable );
635         r.containerTotal = t.offsetHeight;
636         r.containerRow = t.offsetHeight / r.resultCount;
637         os_fitContainer( r );
638         os_trimResultText( r );
639         os_showResults( r );
640 };
641
642 /** Create the result table to be placed in the container div */
643 window.os_createResultTable = function( r, results ) {
644         var c = document.getElementById( r.container );
645         var width = c.offsetWidth - os_operaWidthFix( c.offsetWidth );
646         var html = '<table class="os-suggest-results" id="' + r.resultTable + '" style="width: ' + width + 'px;">';
647         r.results = new Array();
648         r.resultCount = results.length;
649         for( i = 0; i < results.length; i++ ) {
650                 var title = os_decodeValue( results[i] );
651                 r.results[i] = title;
652                 html += '<tr><td class="os-suggest-result" id="' + r.resultTable + i + '"><span id="' + r.resultText + i + '">' + title + '</span></td></tr>';
653         }
654         html += '</table>';
655         return html;
656 };
657
658 /** Show results div */
659 window.os_showResults = function( r ) {
660         if( os_is_stopped ) {
661                 return;
662         }
663         if( r.stayHidden ) {
664                 return;
665         }
666         os_fitContainer( r );
667         var c = document.getElementById( r.container );
668         r.selected = -1;
669         if( c != null ) {
670                 c.scrollTop = 0;
671                 c.style.visibility = 'visible';
672                 r.visible = true;
673         }
674 };
675
676 window.os_operaWidthFix = function( x ) {
677         // For browsers that don't understand overflow-x, estimate scrollbar width
678         if( typeof document.body.style.overflowX != 'string' ) {
679                 return 30;
680         }
681         return 0;
682 };
683
684 /** Brower-dependent functions to find window inner size, and scroll status */
685 window.f_clientWidth = function() {
686         return f_filterResults(
687                 window.innerWidth ? window.innerWidth : 0,
688                 document.documentElement ? document.documentElement.clientWidth : 0,
689                 document.body ? document.body.clientWidth : 0
690         );
691 };
692
693 window.f_clientHeight = function() {
694         return f_filterResults(
695                 window.innerHeight ? window.innerHeight : 0,
696                 document.documentElement ? document.documentElement.clientHeight : 0,
697                 document.body ? document.body.clientHeight : 0
698         );
699 };
700
701 window.f_scrollLeft = function() {
702         return f_filterResults(
703                 window.pageXOffset ? window.pageXOffset : 0,
704                 document.documentElement ? document.documentElement.scrollLeft : 0,
705                 document.body ? document.body.scrollLeft : 0
706         );
707 };
708
709 window.f_scrollTop = function() {
710         return f_filterResults(
711                 window.pageYOffset ? window.pageYOffset : 0,
712                 document.documentElement ? document.documentElement.scrollTop : 0,
713                 document.body ? document.body.scrollTop : 0
714         );
715 };
716
717 window.f_filterResults = function( n_win, n_docel, n_body ) {
718         var n_result = n_win ? n_win : 0;
719         if ( n_docel && ( !n_result || ( n_result > n_docel ) ) ) {
720                 n_result = n_docel;
721         }
722         return n_body && ( !n_result || ( n_result > n_body ) ) ? n_body : n_result;
723 };
724
725 /** Get the height available for the results container */
726 window.os_availableHeight = function( r ) {
727         var absTop = document.getElementById( r.container ).style.top;
728         var px = absTop.lastIndexOf( 'px' );
729         if( px > 0 ) {
730                 absTop = absTop.substring( 0, px );
731         }
732         return f_clientHeight() - ( absTop - f_scrollTop() );
733 };
734
735 /** Get element absolute position {left,top} */
736 window.os_getElementPosition = function( elemID ) {
737         var offsetTrail = document.getElementById( elemID );
738         var offsetLeft = 0;
739         var offsetTop = 0;
740         while ( offsetTrail ) {
741                 offsetLeft += offsetTrail.offsetLeft;
742                 offsetTop += offsetTrail.offsetTop;
743                 offsetTrail = offsetTrail.offsetParent;
744         }
745         if ( navigator.userAgent.indexOf('Mac') != -1 && typeof document.body.leftMargin != 'undefined' ) {
746                 offsetLeft += document.body.leftMargin;
747                 offsetTop += document.body.topMargin;
748         }
749         return { left:offsetLeft, top:offsetTop };
750 };
751
752 /** Create the container div that will hold the suggested titles */
753 window.os_createContainer = function(r) {
754         var c = document.createElement('div');
755         var s = document.getElementById(r.searchbox);
756         var pos = os_getElementPosition(r.searchbox);
757         var left = pos.left;
758         var top = pos.top + s.offsetHeight;
759         c.className = 'os-suggest';
760         c.setAttribute('id', r.container);
761         document.body.appendChild(c);
762
763         // dynamically generated style params
764         // IE workaround, cannot explicitely set "style" attribute
765         c = document.getElementById(r.container);
766         c.style.top = top + 'px';
767         c.style.left = left + 'px';
768         c.style.width = s.offsetWidth + 'px';
769
770         // mouse event handlers
771         c.onmouseover = function(event) {
772                 os_eventMouseover(r.searchbox, event);
773         };
774         c.onmousemove = function(event) {
775                 os_eventMousemove(r.searchbox, event);
776         };
777         c.onmousedown = function(event) {
778                 return os_eventMousedown(r.searchbox, event);
779         };
780         c.onmouseup = function(event) {
781                 os_eventMouseup(r.searchbox, event);
782         };
783         return c;
784 };
785
786 /** change container height to fit to screen */
787 window.os_fitContainer = function( r ) {
788         var c = document.getElementById( r.container );
789         var h = os_availableHeight( r ) - 20;
790         var inc = r.containerRow;
791         h = parseInt( h / inc ) * inc;
792         if( h < ( 2 * inc ) && r.resultCount > 1 ) { // min: two results
793                 h = 2 * inc;
794         }
795         if( ( h / inc ) > os_max_lines_per_suggest ) {
796                 h = inc * os_max_lines_per_suggest;
797         }
798         if( h < r.containerTotal ) {
799                 c.style.height = h + 'px';
800                 r.containerCount = parseInt( Math.round( h / inc ) );
801         } else {
802                 c.style.height = r.containerTotal + 'px';
803                 r.containerCount = r.resultCount;
804         }
805 };
806
807 /** If some entries are longer than the box, replace text with "..." */
808 window.os_trimResultText = function( r ) {
809         // find max width, first see if we could expand the container to fit it
810         var maxW = 0;
811         for( var i = 0; i < r.resultCount; i++ ) {
812                 var e = document.getElementById( r.resultText + i );
813                 if( e.offsetWidth > maxW ) {
814                         maxW = e.offsetWidth;
815                 }
816         }
817         var w = document.getElementById( r.container ).offsetWidth;
818         var fix = 0;
819         if( r.containerCount < r.resultCount ) {
820                 fix = 20; // give 20px for scrollbar
821         } else {
822                 fix = os_operaWidthFix( w );
823         }
824         if( fix < 4 ) {
825                 fix = 4; // basic padding
826         }
827         maxW += fix;
828
829         // resize container to fit more data if permitted
830         var normW = document.getElementById( r.searchbox ).offsetWidth;
831         var prop = maxW / normW;
832         if( prop > os_container_max_width ) {
833                 prop = os_container_max_width;
834         } else if( prop < 1 ) {
835                 prop = 1;
836         }
837         var newW = Math.round( normW * prop );
838         if( w != newW ) {
839                 w = newW;
840                 if( os_animation_timer != null ) {
841                         clearInterval( os_animation_timer.id );
842                 }
843                 os_animation_timer = new os_AnimationTimer( r, w );
844                 os_animation_timer.id = setInterval( "os_animateChangeWidth()", os_animation_delay );
845                 w -= fix; // this much is reserved
846         }
847
848         // trim results
849         if( w < 10 ) {
850                 return;
851         }
852         for( var i = 0; i < r.resultCount; i++ ) {
853                 var e = document.getElementById( r.resultText + i );
854                 var replace = 1;
855                 var lastW = e.offsetWidth + 1;
856                 var iteration = 0;
857                 var changedText = false;
858                 while( e.offsetWidth > w && ( e.offsetWidth < lastW || iteration < 2 ) ) {
859                         changedText = true;
860                         lastW = e.offsetWidth;
861                         var l = e.innerHTML;
862                         e.innerHTML = l.substring( 0, l.length - replace ) + '...';
863                         iteration++;
864                         replace = 4; // how many chars to replace
865                 }
866                 if( changedText ) {
867                         // show hint for trimmed titles
868                         document.getElementById( r.resultTable + i ).setAttribute( 'title', r.results[i] );
869                 }
870         }
871 };
872
873 /** Invoked on timer to animate change in container width */
874 window.os_animateChangeWidth = function() {
875         var r = os_animation_timer.r;
876         var c = document.getElementById( r.container );
877         var w = c.offsetWidth;
878         var normW = document.getElementById( r.searchbox ).offsetWidth;
879         var normL = os_getElementPosition( r.searchbox ).left;
880         var inc = os_animation_timer.inc;
881         var target = os_animation_timer.target;
882         var nw = w + inc;
883         if( ( inc > 0 && nw >= target ) || ( inc <= 0 && nw <= target ) ) {
884                 // finished !
885                 c.style.width = target + 'px';
886                 clearInterval( os_animation_timer.id );
887                 os_animation_timer = null;
888         } else {
889                 // in-progress
890                 c.style.width = nw + 'px';
891                 if( document.documentElement.dir == 'rtl' ) {
892                         c.style.left = ( normL + normW + ( target - nw ) - os_animation_timer.target - 1 ) + 'px';
893                 }
894         }
895 };
896
897 /** Change the highlighted row (i.e. suggestion), from position cur to next */
898 window.os_changeHighlight = function( r, cur, next, updateSearchBox ) {
899         if ( next >= r.resultCount ) {
900                 next = r.resultCount - 1;
901         }
902         if ( next < -1 ) {
903                 next = -1;
904         }
905         r.selected = next;
906         if ( cur == next ) {
907                 return; // nothing to do.
908         }
909
910         if( cur >= 0 ) {
911                 var curRow = document.getElementById( r.resultTable + cur );
912                 if( curRow != null ) {
913                         curRow.className = 'os-suggest-result';
914                 }
915         }
916         var newText;
917         if( next >= 0 ) {
918                 var nextRow = document.getElementById( r.resultTable + next );
919                 if( nextRow != null ) {
920                         nextRow.className = os_HighlightClass();
921                 }
922                 newText = r.results[next];
923         } else {
924                 newText = r.original;
925         }
926
927         // adjust the scrollbar if any
928         if( r.containerCount < r.resultCount ) {
929                 var c = document.getElementById( r.container );
930                 var vStart = c.scrollTop / r.containerRow;
931                 var vEnd = vStart + r.containerCount;
932                 if( next < vStart ) {
933                         c.scrollTop = next * r.containerRow;
934                 } else if( next >= vEnd ) {
935                         c.scrollTop = ( next - r.containerCount + 1 ) * r.containerRow;
936                 }
937         }
938
939         // update the contents of the search box
940         if( updateSearchBox ) {
941                 os_updateSearchQuery( r, newText );
942         }
943 };
944
945 window.os_HighlightClass = function() {
946         var match = navigator.userAgent.match(/AppleWebKit\/(\d+)/);
947         if ( match ) {
948                 var webKitVersion = parseInt( match[1] );
949                 if ( webKitVersion < 523 ) {
950                         // CSS system highlight colors broken on old Safari
951                         // https://bugs.webkit.org/show_bug.cgi?id=6129
952                         // Safari 3.0.4, 3.1 known ok
953                         return 'os-suggest-result-hl-webkit';
954                 }
955         }
956         return 'os-suggest-result-hl';
957 };
958
959 window.os_updateSearchQuery = function( r, newText ) {
960         document.getElementById( r.searchbox ).value = newText;
961         r.query = newText;
962 };
963
964
965 /********************
966  *  Mouse events
967  ********************/
968
969 /** Mouse over the container */
970 window.os_eventMouseover = function( srcId, e ) {
971         var targ = os_getTarget( e );
972         var r = os_map[srcId];
973         if( r == null || !os_mouse_moved ) {
974                 return; // not our event
975         }
976         var num = os_getNumberSuffix( targ.id );
977         if( num >= 0 ) {
978                 os_changeHighlight( r, r.selected, num, false );
979         }
980 };
981
982 /* Get row where the event occured (from its id) */
983 window.os_getNumberSuffix = function( id ) {
984         var num = id.substring( id.length - 2 );
985         if( !( num.charAt( 0 ) >= '0' && num.charAt( 0 ) <= '9' ) ) {
986                 num = num.substring( 1 );
987         }
988         if( os_isNumber( num ) ) {
989                 return parseInt( num );
990         } else {
991                 return -1;
992         }
993 };
994
995 /** Save mouse move as last action */
996 window.os_eventMousemove = function( srcId, e ) {
997         os_mouse_moved = true;
998 };
999
1000 /** Mouse button held down, register possible click */
1001 window.os_eventMousedown = function( srcId, e ) {
1002         var targ = os_getTarget( e );
1003         var r = os_map[srcId];
1004         if( r == null ) {
1005                 return; // not our event
1006         }
1007         var num = os_getNumberSuffix( targ.id );
1008
1009         os_mouse_pressed = true;
1010         if( num >= 0 ) {
1011                 os_mouse_num = num;
1012                 // os_updateSearchQuery( r, r.results[num] );
1013         }
1014         // keep the focus on the search field
1015         document.getElementById( r.searchbox ).focus();
1016
1017         return false; // prevents selection
1018 };
1019
1020 /** Mouse button released, check for click on some row */
1021 window.os_eventMouseup = function( srcId, e ) {
1022         var targ = os_getTarget( e );
1023         var r = os_map[srcId];
1024         if( r == null ) {
1025                 return; // not our event
1026         }
1027         var num = os_getNumberSuffix( targ.id );
1028
1029         if( num >= 0 && os_mouse_num == num ) {
1030                 os_updateSearchQuery( r, r.results[num] );
1031                 os_hideResults( r );
1032                 document.getElementById( r.searchform ).submit();
1033         }
1034         os_mouse_pressed = false;
1035         // keep the focus on the search field
1036         document.getElementById( r.searchbox ).focus();
1037 };
1038
1039 /** Toggle stuff seems to be dead code? */
1040
1041 /** Return the span element that contains the toggle link */
1042 window.os_createToggle = function( r, className ) {
1043         var t = document.createElement( 'span' );
1044         t.className = className;
1045         t.setAttribute( 'id', r.toggle );
1046         var link = document.createElement( 'a' );
1047         link.setAttribute( 'href', 'javascript:void(0);' );
1048         link.onclick = function() { os_toggle( r.searchbox, r.searchform ); };
1049         var msg = document.createTextNode( wgMWSuggestMessages[0] );
1050         link.appendChild( msg );
1051         t.appendChild( link );
1052         return t;
1053 };
1054
1055 /** Call when user clicks on some of the toggle links */
1056 window.os_toggle = function( inputId, formName ) {
1057         r = os_map[inputId];
1058         var msg = '';
1059         if( r == null ) {
1060                 os_enableSuggestionsOn( inputId, formName );
1061                 r = os_map[inputId];
1062                 msg = wgMWSuggestMessages[0];
1063         } else{
1064                 os_disableSuggestionsOn( inputId, formName );
1065                 msg = wgMWSuggestMessages[1];
1066         }
1067         // change message
1068         var link = document.getElementById( r.toggle ).firstChild;
1069         link.replaceChild( document.createTextNode( msg ), link.firstChild );
1070 };
1071
1072 hookEvent( 'load', os_MWSuggestInit );