2 * OpenSearch ajax suggestion engine for MediaWiki
4 * uses core MediaWiki open search support to fetch suggestions
5 * and show them below search boxes and other inputs
7 * by Robert Stojnic (April 2008)
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
18 // cached data, url -> json_text
20 // global variables for suggest_keypress
21 window
.os_cur_keypressed
= 0;
22 window
.os_keypressed_count
= 0;
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;
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. Opera as of version 11 has a fatal problem: the suggestion
56 * lags behind what the user types by one keypress. (Reported as DSK-276870 to
57 * Opera's secret bug tracker.) However, Firefox 4 supports it without
58 * problems, so Opera is just blacklisted here. Ideally we wouldn't blacklist
59 * future versions, in case they fix it, but the fallback isn't bad at all and
60 * the failure if they don't fix it is very annoying, so in this case we'll
61 * blacklist future versions too.
63 window
.os_use_datalist
= 'list' in document
.createElement( 'input' )
64 && $.client
.profile().name
!= 'opera';
66 /** Timeout timer class that will fetch the results */
67 window
.os_Timer = function( id
, r
, query
) {
73 /** Property class for single search box */
74 window
.os_Results = function( name
, formname
) {
75 this.searchform
= formname
; // id of the searchform
76 this.searchbox
= name
; // id of the searchbox
77 this.container
= name
+ 'Suggest'; // div that holds results
78 this.resultTable
= name
+ 'Result'; // id base for the result table (+num = table row)
79 this.resultText
= name
+ 'ResultText'; // id base for the spans within result tables (+num)
80 this.toggle
= name
+ 'Toggle'; // div that has the toggle (enable/disable) link
81 this.query
= null; // last processed query
82 this.results
= null; // parsed titles
83 this.resultCount
= 0; // number of results
84 this.original
= null; // query that user entered
85 this.selected
= -1; // which result is selected
86 this.containerCount
= 0; // number of results visible in container
87 this.containerRow
= 0; // height of result field in the container
88 this.containerTotal
= 0; // total height of the container will all results
89 this.visible
= false; // if container is visible
90 this.stayHidden
= false; // don't try to show if lost focus
93 /** Timer user to animate expansion/contraction of container width */
94 window.os_AnimationTimer = function( r, target ) {
96 var current = document.getElementById(r.container).offsetWidth;
97 this.inc = Math.round( ( target - current ) / os_animation_steps );
98 if( this.inc < os_animation_min_step && this.inc >=0 ) {
99 this.inc = os_animation_min_step; // minimal animation step
101 if( this.inc > -os_animation_min_step && this.inc < 0 ) {
102 this.inc = -os_animation_min_step;
104 this.target = target;
111 /** Initialization, call upon page onload */
112 window
.os_MWSuggestInit = function() {
113 if ( !window
.os_enabled
) {
117 for( var i
= 0; i
< os_autoload_inputs
.length
; i
++ ) {
118 var id
= os_autoload_inputs
[i
];
119 var form
= os_autoload_forms
[i
];
120 element
= document
.getElementById( id
);
121 if( element
!= null ) {
122 os_initHandlers( id
, form
, element
);
127 /* Teardown, called when things like SimpleSearch need to disable MWSuggest */
128 window
.os_MWSuggestTeardown = function() {
129 for( var i
= 0; i
< os_autoload_inputs
.length
; i
++ ) {
130 var id
= os_autoload_inputs
[i
];
131 var form
= os_autoload_forms
[i
];
132 element
= document
.getElementById( id
);
133 if( element
!= null ) {
134 os_teardownHandlers( id
, form
, element
);
139 /* Call this to disable MWSuggest. Works regardless of whether MWSuggest has been initialized already. */
140 window
.os_MWSuggestDisable = function() {
141 window
.os_MWSuggestTeardown();
142 window
.os_enabled
= false;
146 /** Init Result objects and event handlers */
147 window
.os_initHandlers = function( name
, formname
, element
) {
148 var r
= new os_Results( name
, formname
);
149 var formElement
= document
.getElementById( formname
);
151 // Older browsers (Opera 8) cannot get form elements
155 os_hookEvent( element
, 'keyup', os_eventKeyup
);
156 os_hookEvent( element
, 'keydown', os_eventKeydown
);
157 os_hookEvent( element
, 'keypress', os_eventKeypress
);
158 if ( !os_use_datalist
) {
159 // These are needed for the div hack to hide it if the user blurs.
160 os_hookEvent( element
, 'blur', os_eventBlur
);
161 os_hookEvent( element
, 'focus', os_eventFocus
);
162 // We don't want browser auto-suggestions interfering with our div, but
163 // autocomplete must be on for datalist to work (at least in Opera
165 element
.setAttribute( 'autocomplete', 'off' );
168 os_hookEvent( formElement
, 'submit', os_eventOnsubmit
);
171 if( document
.getElementById( r
.toggle
) == null ) {
172 // TODO: disable this while we figure out a way for this to work in all browsers
173 /* if( name == 'searchInput' ) {
174 // special case: place above the main search box
175 var t = os_createToggle( r, 'os-suggest-toggle' );
176 var searchBody = document.getElementById( 'searchBody' );
177 var first = searchBody.parentNode.firstChild.nextSibling.appendChild(t);
179 // default: place below search box to the right
180 var t = os_createToggle( r, 'os-suggest-toggle-def' );
181 var top = element.offsetTop + element.offsetHeight;
182 var left = element.offsetLeft + element.offsetWidth;
183 t.style.position = 'absolute';
184 t.style.top = top + 'px';
185 t.style.left = left + 'px';
186 element.parentNode.appendChild( t );
187 // only now width gets calculated, shift right
188 left -= t.offsetWidth;
189 t.style.left = left + 'px';
190 t.style.visibility = 'visible';
196 window
.os_teardownHandlers = function( name
, formname
, element
) {
197 var formElement
= document
.getElementById( formname
);
199 // Older browsers (Opera 8) cannot get form elements
203 os_unhookEvent( element
, 'keyup', os_eventKeyup
);
204 os_unhookEvent( element
, 'keydown', os_eventKeydown
);
205 os_unhookEvent( element
, 'keypress', os_eventKeypress
);
206 if ( !os_use_datalist
) {
207 // These are needed for the div hack to hide it if the user blurs.
208 os_unhookEvent( element
, 'blur', os_eventBlur
);
209 os_unhookEvent( element
, 'focus', os_eventFocus
);
210 // We don't want browser auto-suggestions interfering with our div, but
211 // autocomplete must be on for datalist to work (at least in Opera
213 element
.removeAttribute( 'autocomplete' );
216 os_unhookEvent( formElement
, 'submit', os_eventOnsubmit
);
220 window
.os_hookEvent = function( element
, hookName
, hookFunct
) {
221 if ( element
.addEventListener
) {
222 element
.addEventListener( hookName
, hookFunct
, false );
223 } else if ( window
.attachEvent
) {
224 element
.attachEvent( 'on' + hookName
, hookFunct
);
228 window
.os_unhookEvent = function( element
, hookName
, hookFunct
) {
229 if ( element
.removeEventListener
) {
230 element
.removeEventListener( hookName
, hookFunct
, false );
231 } else if ( element
.detachEvent
) {
232 element
.detachEvent( 'on' + hookName
, hookFunct
);
236 /********************
238 ********************/
240 /** Event handler that will fetch results on keyup */
241 window
.os_eventKeyup = function( e
) {
242 var targ
= os_getTarget( e
);
243 var r
= os_map
[targ
.id
];
245 return; // not our event
248 // some browsers won't generate keypressed for arrow keys, catch it
249 if( os_keypressed_count
== 0 ) {
250 os_processKey( r
, os_cur_keypressed
, targ
);
252 var query
= targ
.value
;
253 os_fetchResults( r
, query
, os_search_timeout
);
256 /** catch arrows up/down and escape to hide the suggestions */
257 window.os_processKey = function( r, keypressed, targ ) {
258 if ( keypressed == 40 && !r.visible && os_timer == null ) {
259 // If the user hits the down arrow, fetch results immediately if none
260 // are already displayed.
262 os_fetchResults( r, targ.value, 0 );
264 // Otherwise, if we're not using datalist, we need to handle scrolling and
266 if ( os_use_datalist ) {
269 if ( keypressed == 40 ) { // Arrow Down
271 os_changeHighlight( r, r.selected, r.selected + 1, true );
273 } else if ( keypressed == 38 ) { // Arrow Up
275 os_changeHighlight( r, r.selected, r.selected - 1, true );
277 } else if( keypressed == 27 ) { // Escape
278 document.getElementById( r.searchbox ).value = r.original;
279 r.query = r.original;
281 } else if( r.query != document.getElementById( r.searchbox ).value ) {
282 // os_hideResults( r ); // don't show old suggestions
286 /** When keys is held down use a timer to output regular events */
287 window
.os_eventKeypress = function( e
) {
288 var targ
= os_getTarget( e
);
289 var r
= os_map
[targ
.id
];
291 return; // not our event
294 var keypressed
= os_cur_keypressed
;
296 os_keypressed_count
++;
297 os_processKey( r
, keypressed
, targ
);
300 /** Catch the key code (Firefox bug) */
301 window
.os_eventKeydown = function( e
) {
305 var targ
= os_getTarget( e
);
306 var r
= os_map
[targ
.id
];
308 return; // not our event
311 os_mouse_moved
= false;
313 os_cur_keypressed
= ( e
.keyCode
== undefined ) ? e
.which
: e
.keyCode
;
314 os_keypressed_count
= 0;
318 /** When the form is submitted hide everything, cancel updates... */
319 window
.os_eventOnsubmit = function( e
) {
320 var targ
= os_getTarget( e
);
322 os_is_stopped
= true;
323 // kill timed requests
324 if( os_timer
!= null && os_timer
.id
!= null ) {
325 clearTimeout( os_timer
.id
);
328 // Hide all suggestions
329 for( i
= 0; i
< os_autoload_inputs
.length
; i
++ ) {
330 var r
= os_map
[os_autoload_inputs
[i
]];
332 var b
= document
.getElementById( r
.searchform
);
333 if( b
!= null && b
== targ
) {
334 // set query value so the handler won't try to fetch additional results
335 r
.query
= document
.getElementById( r
.searchbox
).value
;
345 /** Hide results from the user, either making the div visibility=hidden or
346 * detaching the datalist from the input. */
347 window
.os_hideResults = function( r
) {
348 if ( os_use_datalist
) {
349 document
.getElementById( r
.searchbox
).setAttribute( 'list', '' );
351 var c
= document
.getElementById( r
.container
);
353 c
.style
.visibility
= 'hidden';
360 window
.os_decodeValue = function( value
) {
361 if ( decodeURIComponent
) {
362 return decodeURIComponent( value
);
365 return unescape( value
);
370 window
.os_encodeQuery = function( value
) {
371 if ( encodeURIComponent
) {
372 return encodeURIComponent( value
);
375 return escape( value
);
380 /** Handles data from XMLHttpRequest, and updates the suggest results */
381 window
.os_updateResults = function( r
, query
, text
, cacheKey
) {
382 os_cache
[cacheKey
] = text
;
391 var p
= eval( '(' + text
+ ')' ); // simple json parse, could do a safer one
392 if( p
.length
< 2 || p
[1].length
== 0 ) {
398 if ( os_use_datalist
) {
399 os_setupDatalist( r
, p
[1] );
401 os_setupDiv( r
, p
[1] );
404 // bad response from server or such
406 os_cache
[cacheKey
] = null;
412 * Create and populate a <datalist>.
414 * @param r os_Result object
415 * @param results Array of the new results to replace existing ones
417 window
.os_setupDatalist = function( r
, results
) {
418 var s
= document
.getElementById( r
.searchbox
);
419 var c
= document
.getElementById( r
.container
);
421 c
= document
.createElement( 'datalist' );
422 c
.setAttribute( 'id', r
.container
);
423 document
.body
.appendChild( c
);
427 s
.setAttribute( 'list', r
.container
);
429 r
.results
= new Array();
430 r
.resultCount
= results
.length
;
432 for ( i
= 0; i
< results
.length
; i
++ ) {
433 var title
= os_decodeValue( results
[i
] );
434 var opt
= document
.createElement( 'option' );
436 r
.results
[i
] = title
;
437 c
.appendChild( opt
);
441 /** Fetch namespaces from checkboxes or hidden fields in the search form,
442 if none defined use wgSearchNamespaces global */
443 window
.os_getNamespaces = function( r
) {
445 var elements
= document
.forms
[r
.searchform
].elements
;
446 for( i
= 0; i
< elements
.length
; i
++ ) {
447 var name
= elements
[i
].name
;
448 if( typeof name
!= 'undefined' && name
.length
> 2 && name
[0] == 'n' &&
450 ( elements
[i
].type
== 'checkbox' && elements
[i
].checked
) ||
451 ( elements
[i
].type
== 'hidden' && elements
[i
].value
== '1' )
454 if( namespaces
!= '' ) {
457 namespaces
+= name
.substring( 2 );
460 if( namespaces
== '' ) {
461 namespaces
= wgSearchNamespaces
.join('|');
466 /** Update results if user hasn't already typed something else */
467 window
.os_updateIfRelevant = function( r
, query
, text
, cacheKey
) {
468 var t
= document
.getElementById( r
.searchbox
);
469 if( t
!= null && t
.value
== query
) { // check if response is still relevant
470 os_updateResults( r
, query
, text
, cacheKey
);
475 /** Fetch results after some timeout */
476 window
.os_delayedFetch = function() {
477 if( os_timer
== null ) {
481 var query
= os_timer
.query
;
483 var path
= mw
.config
.get( 'wgMWSuggestTemplate' ).replace( "{namespaces}", os_getNamespaces( r
) )
484 .replace( "{dbname}", wgDBname
)
485 .replace( "{searchTerms}", os_encodeQuery( query
) );
487 // try to get from cache, if not fetch using ajax
488 var cached
= os_cache
[path
];
489 if( cached
!= null && cached
!= undefined ) {
490 os_updateIfRelevant( r
, query
, cached
, path
);
492 var xmlhttp
= sajax_init_object();
495 xmlhttp
.open( 'GET', path
, true );
496 xmlhttp
.onreadystatechange = function() {
497 if ( xmlhttp
.readyState
== 4 && typeof os_updateIfRelevant
== 'function' ) {
498 os_updateIfRelevant( r
, query
, xmlhttp
.responseText
, path
);
501 xmlhttp
.send( null );
503 if ( window
.location
.hostname
== 'localhost' ) {
504 alert( "Your browser blocks XMLHttpRequest to 'localhost', try using a real hostname for development/testing." );
512 /** Init timed update via os_delayedUpdate() */
513 window
.os_fetchResults = function( r
, query
, timeout
) {
518 } else if( query
== r
.query
) {
522 os_is_stopped
= false; // make sure we're running
524 // cancel any pending fetches
525 if( os_timer
!= null && os_timer
.id
!= null ) {
526 clearTimeout( os_timer
.id
);
528 // schedule delayed fetching of results
530 os_timer
= new os_Timer( setTimeout( "os_delayedFetch()", timeout
), r
, query
);
532 os_timer
= new os_Timer( null, r
, query
);
533 os_delayedFetch(); // do it now!
537 /** Find event target */
538 window
.os_getTarget = function( e
) {
544 } else if ( e
.srcElement
) {
551 /** Check if x is a valid integer */
552 window
.os_isNumber = function( x
) {
553 if( x
== '' || isNaN( x
) ) {
556 for( var i
= 0; i
< x
.length
; i
++ ) {
557 var c
= x
.charAt( i
);
558 if( !( c
>= '0' && c
<= '9' ) ) {
565 /** Call this to enable suggestions on input (id=inputId), on a form (name=formName) */
566 window
.os_enableSuggestionsOn = function( inputId
, formName
) {
567 os_initHandlers( inputId
, formName
, document
.getElementById( inputId
) );
570 /** Call this to disable suggestios on input box (id=inputId) */
571 window
.os_disableSuggestionsOn = function( inputId
) {
574 // cancel/hide results
577 // turn autocomplete on !
578 document
.getElementById( inputId
).setAttribute( 'autocomplete', 'on' );
580 os_map
[inputId
] = null;
583 // Remove the element from the os_autoload_* arrays
584 var index
= os_autoload_inputs
.indexOf( inputId
);
586 os_autoload_inputs
[index
] = os_autoload_forms
[index
] = '';
590 /************************************************
591 * Div-only functions (irrelevant for datalist)
592 ************************************************/
594 /** Event: loss of focus of input box */
595 window
.os_eventBlur = function( e
) {
596 var targ
= os_getTarget( e
);
597 var r
= os_map
[targ
.id
];
599 return; // not our event
601 if( !os_mouse_pressed
) {
603 // force canvas to stay hidden
605 // cancel any pending fetches
606 if( os_timer
!= null && os_timer
.id
!= null ) {
607 clearTimeout( os_timer
.id
);
613 /** Event: focus (catch only when stopped) */
614 window
.os_eventFocus = function( e
) {
615 var targ
= os_getTarget( e
);
616 var r
= os_map
[targ
.id
];
618 return; // not our event
620 r
.stayHidden
= false;
624 * Create and populate a <div>, for non-<datalist>-supporting browsers.
626 * @param r os_Result object
627 * @param results Array of the new results to replace existing ones
629 window
.os_setupDiv = function( r
, results
) {
630 var c
= document
.getElementById( r
.container
);
632 c
= os_createContainer( r
);
634 c
.innerHTML
= os_createResultTable( r
, results
);
635 // init container table sizes
636 var t
= document
.getElementById( r
.resultTable
);
637 r
.containerTotal
= t
.offsetHeight
;
638 r
.containerRow
= t
.offsetHeight
/ r
.resultCount
;
639 os_fitContainer( r
);
640 os_trimResultText( r
);
644 /** Create the result table to be placed in the container div */
645 window
.os_createResultTable = function( r
, results
) {
646 var c
= document
.getElementById( r
.container
);
647 var width
= c
.offsetWidth
- os_operaWidthFix( c
.offsetWidth
);
648 var html
= '<table class="os-suggest-results" id="' + r
.resultTable
+ '" style="width: ' + width
+ 'px;">';
649 r
.results
= new Array();
650 r
.resultCount
= results
.length
;
651 for( i
= 0; i
< results
.length
; i
++ ) {
652 var title
= os_decodeValue( results
[i
] );
653 r
.results
[i
] = title
;
654 html
+= '<tr><td class="os-suggest-result" id="' + r
.resultTable
+ i
+ '"><span id="' + r
.resultText
+ i
+ '">' + title
+ '</span></td></tr>';
660 /** Show results div */
661 window
.os_showResults = function( r
) {
662 if( os_is_stopped
) {
668 os_fitContainer( r
);
669 var c
= document
.getElementById( r
.container
);
673 c
.style
.visibility
= 'visible';
678 window
.os_operaWidthFix = function( x
) {
679 // For browsers that don't understand overflow-x, estimate scrollbar width
680 if( typeof document
.body
.style
.overflowX
!= 'string' ) {
686 /** Brower-dependent functions to find window inner size, and scroll status */
687 window
.f_clientWidth = function() {
688 return f_filterResults(
689 window
.innerWidth
? window
.innerWidth
: 0,
690 document
.documentElement
? document
.documentElement
.clientWidth
: 0,
691 document
.body
? document
.body
.clientWidth
: 0
695 window
.f_clientHeight = function() {
696 return f_filterResults(
697 window
.innerHeight
? window
.innerHeight
: 0,
698 document
.documentElement
? document
.documentElement
.clientHeight
: 0,
699 document
.body
? document
.body
.clientHeight
: 0
703 window
.f_scrollLeft = function() {
704 return f_filterResults(
705 window
.pageXOffset
? window
.pageXOffset
: 0,
706 document
.documentElement
? document
.documentElement
.scrollLeft
: 0,
707 document
.body
? document
.body
.scrollLeft
: 0
711 window
.f_scrollTop = function() {
712 return f_filterResults(
713 window
.pageYOffset
? window
.pageYOffset
: 0,
714 document
.documentElement
? document
.documentElement
.scrollTop
: 0,
715 document
.body
? document
.body
.scrollTop
: 0
719 window
.f_filterResults = function( n_win
, n_docel
, n_body
) {
720 var n_result
= n_win
? n_win
: 0;
721 if ( n_docel
&& ( !n_result
|| ( n_result
> n_docel
) ) ) {
724 return n_body
&& ( !n_result
|| ( n_result
> n_body
) ) ? n_body
: n_result
;
727 /** Get the height available for the results container */
728 window
.os_availableHeight = function( r
) {
729 var absTop
= document
.getElementById( r
.container
).style
.top
;
730 var px
= absTop
.lastIndexOf( 'px' );
732 absTop
= absTop
.substring( 0, px
);
734 return f_clientHeight() - ( absTop
- f_scrollTop() );
737 /** Get element absolute position {left,top} */
738 window
.os_getElementPosition = function( elemID
) {
739 var offsetTrail
= document
.getElementById( elemID
);
742 while ( offsetTrail
) {
743 offsetLeft
+= offsetTrail
.offsetLeft
;
744 offsetTop
+= offsetTrail
.offsetTop
;
745 offsetTrail
= offsetTrail
.offsetParent
;
747 if ( navigator
.userAgent
.indexOf('Mac') != -1 && typeof document
.body
.leftMargin
!= 'undefined' ) {
748 offsetLeft
+= document
.body
.leftMargin
;
749 offsetTop
+= document
.body
.topMargin
;
751 return { left
:offsetLeft
, top
:offsetTop
};
754 /** Create the container div that will hold the suggested titles */
755 window
.os_createContainer = function( r
) {
756 var c
= document
.createElement( 'div' );
757 var s
= document
.getElementById( r
.searchbox
);
758 var pos
= os_getElementPosition( r
.searchbox
);
760 var top
= pos
.top
+ s
.offsetHeight
;
761 c
.className
= 'os-suggest';
762 c
.setAttribute( 'id', r
.container
);
763 document
.body
.appendChild( c
);
765 // dynamically generated style params
766 // IE workaround, cannot explicitely set "style" attribute
767 c
= document
.getElementById( r
.container
);
768 c
.style
.top
= top
+ 'px';
769 c
.style
.left
= left
+ 'px';
770 c
.style
.width
= s
.offsetWidth
+ 'px';
772 // mouse event handlers
773 c
.onmouseover = function( event
) { os_eventMouseover( r
.searchbox
, event
); };
774 c
.onmousemove = function( event
) { os_eventMousemove( r
.searchbox
, event
); };
775 c
.onmousedown = function( event
) { return os_eventMousedown( r
.searchbox
, event
); };
776 c
.onmouseup = function( event
) { os_eventMouseup( r
.searchbox
, event
); };
780 /** change container height to fit to screen */
781 window
.os_fitContainer = function( r
) {
782 var c
= document
.getElementById( r
.container
);
783 var h
= os_availableHeight( r
) - 20;
784 var inc
= r
.containerRow
;
785 h
= parseInt( h
/ inc
) * inc
;
786 if( h
< ( 2 * inc
) && r
.resultCount
> 1 ) { // min: two results
789 if( ( h
/ inc
) > os_max_lines_per_suggest
) {
790 h
= inc
* os_max_lines_per_suggest
;
792 if( h
< r
.containerTotal
) {
793 c
.style
.height
= h
+ 'px';
794 r
.containerCount
= parseInt( Math
.round( h
/ inc
) );
796 c
.style
.height
= r
.containerTotal
+ 'px';
797 r
.containerCount
= r
.resultCount
;
801 /** If some entries are longer than the box, replace text with "..." */
802 window
.os_trimResultText = function( r
) {
803 // find max width, first see if we could expand the container to fit it
805 for( var i
= 0; i
< r
.resultCount
; i
++ ) {
806 var e
= document
.getElementById( r
.resultText
+ i
);
807 if( e
.offsetWidth
> maxW
) {
808 maxW
= e
.offsetWidth
;
811 var w
= document
.getElementById( r
.container
).offsetWidth
;
813 if( r
.containerCount
< r
.resultCount
) {
814 fix
= 20; // give 20px for scrollbar
816 fix
= os_operaWidthFix( w
);
819 fix
= 4; // basic padding
823 // resize container to fit more data if permitted
824 var normW
= document
.getElementById( r
.searchbox
).offsetWidth
;
825 var prop
= maxW
/ normW
;
826 if( prop
> os_container_max_width
) {
827 prop
= os_container_max_width
;
828 } else if( prop
< 1 ) {
831 var newW
= Math
.round( normW
* prop
);
834 if( os_animation_timer
!= null ) {
835 clearInterval( os_animation_timer
.id
);
837 os_animation_timer
= new os_AnimationTimer( r
, w
);
838 os_animation_timer
.id
= setInterval( "os_animateChangeWidth()", os_animation_delay
);
839 w
-= fix
; // this much is reserved
846 for( var i
= 0; i
< r
.resultCount
; i
++ ) {
847 var e
= document
.getElementById( r
.resultText
+ i
);
849 var lastW
= e
.offsetWidth
+ 1;
851 var changedText
= false;
852 while( e
.offsetWidth
> w
&& ( e
.offsetWidth
< lastW
|| iteration
< 2 ) ) {
854 lastW
= e
.offsetWidth
;
856 e
.innerHTML
= l
.substring( 0, l
.length
- replace
) + '...';
858 replace
= 4; // how many chars to replace
861 // show hint for trimmed titles
862 document
.getElementById( r
.resultTable
+ i
).setAttribute( 'title', r
.results
[i
] );
867 /** Invoked on timer to animate change in container width */
868 window
.os_animateChangeWidth = function() {
869 var r
= os_animation_timer
.r
;
870 var c
= document
.getElementById( r
.container
);
871 var w
= c
.offsetWidth
;
872 var normW
= document
.getElementById( r
.searchbox
).offsetWidth
;
873 var normL
= os_getElementPosition( r
.searchbox
).left
;
874 var inc
= os_animation_timer
.inc
;
875 var target
= os_animation_timer
.target
;
877 if( ( inc
> 0 && nw
>= target
) || ( inc
<= 0 && nw
<= target
) ) {
879 c
.style
.width
= target
+ 'px';
880 clearInterval( os_animation_timer
.id
);
881 os_animation_timer
= null;
884 c
.style
.width
= nw
+ 'px';
885 if( document
.documentElement
.dir
== 'rtl' ) {
886 c
.style
.left
= ( normL
+ normW
+ ( target
- nw
) - os_animation_timer
.target
- 1 ) + 'px';
891 /** Change the highlighted row (i.e. suggestion), from position cur to next */
892 window
.os_changeHighlight = function( r
, cur
, next
, updateSearchBox
) {
893 if ( next
>= r
.resultCount
) {
894 next
= r
.resultCount
- 1;
901 return; // nothing to do.
905 var curRow
= document
.getElementById( r
.resultTable
+ cur
);
906 if( curRow
!= null ) {
907 curRow
.className
= 'os-suggest-result';
912 var nextRow
= document
.getElementById( r
.resultTable
+ next
);
913 if( nextRow
!= null ) {
914 nextRow
.className
= os_HighlightClass();
916 newText
= r
.results
[next
];
918 newText
= r
.original
;
921 // adjust the scrollbar if any
922 if( r
.containerCount
< r
.resultCount
) {
923 var c
= document
.getElementById( r
.container
);
924 var vStart
= c
.scrollTop
/ r
.containerRow
;
925 var vEnd
= vStart
+ r
.containerCount
;
926 if( next
< vStart
) {
927 c
.scrollTop
= next
* r
.containerRow
;
928 } else if( next
>= vEnd
) {
929 c
.scrollTop
= ( next
- r
.containerCount
+ 1 ) * r
.containerRow
;
933 // update the contents of the search box
934 if( updateSearchBox
) {
935 os_updateSearchQuery( r
, newText
);
939 window
.os_HighlightClass = function() {
940 var match
= navigator
.userAgent
.match(/AppleWebKit\/(\d+)/);
942 var webKitVersion
= parseInt( match
[1] );
943 if ( webKitVersion
< 523 ) {
944 // CSS system highlight colors broken on old Safari
945 // https://bugs.webkit.org/show_bug.cgi?id=6129
946 // Safari 3.0.4, 3.1 known ok
947 return 'os-suggest-result-hl-webkit';
950 return 'os-suggest-result-hl';
953 window
.os_updateSearchQuery = function( r
, newText
) {
954 document
.getElementById( r
.searchbox
).value
= newText
;
959 /********************
961 ********************/
963 /** Mouse over the container */
964 window
.os_eventMouseover = function( srcId
, e
) {
965 var targ
= os_getTarget( e
);
966 var r
= os_map
[srcId
];
967 if( r
== null || !os_mouse_moved
) {
968 return; // not our event
970 var num
= os_getNumberSuffix( targ
.id
);
972 os_changeHighlight( r
, r
.selected
, num
, false );
976 /* Get row where the event occured (from its id) */
977 window
.os_getNumberSuffix = function( id
) {
978 var num
= id
.substring( id
.length
- 2 );
979 if( !( num
.charAt( 0 ) >= '0' && num
.charAt( 0 ) <= '9' ) ) {
980 num
= num
.substring( 1 );
982 if( os_isNumber( num
) ) {
983 return parseInt( num
);
989 /** Save mouse move as last action */
990 window
.os_eventMousemove = function( srcId
, e
) {
991 os_mouse_moved
= true;
994 /** Mouse button held down, register possible click */
995 window
.os_eventMousedown = function( srcId
, e
) {
996 var targ
= os_getTarget( e
);
997 var r
= os_map
[srcId
];
999 return; // not our event
1001 var num
= os_getNumberSuffix( targ
.id
);
1003 os_mouse_pressed
= true;
1006 // os_updateSearchQuery( r, r.results[num] );
1008 // keep the focus on the search field
1009 document
.getElementById( r
.searchbox
).focus();
1011 return false; // prevents selection
1014 /** Mouse button released, check for click on some row */
1015 window
.os_eventMouseup = function( srcId
, e
) {
1016 var targ
= os_getTarget( e
);
1017 var r
= os_map
[srcId
];
1019 return; // not our event
1021 var num
= os_getNumberSuffix( targ
.id
);
1023 if( num
>= 0 && os_mouse_num
== num
) {
1024 os_updateSearchQuery( r
, r
.results
[num
] );
1025 os_hideResults( r
);
1026 document
.getElementById( r
.searchform
).submit();
1028 os_mouse_pressed
= false;
1029 // keep the focus on the search field
1030 document
.getElementById( r
.searchbox
).focus();
1033 /** Toggle stuff seems to be dead code? */
1035 /** Return the span element that contains the toggle link */
1036 window
.os_createToggle = function( r
, className
) {
1037 var t
= document
.createElement( 'span' );
1038 t
.className
= className
;
1039 t
.setAttribute( 'id', r
.toggle
);
1040 var link
= document
.createElement( 'a' );
1041 link
.setAttribute( 'href', 'javascript:void(0);' );
1042 link
.onclick = function() { os_toggle( r
.searchbox
, r
.searchform
); };
1043 var msg
= document
.createTextNode( wgMWSuggestMessages
[0] );
1044 link
.appendChild( msg
);
1045 t
.appendChild( link
);
1049 /** Call when user clicks on some of the toggle links */
1050 window
.os_toggle = function( inputId
, formName
) {
1051 r
= os_map
[inputId
];
1054 os_enableSuggestionsOn( inputId
, formName
);
1055 r
= os_map
[inputId
];
1056 msg
= wgMWSuggestMessages
[0];
1058 os_disableSuggestionsOn( inputId
, formName
);
1059 msg
= wgMWSuggestMessages
[1];
1062 var link
= document
.getElementById( r
.toggle
).firstChild
;
1063 link
.replaceChild( document
.createTextNode( msg
), link
.firstChild
);
1066 hookEvent( 'load', os_MWSuggestInit
);