mwsuggest.js 31 KB

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