mwsuggest.js 25 KB

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