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