* Room info popup now uses Scriptactulous appear and fade effects
[citadel.git] / webcit / static / wclib.js
1 //
2 // $Id: wclib.js,v 625.2 2005/09/18 04:04:32 ajc Exp $
3 //
4 // JavaScript function library for WebCit.
5 //
6 //
7
8
9 var browserType;
10 var room_is_trash = 0;
11
12 if (document.layers) {browserType = "nn4"}
13 if (document.all) {browserType = "ie"}
14 if (window.navigator.userAgent.toLowerCase().match("gecko")) {
15         browserType= "gecko"
16 }
17
18 var ns6=document.getElementById&&!document.all;
19
20
21
22 // We love string tokenizers.
23 function extract_token(source_string, token_num, delimiter) {
24         var i = 0;
25         var extracted_string = source_string;
26
27         if (token_num > 0) {
28                 for (i=0; i<token_num; ++i) {
29                         var j = extracted_string.indexOf(delimiter);
30                         if (j >= 0) {
31                                 extracted_string = extracted_string.substr(j+1);
32                         }
33                 }
34         }
35
36         j = extracted_string.indexOf(delimiter);
37         if (j >= 0) {
38                 extracted_string = extracted_string.substr(0, j);
39         }
40
41         return extracted_string;
42 }
43
44
45
46 // This code handles the popups for important-messages.
47 function hide_imsg_popup() {
48         if (browserType == "gecko" )
49                 document.poppedLayer = eval('document.getElementById(\'important_message\')');
50         else if (browserType == "ie")
51                 document.poppedLayer = eval('document.all[\'important_message\']');
52         else
53                 document.poppedLayer = eval('document.layers[\'`important_message\']');
54
55         document.poppedLayer.style.visibility = "hidden";
56 }
57
58
59 // This function activates the ajax-powered recipient autocompleters on the message entry screen.
60 function activate_entmsg_autocompleters() {
61         new Ajax.Autocompleter('cc_id', 'cc_name_choices', 'cc_autocomplete', {} );
62         new Ajax.Autocompleter('bcc_id', 'bcc_name_choices', 'bcc_autocomplete', {} );
63         new Ajax.Autocompleter('recp_id', 'recp_name_choices', 'recp_autocomplete', {} );
64 }
65
66
67
68 // Toggle the icon bar between menu/roomlist...
69 var which_div_expanded = null;
70 var num_drop_targets = 0;
71 var drop_targets_elements = new Array();
72 var drop_targets_roomnames = new Array();
73
74 function switch_to_room_list() {
75         $('iconbar').innerHTML = $('iconbar').innerHTML.substr(0, $('iconbar').innerHTML.indexOf('switch'));
76         CtdlLoadScreen('iconbar');
77         new Ajax.Updater('iconbar', 'iconbar_ajax_rooms', { method: 'get' } );
78 }
79
80 function expand_floor(floor_div) {
81         if (which_div_expanded != null) {
82                 if ($(which_div_expanded) != null) {
83                         $(which_div_expanded).style.display = 'none' ;
84                 }
85         }
86
87         // clicking on the already-expanded floor causes the whole list to collapse
88         if (which_div_expanded == floor_div) {
89                 which_div_expanded = null;
90
91                 // notify the server that no floors are expanded
92                 new Ajax.Request(
93                         'set_floordiv_expanded/-1', {
94                                 method: 'post'
95                         }
96                 );
97                 return true;
98         }
99
100         // expand the requested floor
101         $(floor_div).style.display = 'block';
102         which_div_expanded = floor_div;
103
104         // notify the server of which floor is expanded
105         new Ajax.Request(
106                 'set_floordiv_expanded/'+floor_div, {
107                         method: 'post'
108                 }
109         );
110 }
111
112 function switch_to_menu_buttons() {
113         which_div_expanded = null;
114         num_drop_targets = 0;
115         CtdlLoadScreen('iconbar');
116         new Ajax.Updater('iconbar', 'iconbar_ajax_menu', { method: 'get' } );
117 }
118
119
120 // Static variables for mailbox view...
121 //
122 var CtdlNumMsgsSelected = 0;
123 var CtdlMsgsSelected = new Array();
124 var CtdlLastMsgnumSelected = 0;
125
126 // This gets called when you single click on a message in the mailbox view.
127 // We know that the element id of the table row will be the letter 'm' plus the message number.
128 //
129 function CtdlSingleClickMsg(evt, msgnum) {
130
131         // Clear the preview pane until we load the new message
132         $('preview_pane').innerHTML = '';
133
134         // De-select any messages that were already selected, *unless* the Ctrl or
135         // Shift key is being pressed, in which case the user wants multi select
136         // or group select.
137         if ( (!evt.ctrlKey) && (!evt.shiftKey) ) {
138                 if (CtdlNumMsgsSelected > 0) {
139                         for (i=0; i<CtdlNumMsgsSelected; ++i) {
140                                 $('m'+CtdlMsgsSelected[i]).style.backgroundColor = '#fff';
141                                 $('m'+CtdlMsgsSelected[i]).style.color = '#000';
142                         }
143                         CtdlNumMsgsSelected = 0;
144                 }
145         }
146
147         // For multi select ... is the message being clicked already selected?
148         already_selected = 0;
149         if ( (evt.ctrlKey) && (CtdlNumMsgsSelected > 0) ) {
150                 for (i=0; i<CtdlNumMsgsSelected; ++i) {
151                         if (CtdlMsgsSelected[i] == msgnum) {
152                                 already_selected = 1;
153                                 already_selected_pos = i;
154                         }
155                 }
156         }
157
158         // Now select (or de-select) the message
159         if ( (evt.ctrlKey) && (already_selected == 1) ) {
160
161                 // Deselect: first un-highlight it...
162                 $('m'+msgnum).style.backgroundColor = '#fff';
163                 $('m'+msgnum).style.color = '#000';
164
165                 // Then remove it from the selected messages list.
166                 for (i=already_selected_pos; i<(CtdlNumMsgsSelected-1); ++i) {
167                         CtdlMsgsSelected[i] = CtdlMsgsSelected[i+1];
168                 }
169                 CtdlNumMsgsSelected = CtdlNumMsgsSelected - 1;
170                 
171         }
172
173         else if (evt.shiftKey) {
174                 
175                 // Group select: first clear everything out...
176                 if (CtdlNumMsgsSelected > 0) {
177                         for (i=0; i<CtdlNumMsgsSelected; ++i) {
178                                 $('m'+CtdlMsgsSelected[i]).style.backgroundColor = '#fff';
179                                 $('m'+CtdlMsgsSelected[i]).style.color = '#000';
180                         }
181                 }
182                 CtdlNumMsgsSelected = 0;
183
184                 // Then highlight and select the group.
185                 // Traverse the table looking for a row whose ID contains the desired msgnum
186
187                 var in_the_group = 0;
188                 var is_edge = 0;
189                 var table = $('summary_headers');
190                 if (table) {
191                         for (var r = 0; r < table.rows.length; r++) {
192                                 var thename = table.rows[r].id;
193                                 if ( (thename.substr(1) == msgnum) || (thename.substr(1) == CtdlLastMsgnumSelected) ) {
194                                         in_the_group = 1 - in_the_group;
195                                         is_edge = 1;
196                                 }
197                                 else {
198                                         is_edge = 0;
199                                 }
200                                 if ( (in_the_group == 1) || (is_edge == 1) ) {
201                                         // Highlight it...
202                                         table.rows[r].style.backgroundColor='#69aaff';
203                                         table.rows[r].style.color='#fff';
204
205                                         // And add it to the selected messages list.
206                                         CtdlNumMsgsSelected = CtdlNumMsgsSelected + 1;
207                                         CtdlMsgsSelected[CtdlNumMsgsSelected-1] = thename.substr(1);
208                                 }
209                         }
210                 }
211         }
212
213         else {
214                 // Select: first highlight it...
215                 $('m'+msgnum).style.backgroundColor='#69aaff';
216                 $('m'+msgnum).style.color='#fff';
217
218                 // Then add it to the selected messages list.
219                 CtdlNumMsgsSelected = CtdlNumMsgsSelected + 1;
220                 CtdlMsgsSelected[CtdlNumMsgsSelected-1] = msgnum;
221
222                 // Gradient
223                 CtdlLoadScreen('preview_pane');
224                 // Update the preview pane
225                 new Ajax.Updater('preview_pane', 'msg/'+msgnum, { method: 'get' } );
226         
227                 // Mark the message as read
228                 new Ajax.Request(
229                         'ajax_servcmd', {
230                                 method: 'post',
231                                 parameters: 'g_cmd=SEEN '+msgnum+'|1',
232                                 onComplete: CtdlRemoveTheUnseenBold(msgnum)
233                         }
234                 );
235         }
236         
237         // Save the selected position in case the user does a group select next time.
238         CtdlLastMsgnumSelected = msgnum;
239
240         return false;           // try to defeat the default click behavior
241 }
242
243 // Delete selected messages.
244 function CtdlDeleteSelectedMessages(evt) {
245         
246         if (CtdlNumMsgsSelected < 1) {
247                 // Nothing to delete, so exit silently.
248                 return false;
249         }
250         for (i=0; i<CtdlNumMsgsSelected; ++i) {
251                 if (parseInt(room_is_trash) > 0) {
252                         new Ajax.Request(
253                                 'ajax_servcmd', {
254                                         method: 'post',
255                                         parameters: 'g_cmd=DELE ' + CtdlMsgsSelected[i],
256                                         onComplete: CtdlClearDeletedMsg(CtdlMsgsSelected[i])
257                                 }
258                         );
259                 }
260                 else {
261                         new Ajax.Request(
262                                 'ajax_servcmd', {
263                                         method: 'post',
264                                         parameters: 'g_cmd=MOVE ' + CtdlMsgsSelected[i] + '|_TRASH_|0',
265                                         onComplete: CtdlClearDeletedMsg(CtdlMsgsSelected[i])
266                                 }
267                         );
268                 }
269         }
270         CtdlNumMsgsSelected = 0;
271
272         // Clear the preview pane too.
273         $('preview_pane').innerHTML = '';
274 }
275
276
277 // Move selected messages.
278 function CtdlMoveSelectedMessages(evt, target_roomname) {
279         
280         if (CtdlNumMsgsSelected < 1) {
281                 // Nothing to delete, so exit silently.
282                 return false;
283         }
284         for (i=0; i<CtdlNumMsgsSelected; ++i) {
285                 new Ajax.Request(
286                         'ajax_servcmd', {
287                                 method:'post',
288                                 parameters:'g_cmd=MOVE ' + CtdlMsgsSelected[i] + '|' + target_roomname + '|0',
289                                 onComplete:CtdlClearDeletedMsg(CtdlMsgsSelected[i])
290                         }
291                 );
292         }
293         CtdlNumMsgsSelected = 0;
294
295         // Clear the preview pane too.
296         $('preview_pane').innerHTML = '';
297 }
298
299
300
301 // This gets called when the user touches the keyboard after selecting messages...
302 function CtdlMsgListKeyPress(evt) {
303         if(document.all) {                              // aIEeee
304                 var whichKey = window.event.keyCode;
305         }
306         else {                                          // non-sux0r browsers
307                 var whichKey = evt.which;
308         }
309         if (whichKey == 46) {                           // DELETE key
310                 CtdlDeleteSelectedMessages(evt);
311         }
312         return true;
313 }
314
315 // Take the boldface away from a message to indicate that it has been seen.
316 function CtdlRemoveTheUnseenBold(msgnum) {
317         $('m'+msgnum).style.fontWeight='normal';
318 }
319
320 // A message has been deleted, so yank it from the list.
321 function CtdlClearDeletedMsg(msgnum) {
322
323
324         // Traverse the table looking for a row whose ID contains the desired msgnum
325         var table = $('summary_headers');
326         if (table) {
327                 for (var r = 0; r < table.rows.length; r++) {
328                         var thename = table.rows[r].id;
329                         if (thename.substr(1) == msgnum) {
330                                 try {
331                                         table.deleteRow(r);
332                                 }
333                                 catch(e) {
334                                         alert('error: browser failed to clear row ' + r);
335                                 }
336                         }
337                 }
338         }
339         else {                                          // if we can't delete it,
340                 new Effect.Squish('m'+msgnum);          // just hide it.
341         }
342
343
344 }
345
346 // These functions called when the user down-clicks on the message list resizer bar
347
348 var saved_x = 0;
349 var saved_y = 0;
350
351 function CtdlResizeMsgListMouseUp(evt) {
352         document.onmouseup = null;
353         document.onmousemove = null;
354         if (document.layers) {
355                 document.releaseEvents(Event.MOUSEUP | Event.MOUSEMOVE);
356         }
357         return true;
358 }
359
360 function CtdlResizeMsgListMouseMove(evt) {
361         y = (ns6 ? evt.clientY : event.clientY);
362         increment = y - saved_y;
363
364         // First move the bottom of the message list...
365         d = $('message_list');
366         if (d.offsetHeight){
367                 divHeight = d.offsetHeight;
368         }
369         else if (d.style.pixelHeight) {
370                 divHeight = d.style.pixelHeight;
371         }
372         d.style.height = (divHeight + increment) + 'px';
373
374         // Then move the top of the preview pane...
375         d = $('preview_pane');
376         if (d.offsetTop){
377                 divTop = d.offsetTop;
378         }
379         else if (d.style.pixelTop) {
380                 divTop = d.style.pixelTop;
381         }
382         d.style.top = (divTop + increment) + 'px';
383
384         // Resize the bottom of the preview pane...
385         d = $('preview_pane');
386         if (d.offsetHeight){
387                 divHeight = d.offsetHeight;
388         }
389         else if (d.style.pixelHeight) {
390                 divHeight = d.style.pixelHeight;
391         }
392         d.style.height = (divHeight - increment) + 'px';
393
394         // Then move the top of the slider bar.
395         d = $('resize_msglist');
396         if (d.offsetTop){
397                 divTop = d.offsetTop;
398         }
399         else if (d.style.pixelTop) {
400                 divTop = d.style.pixelTop;
401         }
402         d.style.top = (divTop + increment) + 'px';
403
404         saved_y = y;
405         return true;
406 }
407
408 function CtdlResizeMsgListMouseDown(evt) {
409         saved_y = (ns6 ? evt.clientY : event.clientY);
410         document.onmouseup = CtdlResizeMsgListMouseUp;
411         document.onmousemove = CtdlResizeMsgListMouseMove;
412         if (document.layers) {
413                 document.captureEvents(Event.MOUSEUP | Event.MOUSEMOVE);
414         }
415         return false;           // disable the default action
416 }
417
418
419
420 // These functions handle drag and drop message moving
421
422 var mm_div = null;
423
424 function CtdlMoveMsgMouseDown(evt, msgnum) {
425
426         // do the highlight first
427         CtdlSingleClickMsg(evt, msgnum);
428
429         // Now handle the possibility of dragging
430         saved_x = (ns6 ? evt.clientX : event.clientX);
431         saved_y = (ns6 ? evt.clientY : event.clientY);
432         document.onmouseup = CtdlMoveMsgMouseUp;
433         document.onmousemove = CtdlMoveMsgMouseMove;
434         if (document.layers) {
435                 document.captureEvents(Event.MOUSEUP | Event.MOUSEMOVE);
436         }
437
438         return false;
439 }
440
441 function CtdlMoveMsgMouseMove(evt) {
442         x = (ns6 ? evt.clientX : event.clientX);
443         y = (ns6 ? evt.clientY : event.clientY);
444
445         if ( (x == saved_x) && (y == saved_y) ) {
446                 return true;
447         }
448
449         if (CtdlNumMsgsSelected < 1) { 
450                 return true;
451         }
452
453         if (!mm_div) {
454
455
456                 drag_o_text = "<div style=\"overflow:none; background-color:#fff; color:#000; border: 1px solid black; filter:alpha(opacity=75); -moz-opacity:.75; opacity:.75;\"><tr><td>";
457                 for (i=0; i<CtdlNumMsgsSelected; ++i) {
458                         drag_o_text = drag_o_text + 
459                                 ctdl_ts_getInnerText(
460                                         $('m'+CtdlMsgsSelected[i]).cells[0]
461                                 ) + '<br>';
462                 }
463                 drag_o_text = drag_o_text + "<div>";
464
465                 mm_div = document.createElement("DIV");
466                 mm_div.style.position='absolute';
467                 mm_div.style.top = y + 'px';
468                 mm_div.style.left = x + 'px';
469                 mm_div.style.pixelHeight = '300';
470                 mm_div.style.pixelWidth = '300';
471                 mm_div.innerHTML = drag_o_text;
472                 document.body.appendChild(mm_div);
473         }
474         else {
475                 mm_div.style.top = y + 'px';
476                 mm_div.style.left = x + 'px';
477         }
478
479         return false;   // prevent the default mouse action from happening?
480 }
481
482 function CtdlMoveMsgMouseUp(evt) {
483         document.onmouseup = null;
484         document.onmousemove = null;
485         if (document.layers) {
486                 document.releaseEvents(Event.MOUSEUP | Event.MOUSEMOVE);
487         }
488
489         if (mm_div) {
490                 document.body.removeChild(mm_div);      
491                 mm_div = null;
492         }
493
494         if (num_drop_targets < 1) {     // nowhere to drop
495                 return true;
496         }
497
498         // Did we release the mouse button while hovering over a drop target?
499         // NOTE: this only works cross-browser because the iconbar div is always
500         //      positioned at 0,0.  Browsers differ in whether the 'offset'
501         //      functions return pos relative to the document or parent.
502
503         for (i=0; i<num_drop_targets; ++i) {
504
505                 x = (ns6 ? evt.clientX : event.clientX);
506                 y = (ns6 ? evt.clientY : event.clientY);
507
508                 l = parseInt(drop_targets_elements[i].offsetLeft);
509                 t = parseInt(drop_targets_elements[i].offsetTop);
510                 r = parseInt(drop_targets_elements[i].offsetLeft)
511                   + parseInt(drop_targets_elements[i].offsetWidth);
512                 b = parseInt(drop_targets_elements[i].offsetTop)
513                   + parseInt(drop_targets_elements[i].offsetHeight);
514
515                 /* alert('Offsets are: ' + l + ' ' + t + ' ' + r + ' ' + b + '.'); */
516         
517                 if ( (x >= l) && (x <= r) && (y >= t) && (y <= b) ) {
518                         // Yes, we dropped it on a hotspot.
519                         CtdlMoveSelectedMessages(evt, drop_targets_roomnames[i]);
520                         return true;
521                 }
522         }
523
524         return true;
525 }
526
527
528 function ctdl_ts_getInnerText(el) {
529         if (typeof el == "string") return el;
530         if (typeof el == "undefined") { return el };
531         if (el.innerText) return el.innerText;  //Not needed but it is faster
532         var str = "";
533         
534         var cs = el.childNodes;
535         var l = cs.length;
536         for (var i = 0; i < l; i++) {
537                 switch (cs[i].nodeType) {
538                         case 1: //ELEMENT_NODE
539                                 str += ts_getInnerText(cs[i]);
540                                 break;
541                         case 3: //TEXT_NODE
542                                 str += cs[i].nodeValue;
543                                 break;
544                 }
545         }
546         return str;
547 }
548
549
550
551 // This function handles the creation of new notes in the "Notes" view.
552 //
553 function add_new_note() {
554
555         new_eid = Math.random() + '';
556         new_eid = new_eid.substr(3);
557
558         $('new_notes_here').innerHTML = $('new_notes_here').innerHTML
559                 + '<IMG ALIGN=MIDDLE src=\"static/storenotes_48x.gif\">'
560                 + '<span id=\"note' + new_eid + '\">' + Date() + '</span><br />'
561         ;
562
563         new Ajax.InPlaceEditor('note' + new_eid,
564                 'updatenote?eid=' + new_eid , {rows:5,cols:72});
565 }
566
567 function CtdlShowRaw(msgnum) {
568 var customnav = document.createElement("span");
569 var mode_citadel = document.createElement("a");
570 mode_citadel.appendChild(document.createTextNode("Citadel Source"));
571 var mode_rfc822 = document.createElement("a");
572 mode_rfc822.appendChild(document.createTextNode(" RFC822 Source"));
573 mode_citadel.setAttribute("href","#");
574 mode_rfc822.setAttribute("href","#");
575 mode_rfc822.setAttribute("onclick","rawSwitch822('" + msgnum + "');");
576 mode_citadel.setAttribute("onclick","rawSwitchCitadel('" + msgnum + "');");
577 customnav.appendChild(mode_citadel);
578 customnav.appendChild(mode_rfc822);
579 customnav.setAttribute("class","floatcustomnav");
580 floatwindow("headerscreen","pre",customnav);
581 rawSwitch822(msgnum);
582 }
583
584 function rawSwitch822(msgnum) {
585 CtdlLoadScreen("headerscreen");
586 new Ajax.Updater("headerscreen", 
587 'ajax_servcmd_esc',
588  { method: 'post',parameters: 'g_cmd=MSG2 ' +msgnum  } );
589
590 }
591
592 function rawSwitchCitadel(msgnum) {
593 CtdlLoadScreen("headerscreen");
594 new Ajax.Updater("headerscreen", 
595 'ajax_servcmd_esc',
596  { method: 'post',parameters: 'g_cmd=MSG0 ' +msgnum  } );
597
598 }
599
600 function floatwindow(newdivid,contentelementtype,customnav) {
601 var windiv = document.createElement("div");
602 windiv.setAttribute("class","floatwindow");
603 var winid = newdivid+"_window";
604 windiv.setAttribute("id",winid);
605 var nav = document.createElement("div");
606 if (customnav != null) {
607 nav.appendChild(customnav);
608 }
609 var minimizeA = document.createElement("a");
610 var minimizeButton = document.createTextNode("Close");
611 minimizeA.appendChild(minimizeButton);
612 minimizeA.setAttribute("onclick","killFloatWindow(this);");
613 minimizeA.setAttribute("href","#");
614 nav.appendChild(minimizeA);
615 nav.setAttribute("class","floatnav");
616 windiv.appendChild(nav);
617 var contentarea = document.createElement("pre");
618 contentarea.setAttribute("class","floatcontent");
619 contentarea.setAttribute("id",newdivid);
620 windiv.appendChild(contentarea);
621 document.body.appendChild(windiv);
622 }
623 function killFloatWindow(caller) {
624 var span = caller.parentNode;
625 var fwindow = span.parentNode;
626 fwindow.parentNode.removeChild(fwindow);
627 }
628 // Place a gradient loadscreen on an element, e.g to use before Ajax.updater
629 function CtdlLoadScreen(elementid) {
630 var elem = document.getElementById(elementid);
631 elem.innerHTML = "<div align=center><br><table border=0 cellpadding=10 bgcolor=\"#ffffff\"><tr><td><img src=\"static/throbber.gif\" /><font color=\"#AAAAAA\">&nbsp;&nbsp;Loading....</font></td></tr></table><br /></div>";
632 }
633
634
635 // Show info for a user, basically replaces showuser()
636 // matt@comalies is to blame for this poorly coded masterpiece. 
637 function CtdlShowUserInfoPopup(Element) {
638         try {
639                 // hopefully no one needs to use the class attribute... could be better done 
640                 // with xmlns though..
641                 var user = Element.getAttribute("class");
642                 var updname = "biospace_"+user;
643                 if (document.getElementById(updname) == null) {
644                         // insert a space for the bio
645                         var pNode = Element.parentNode;
646                         var newdiv = document.createElement("div");
647                         newdiv.id = updname;
648                         newdiv.innerHTML = "Getting user info....";
649                         pNode.appendChild(newdiv);
650                         CtdlLoadScreen(updname);
651                         new Ajax.Updater(updname, 'showuser_ajax?who='+user, { method: 'get' } );
652                 }
653         }
654         catch(err) {
655                 return true;
656         }
657         return false;
658 }
659
660
661 // Pop open the address book
662 function PopOpenAddressBook() {
663         $('address_book_inner_div').innerHTML = "<div align=center><br><table border=0 cellpadding=10 bgcolor=\"#ffffff\"><tr><td><img src=\"static/throbber.gif\" /><font color=\"#AAAAAA\">&nbsp;&nbsp;Loading....</font></td></tr></table><br /></div>";
664         Effect.Appear('address_book_popup', { duration: 0.5 } );
665         new Ajax.Updater(
666                 'address_book_inner_div',
667                 'display_address_book_inner_div',
668                 { method: 'get', parameters: Math.random() }
669         );
670 }