bug 1389: Consistent indentation
[cacert-devel.git] / www / ac.js
1 /* vim:ts=4:sts=4:sw=2:noai:noexpandtab
2 *
3 * Auto-complete client side javascript.
4 * Copyright (c) 2005 Steven McCoy <fnjordy@gmail.com>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21 /* format of constructor is overloaded:
22 * AC(<type>, <id>, <submit callback>)
23 * AC(<type>, <id>)
24 * AC(<id>)
25 */
26 function AC(id) {
27 /* find search type */
28 if (arguments.length > 1) {
29 this.type = arguments[0];
30 id = arguments[1];
31 } else {
32 this.type = id;
33 }
34
35 /* input element we are autocompleting on */
36 this.obj = document.getElementById(id);
37 this.obj.value = '';
38
39 /* base url to send request too */
40 this.url = '/ac.php';
41
42 /* function to call when option selected */
43 this.submit_callback = (arguments.length > 2) ? arguments[2] : null;
44
45 /* popup layer we will display results in */
46 this.div = document.createElement('DIV');
47 this.div.className = 'ac_menu';
48 this.div.style.visibility = 'hidden';
49 this.div.style.position = 'absolute';
50 this.div.style.zIndex = 1;
51 this.div.style.width = this.obj.offsetWidth - 2 + "px";
52
53 this.div.style.left = this.total_offset(this.obj,'offsetLeft') + "px";
54 this.div.style.top = this.total_offset(this.obj,'offsetTop') + this.obj.offsetHeight - 1 + "px";
55
56 /* tie to input element */
57 this.obj.parentNode.insertBefore(this.div, this.obj.nextSibling);
58
59 /* iframe for non-XmlHttpRequest() browsers */
60 this.iframe = null;
61
62 /* install event handlers */
63 this.obj.onkeydown = this.onkeydown;
64 this.obj.onkeyup = this.onkeyup;
65 this.obj.onkeypress = this.onkeypress;
66 this.obj.onblur = function() { this.AC.close_popup(); }
67
68 this.obj.AC = this; /* self reference */
69 this.selected_option = null; /* the currently selected option */
70
71 this.request = null; /* http request object */
72 this.cache = new Array(); /* cache of results from server */
73 this.typing = false; /* whether user is still typing */
74 this.typing_timeout = 10;
75 this.sending_timeout = 10;
76
77 this.search_term = null; /* current search term */
78 this.previous_term = null; /* previous search term */
79 this.searched_term = null; /* search from keyboard */
80
81 this.last_input = null; /* previous typed entry */
82
83 /* Unicode inputs require polling of the input control for updates */
84 this.poll_input = false;
85
86 /* update extern mapping array for rpc reply */
87 _ac_map_add(this);
88 }
89
90 AC.prototype.enable_unicode = function() {
91 this.poll_input = true;
92 _ac_key_check(this,this.typing_timeout);
93 }
94
95 AC.prototype.total_offset = function(element, property) {
96 var total = 0;
97 while (element) {
98 total += element[property];
99 element = element.offsetParent;
100 }
101 return total;
102 }
103
104 /* hide popup and cleanup */
105 AC.prototype.close_popup = function() {
106 this.div.style.visibility = 'hidden';
107
108 /* no selected item, no typing, and close any pending request */
109 this.selected_option = null;
110 this.typing = false;
111 this.search_term = null;
112 this.previous_term = null;
113 }
114
115 /* create object for rpc call */
116 AC.prototype.XMLHttpRequest = function() {
117 var request = null;
118 if (typeof XMLHttpRequest != 'undefined') {
119 request = new XMLHttpRequest();
120 } else {
121 try {
122 request = new ActiveXObject('Msxml2.XMLHTTP')
123 } catch(e) {
124 try {
125 request = new ActiveXObject('Microsoft.XMLHTTP')
126 } catch(e) {
127 request = null
128 }
129 }
130 }
131 return request;
132 }
133
134 /* helper functions to process typing timer */
135 var _ac_key_thunk = new Array();
136 function _ac_key_thunk_call(id) {
137 if (_ac_key_thunk[id]) {
138 var ac = _ac_key_thunk[id][1];
139
140 /* now check as if onkeyup() was called */
141 /* first find unselected text */
142 var unselected = ac.obj.value;
143 if (document.selection) {
144 var range = document.selection.createRange();
145 if (range) {
146 /* to limit the execution this would be nice, but parentElement() not supported in Opera */
147 // if (range && range.parentElement && range.parentElement() == ac.obj) {
148 var length = unselected.length - range.text.length;
149 if (length > 0) {
150 unselected = unselected.substring(0, length);
151 }
152 }
153 } else if (ac.obj.setSelectionRange) {
154 var length = ac.obj.selectionEnd - ac.obj.selectionStart;
155 if (length > 0)
156 unselected = unselected.substring(0,ac.obj.selectionStart);
157 }
158
159 if (unselected != ac.last_input) {
160 if (unselected.length > 0) {
161 ac.searched_term = unselected;
162 ac.suggest(ac.searched_term);
163 } else {
164 _ac_cancel(ac);
165 ac.close_popup();
166 }
167 ac.last_input = unselected;
168 }
169
170 /* re-install timer for polling */
171 if (ac.poll_input) {
172 _ac_key_thunk[id][2] = setTimeout("_ac_key_thunk_call("+id+")",ac.typing_timeout);
173 } else {
174 /* remove from list and cleanup list */
175 _ac_key_thunk[id] = null;
176 for (i = _ac_key_thunk.length; i > 0; i--)
177 if (_ac_key_thunk[i] == null)
178 _ac_key_thunk.length--;
179 }
180 }
181 }
182
183 function _ac_key_check(ac,timeout) {
184 /* first remove any pending key check */
185 for (i = _ac_key_thunk.length-1; i >= 0; i--)
186 if (_ac_key_thunk[i] != null && _ac_key_thunk[i][0] == ac.obj.id) {
187 clearTimeout(_ac_key_thunk[i][2]);
188 _ac_key_thunk[i] = null;
189 }
190
191 /* now setup a new one */
192 var i = _ac_key_thunk.length;
193 var handle = setTimeout("_ac_key_thunk_call("+i+")",timeout);
194 _ac_key_thunk[i] = new Array(ac.obj.id,ac,handle);
195 }
196
197 /* helper functions to process sending timer */
198 var _ac_thunk = new Array();
199 function _ac_thunk_call(id) {
200 if (_ac_thunk[id]) {
201 var ac = _ac_thunk[id][1];
202 ac.typing = false;
203 ac.send(_ac_thunk[id][2]);
204 _ac_thunk[id] = null;
205 for (i = _ac_thunk.length; i > 0; i--)
206 if (_ac_thunk[i] == null)
207 _ac_thunk.length--;
208 }
209 }
210
211 /* cancel a pending request */
212 function _ac_cancel(ac) {
213 for (i = _ac_thunk.length-1; i >= 0; i--)
214 if (_ac_thunk[i] != null && _ac_thunk[i][0] == ac.obj.id) {
215 clearTimeout(_ac_thunk[i][3]);
216 _ac_thunk[i] = null;
217 }
218 }
219
220 function _ac_add(ac,query,timeout) {
221 var i = _ac_thunk.length;
222 var handle = setTimeout("_ac_thunk_call("+i+")",timeout);
223 _ac_thunk[i] = new Array(ac.obj.id,ac,query,handle);
224 }
225
226 /* helper functions for webserver rpc processing */
227 var _ac_map = new Array();
228 function _ac_map_add(ac) {
229 _ac_map[ac.obj.id] = ac;
230 }
231
232 /* called to initiation suggestion process */
233 AC.prototype.suggest = function(query) {
234 /* remove redundant searches */
235 if (query == this.search_term)
236 return;
237
238 /* cancel any existing http call */
239 _ac_cancel(this);
240 if (this.request && this.request.readyState != 0) {
241 this.request.abort();
242 }
243
244 /* check cache */
245 var lc = query.toLowerCase();
246 for (i = 0; i < this.cache.length; i++)
247 if (this.cache[i][0] == lc) {
248 var results = this.cache[i][1];
249 this.search_term = query;
250 this.update_popup(results);
251 return;
252 }
253
254 /* send call to server */
255 this.typing = true;
256 this.send(query);
257 }
258
259 /* called to send message to a server */
260 AC.prototype.send = function(query) {
261 /* check throttle timer */
262 if (this.typing) {
263 _ac_add(this,query,this.sending_timeout);
264 return;
265 }
266
267 /* initiate new call */
268 this.search_term = query;
269 if (this.iframe == null) {
270 this.request = this.XMLHttpRequest();
271 if (this.request == null) {
272 var iframe = document.createElement('IFRAME');
273 iframe.src = this.url+'?i=1&id='+encodeURI(this.obj.id)+'&t='+encodeURI(this.type)+'&s='+encodeURI(query);
274 /* opera 7.54 doesn't like iframe styles */
275 iframe.style.width = '0px';
276 iframe.style.height = '0px';
277 this.iframe = this.obj.appendChild(iframe);
278 this.obj.focus();
279 } else {
280 /* send XmlHttpRequest */
281 var AC = this;
282 this.request.onreadystatechange = function() {
283 if (AC.request.readyState == 4) {
284 try {
285 if (AC.request.status != 200 || AC.request.responseText.charAt(0) == '<') {
286 /* some error */
287 } else {
288 eval(AC.request.responseText);
289 }
290 } catch (e) {}
291 }
292 }
293 this.request.open("GET", this.url+"?id="+encodeURI(this.obj.id)+"&t="+encodeURI(this.type)+"&s="+encodeURI(query));
294 this.request.send(null);
295 }
296 } else {
297 /* re-submit iframe */
298 this.iframe.src = this.url+'?i=1&id='+encodeURI(this.obj.id)+'&t='+encodeURI(this.type)+'&s='+encodeURI(query);
299 this.obj.focus();
300 }
301 }
302
303 /* called with array of search results */
304 AC.prototype.update_popup = function(results) {
305 if (this.search_term != null && results != null && results.length > 0) {
306 /* remove currently listed options */
307 while (this.div.hasChildNodes())
308 this.div.removeChild(this.div.firstChild);
309
310 /* default to first result when adding characters */
311 if (this.previous_term == null || this.search_term.length >= this.previous_term.length) {
312 this.selected_option = 0;
313 } else {
314 /* remove selection when deleteing */
315 this.selected_option = null;
316 }
317 this.previous_term = this.search_term;
318
319 for (i = 0; i < results.length; i++) {
320 var div = document.createElement('DIV');
321 div.divid = results[i][2];
322 div.AC = this;
323 if (this.selected_option == div.divid)
324 div.className = 'ac_highlight';
325 else
326 div.className = 'ac_normal';
327 div.name = results[i][0];
328 div.value = results[i][1];
329 div.innerHTML = results[i][3];
330 div.onmousedown = function() { this.AC.onselected(); }
331 div.onmouseover = function() {
332 if (this.AC.selected_option != null)
333 this.AC.div.childNodes[this.AC.selected_option].className = 'ac_normal';
334 this.AC.selected_option = this.divid;
335 this.AC.cabbage = this.AC.selected_option;
336 this.className = 'ac_highlight';
337 }
338 div.onmouseout = function() { this.className = 'ac_normal'; }
339 this.div.appendChild(div);
340 }
341 this.div.style.visibility = 'visible';
342
343 /* complete text box with selected text */
344
345 if (this.selected_option == 0 &&
346 (this.obj.createTextRange || this.obj.setSelectionRange) &&
347 this.obj.value != results[0][1] &&
348 results[0][1].substring(0,this.search_term.length).toLowerCase() == this.search_term.toLowerCase())
349 {
350 this.obj.value = results[0][1];
351 if (this.obj.createTextRange) {
352 var range = this.obj.createTextRange();
353 range.moveStart('character',this.search_term.length);
354 range.select();
355 } else {
356 // var range = document.createRange();
357 // range.setStart(this.obj,this.search_term.length);
358 this.obj.setSelectionRange(this.search_term.length,this.obj.value.length);
359 }
360 }
361 } else {
362 this.close_popup();
363 }
364
365 /* update cache */
366 var found = false;
367 var lc = this.search_term.toLowerCase();
368 for (i = 0; i < this.cache.length; i++)
369 if (this.cache[i][0] == lc) {
370 found = true;
371 break;
372 }
373
374 if (!found) {
375 this.cache[this.cache.length] = new Array(lc, results);
376 }
377 }
378
379 /* update auto-compete input element with selected option */
380 AC.prototype.update_input = function() {
381 this.obj.value = this.div.childNodes[this.selected_option].name;
382 }
383
384 /* when option is clicked with mouse, or entered with keyboard */
385 AC.prototype.onselected = function() {
386 if (this.selected_option == null)
387 if (this.cabbage == null)
388 return;
389 else
390 this.selected_option = this.cabbage; /* opera funky */
391
392 this.update_input();
393
394 /* hide popup */
395 this.close_popup();
396 /* submit form */
397 if (this.submit_callback)
398 this.submit_callback();
399 }
400
401 /* capture up & down actions to prevent moving cursor left or right */
402 /* input.onkeypress() */
403 AC.prototype.onkeypress = function(e) {
404 if (!e) e = window.event;
405 var c = e.keyCode;
406 if (c == 0) c = e.charCode;
407 if(e.charCode) {_ac_key_check(this.AC,this.AC.typing_timeout); return;}
408 switch (c) {
409 case 38: /* up */
410 case 40: /* down */
411 e.returnValue = false;
412 if (e.preventDefault) e.preventDefault();
413 break;
414
415 default: break;
416 }
417 }
418
419 /* move cursor on down to allow repeating */
420 /* input.onkeydown() */
421 AC.prototype.onkeydown = function(e) {
422 if (!e) e = window.event;
423 var c = e.keyCode;
424 if (c == 0) c = e.charCode;
425 var i = this.AC.selected_option == null ? -1 : this.AC.selected_option;
426 if(e.charCode) {_ac_key_check(this.AC,this.AC.typing_timeout); return;}
427 switch (c) {
428 case 38: /* up */
429 i--;
430 e.returnValue = false;
431 if (e.preventDefault) e.preventDefault();
432 break;
433
434 case 40: /* down */
435 i++;
436 e.returnValue = false;
437 if (e.preventDefault) e.preventDefault();
438 break;
439
440 default:
441 _ac_key_check(this.AC,this.AC.typing_timeout);
442 break;
443 }
444
445 if (c == 38 || c == 40) {
446 var length = this.AC.div.childNodes.length;
447 if (i < 0) i = 0;
448 if (i >= length) i = length-1;
449 if (i != this.AC.selected_option) {
450 for (j = 0; j < length; j++) {
451 if (j == i) {
452 this.AC.obj.value = this.AC.div.childNodes[j].value;
453 this.AC.selected_option = this.AC.div.childNodes[j].divid;
454 this.AC.div.childNodes[j].className = 'ac_highlight';
455 } else {
456 this.AC.div.childNodes[j].className = 'ac_normal';
457 }
458 }
459
460 /* update search term */
461 this.AC.search_term = this.AC.div.childNodes[this.AC.selected_option].value;
462
463 /* popup if hidden */
464 if (this.AC.div.style.visibility == 'hidden') {
465 this.AC.suggest (this.AC.searched_term);
466 }
467 }
468 }
469 }
470
471 /* input.onkeyup() */
472 AC.prototype.onkeyup = function(e) {
473 if (!e) e = window.event;
474 var c = e.keyCode;
475 if (c == 0) c = e.charCode;
476 switch (c) {
477 /* prevent strange selections at top of option list */
478 case 38: /* up */
479 case 40: /* down */
480 e.returnValue = false;
481 if (e.preventDefault) e.preventDefault();
482 break;
483
484 /* select highlighted option */
485 case 13: /* enter */
486 this.AC.onselected();
487 e.returnValue = false;
488 if (e.preventDefault) e.preventDefault();
489 break;
490
491 /* hide popup window */
492 case 27: /* escape */
493 this.AC.close_popup();
494 e.returnValue = false;
495 if (e.preventDefault) e.preventDefault();
496 break;
497
498 /* get new suggestion for new data */
499 default:
500
501 /* for latin this is ok:
502 if (this.value.length > 0) {
503 this.AC.searched_term = this.value;
504 this.AC.suggest(this.value);
505 } else {
506 _ac_cancel(this.AC);
507 this.AC.close_popup();
508 }
509 */
510 break;
511 }
512 }
513
514 /* iframe or XmlHttpRequest() callback */
515 function _ac_rpc() {
516 var id = arguments[0];
517 if (_ac_map[id]) {
518 /* we cannot shift() arguments as it is an object :( */
519 _ac_map[id].process_reply.apply(_ac_map[id],arguments);
520 }
521 }
522
523 /* parse rpc results into html for the popup */
524 AC.prototype.process_reply = function() {
525 var results = new Array();
526 var c = 0;
527 var re = new RegExp('('+this.searched_term+')', "gi");
528 var nt = '<font color="red"><b>$1</b></font>';
529 for (i = 1; i < arguments.length; i += 2) {
530 var name = this.highlight ? arguments[i+1].replace(re, nt) : arguments[i+1];
531 var value = this.highlight ? arguments[i].replace(re, nt) : arguments[i];
532 var html = "<span class='d'>"+name+"</span><span class='a'>"+value+"</span>";
533 results[c] = new Array(arguments[i+1], arguments[i], c, html);
534 c++;
535 }
536
537 this.update_popup(results);
538 }
539
540 function escapeURI(La){
541 if(encodeURIComponent) {
542 return encodeURIComponent(La);
543 }
544 if(escape) {
545 return escape(La)
546 }
547 }