* Fixed a multiselect bug in the mailbox view. Ctrl-click was selecting a message...
[citadel.git] / webcit / static / summaryview.js
1 /**
2  * Webcit Summary View v2
3  *   All comments, flowers and death threats to Mathew McBride
4  *   <matt@mcbridematt.dhs.org> / <matt@comalies>
5  * Copyright 2009 The Citadel Team
6  * Licensed under the GPL V3
7  */
8 document.observe("dom:loaded", createMessageView);
9
10 var message_view = null;
11 var loadingMsg = null;
12 var rowArray = null;
13 var currentSortMode = null;
14
15 // Header elements
16 var mlh_date = null;
17 var mlh_subject = null;
18 var mlh_from = null;
19 var currentSorterToggle = null;
20 var query = "";
21 var currentlyMarkedRows = new Object();
22 var markedRowId = null;
23
24 var mouseDownEvent = null;
25 var exitedMouseDown = false;
26
27 var currentPage = 0;
28 var sortModes = {
29   "rdate" : sortRowsByDateDescending,
30   "date" : sortRowsByDateAscending,
31   //  "reverse" : sortRowsByDateDescending,
32   "subj" : sortRowsBySubjectAscending,
33   "rsubj" : sortRowsBySubjectDescending,
34   "sender": sortRowsByFromAscending,
35   "rsender" : sortRowsByFromDescending
36 };
37 var toggles = {};
38
39 var nummsgs = 0;
40 var startmsg = 0;
41 function createMessageView() {
42   message_view = document.getElementById("message_list_body");
43   loadingMsg = document.getElementById("loading");
44   getMessages();
45   mlh_date = $("mlh_date");
46   mlh_subject = $('mlh_subject');
47   mlh_from = $('mlh_from');
48   toggles["rdate"] = mlh_date;
49   toggles["date"] = mlh_date;
50   // toggles["reverse"] = mlh_date;
51   toggles["subj"] = mlh_subject;
52   toggles["rsubj"] = mlh_subject;
53   toggles["sender"] = mlh_from;
54   toggles["rsender"] = mlh_from;
55   mlh_date.observe('click',ApplySort);
56   mlh_subject.observe('click',ApplySort);
57   mlh_from.observe('click',ApplySort);
58   $(document).observe('keyup',CtdlMessageListKeyUp,false);
59   //window.oncontextmenu = function() { return false; };  
60   $('resize_msglist').observe('mousedown', CtdlResizeMouseDown);
61   $('m_refresh').observe('click', getMessages);
62   document.getElementById('m_refresh').setAttribute("href","#");
63   Event.observe(document.onresize ? document : window, "resize", normalizeHeaderTable);
64   Event.observe(document.onresize ? document : window, "resize", sizePreviewPane);
65   $('summpage').observe('change', getPage);
66   takeOverSearchOMatic();
67   setupDragDrop(); // here for now
68 }
69 function getMessages() {
70   if (loadingMsg.parentNode == null) {
71     message_view.innerHTML = "";
72     message_view.appendChild(loadingMsg);
73   }
74 roomName = getTextContent(document.getElementById("rmname"));
75  var parameters = {'room':roomName, 'startmsg': startmsg, 'stopmsg': -1};
76  if (is_safe_mode) {
77    parameters['stopmsg'] = parseInt(startmsg)+500;
78    //parameters['maxmsgs'] = 500;
79    if (currentSortMode != null) {
80      var SortBy = currentSortMode[0];
81      if (SortBy.charAt(0) == 'r') {
82        SortBy = SortBy.substr(1);
83        parameters["SortOrder"] = "0";
84      }
85      parameters["SortBy"] = SortBy;
86    }
87  } 
88  if (query.length > 0) {
89    parameters["query"] = query;
90  }
91 new Ajax.Request("roommsgs", {
92     method: 'get',
93         onSuccess: loadMessages,
94         parameters: parameters,
95         sanitize: false,
96       evalJSON: false,
97       onFailure: function(e) { alert("Failure: " + e);}
98         });
99 }
100 function loadMessages(transport) {
101   try {
102   var data = eval('('+transport.responseText+')');
103   if (!!data && transport.responseText.length < 2) {
104     alert("Message loading failed");
105   } 
106   nummsgs = data['nummsgs'];
107   var msgs = data['msgs'];
108   var length = msgs.length;
109   rowArray = new Array(); // store so they can be sorted
110   WCLog("Row array length: "+rowArray.length);
111   var start = new Date();
112   for(var i=0; i<length;i++) {
113     var trElement = document.createElement("tr");
114     var data = msgs[i];
115     var msgId = data[0];
116     var rowId = "msg_" + msgId;
117     trElement.setAttribute("id",rowId);
118     //$(trElement).observe('click', CtdlMessageListClick);
119     trElement.ctdlMsgId = msgId;
120     for(var j=1; j<5;j++) { // 1=msgId (hidden), 4 date timestamp (hidden) 6 = isNew etc. 
121       var content = data[j];
122       if(content.length < 1) {
123         content = "(blank)";
124       }
125       if (j==3) {
126         trElement.ctdlDate = content;
127       } else { 
128         try {
129       var tdElement = document.createElement("td");
130       trElement.appendChild(tdElement);
131       var txtContent = document.createTextNode(content);
132       tdElement.appendChild(txtContent);
133       var x=j;
134       if (x==4) x=3;
135       var classStmt = "col"+x;
136       //tdElement.setAttribute("class", classStmt);
137       tdElement.className = classStmt;
138         } catch (e) {
139           WCLog("Error on #"+msgId +" col"+j+":"+e);
140         }
141       }
142     }
143     if (data[5]) {
144       trElement.ctdlNewMsg = true;
145     }
146     trElement.dropEnabled = true;
147     trElement.ctdlMarked = false;
148     rowArray[i] = trElement; 
149   } 
150   var end = new Date();
151   var delta = end.getTime() - start.getTime();
152     WCLog("loadMessages construct: " + delta);
153   } catch (e) {
154     //window.alert(e+"|"+e.description);
155   }
156   if (currentSortMode == null) {
157   if (sortmode.length < 1) {
158     sortmode = "rdate";
159   }
160   currentSortMode = [sortmode, sortModes[sortmode]];
161   currentSorterToggle = toggles[sortmode];
162   }
163   if (!is_safe_mode) {
164   resortAndDisplay(currentSortMode[1]);
165   } else {
166     setupPageSelector();
167     resortAndDisplay(null);
168   }
169   if (loadingMsg.parentNode != null) {
170     loadingMsg.parentNode.removeChild(loadingMsg);
171   }
172   sizePreviewPane();
173 }
174 function resortAndDisplay(sortMode) {
175   WCLog("Begin resortAndDisplay");
176   var start = new Date();
177   /* We used to try and clear out the message_view element,
178      but stupid IE doesn't even do that properly */
179   var message_view_parent = message_view.parentNode;
180   message_view_parent.removeChild(message_view);
181   message_view = document.createElement("tbody");
182   message_view.setAttribute("id","message_list_body");
183   message_view.className="mailbox_summary";
184   message_view_parent.appendChild(message_view);
185   
186   var fragment = document.createDocumentFragment();
187   if (sortMode != null) {
188     rowArray.sort(sortMode);
189   }
190   var length = rowArray.length;
191   for(var x=0; x<length; ++x) {
192     try {
193       var currentRow = rowArray[x];
194       currentRow.setAttribute("class","");
195       var className = "";
196     if (((x-1) % 2) == 0) {
197       className = "table-alt-row";
198     } else {
199       className = "table-row";
200     }
201     if (currentRow.ctdlNewMsg) {
202       className += " new_message";
203     }
204     currentRow.className = className;
205     /* Using element.onclick is evil, but until IE 
206        supports addEventListener, it is much faster
207        than prototype observe */
208     currentRow.onclick = CtdlMessageListClick;
209     currentRow.ctdlDnDElement = summaryViewDragAndDropHandler;
210     currentRow.ctdlRowId = x;
211     fragment.appendChild(currentRow);
212     } catch (e) {
213       alert("Exception" + e);
214     }
215   }
216   message_view.appendChild(fragment);
217   var end = new Date();
218     var delta = end.getTime() - start.getTime();
219     WCLog("resortAndDisplay sort and append: " + delta);
220   ApplySorterToggle();
221   normalizeHeaderTable();
222 }
223 function sortRowsByDateAscending(a, b) {
224   var dateOne = a.ctdlDate;
225   var dateTwo = b.ctdlDate;
226   return (dateOne - dateTwo);
227 }
228 function sortRowsByDateDescending(a, b) {
229   var dateOne = a.ctdlDate;
230   var dateTwo = b.ctdlDate;
231   return (dateTwo - dateOne);
232 }
233
234 function sortRowsBySubjectAscending(a, b) {
235   var subjectOne = getTextContent(a.getElementsByTagName("td")[0]).toLowerCase();
236   var subjectTwo = getTextContent(b.getElementsByTagName("td")[0]).toLowerCase();
237   return strcmp(subjectOne, subjectTwo);
238 }
239
240 function sortRowsBySubjectDescending(a, b) {
241   var subjectOne = getTextContent(a.getElementsByTagName("td")[0]).toLowerCase();
242   var subjectTwo = getTextContent(b.getElementsByTagName("td")[0]).toLowerCase();
243   return strcmp(subjectTwo, subjectOne);
244 }
245
246 function sortRowsByFromAscending(a, b) {
247   var fromOne = getTextContent(a.getElementsByTagName("td")[1]).toLowerCase();
248   var fromTwo = getTextContent(b.getElementsByTagName("td")[1]).toLowerCase();
249   return strcmp(fromOne, fromTwo);
250 }
251
252 function sortRowsByFromDescending(a, b) {
253   var fromOne = getTextContent(a.getElementsByTagName("td")[1]).toLowerCase();
254   var fromTwo = getTextContent(b.getElementsByTagName("td")[1]).toLowerCase();
255   return strcmp(fromTwo, fromOne);
256 }
257
258 function CtdlMessageListClick(evt) {
259   /* Since element.onload is used here, test to see if evt is defined */
260   var event = evt ? evt : window.event; 
261   var target = event.target ? event.target: event.srcElement; // and again..
262   var parent = target.parentNode;
263   var msgId = parent.ctdlMsgId;
264   // If the ctrl key modifier wasn't used, unmark all rows and load the message
265   if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
266     unmarkAllRows();
267     markedRowId = parent.ctdlRowId;
268     document.getElementById("preview_pane").innerHTML = "";
269     new Ajax.Updater('preview_pane', 'msg/'+msgId, {method: 'get'});
270     markRow(parent);
271     new Ajax.Request('ajax_servcmd', {
272       method: 'post',
273           parameters: 'g_cmd=SEEN ' + msgId + '|1',
274           onComplete: CtdlMarkRowAsRead(parent)});
275   // If the shift key modifier is used, mark a range...
276   } else if (event.button != 2 && event.shiftKey) {
277     markRow(parent);
278     var rowId = parent.ctdlRowId;
279     var startMarkingFrom = 0;
280     var finish = 0;
281     if (rowId > markedRowId) {
282       startMarkingFrom = markedRowId+1;
283       finish = rowId;
284     } else if (rowId < markedRowId) {
285       startMarkingFrom = rowId+1;
286       finish = markedRowId;
287     } 
288     for(var x = startMarkingFrom; x<finish; x++) {
289       WCLog("Marking row "+x);
290       markRow(rowArray[x]);
291     }
292   // If the ctrl key modifier is used, toggle one message
293   } else if (event.button != 2 && (event.ctrlKey || event.altKey)) {
294     if (parent.ctdlMarked == true) {
295       unmarkRow(parent);
296     }
297     else {
298       markRow(parent);
299     }
300   }
301 }
302 function CtdlMarkRowAsRead(rowElement) {
303   var classes = rowElement.className;
304   classes = classes.replace("new_message","");
305   rowElement.className = classes;
306 }
307 function ApplySort(event) {
308   var target = event.target;
309   var sortId = target.id;
310   removeOldSortClass();
311   currentSorterToggle = target;
312   var sortModes = getSortMode(target); // returns [[key, func],[key,func]]
313   var sortModeToUse = null;
314   if (currentSortMode[0] == sortModes[0][0]) {
315     sortModeToUse = sortModes[1];
316   } else {
317     sortModeToUse = sortModes[0];
318   }
319   currentSortMode = sortModeToUse;
320   if (is_safe_mode) {
321     getMessages(); // in safe mode, we load from server already sorted
322   } else {
323   resortAndDisplay(sortModeToUse[1]);
324   }
325 }
326 function getSortMode(toggleElem) {
327   var forward = null;
328   var reverse = null;
329   for(var key in toggles) {
330     var kr = (key.charAt(0) == 'r');
331     if (toggles[key] == toggleElem && !kr) {
332       forward = [key, sortModes[key]];
333     } else if (toggles[key] == toggleElem && kr) {
334       reverse = [key, sortModes[key]];
335     }
336   }
337   return [forward, reverse];
338 }
339 function removeOldSortClass() {
340   if (currentSorterToggle) {
341     var classes = currentSorterToggle.className;
342     /* classes = classes.replace("current_sort_mode","");
343     classes = classes.replace("sort_ascending","");
344     classes = classes.replace("sort_descending",""); */
345     currentSorterToggle.className = "";
346   }
347 }
348 function markRow(row) {
349   var msgId = row.ctdlMsgId;
350   row.className = row.className += " marked_row";
351   row.ctdlMarked = true;
352   currentlyMarkedRows[msgId] = row;
353 }
354 function unmarkRow(row) {
355   var msgId = row.ctdlMsgId;
356   row.className = row.className.replace("marked_row","");
357   row.ctdlMarked = false;
358   delete currentlyMarkedRows[msgId];
359 }
360 function unmarkAllRows() {
361   for(msgId in currentlyMarkedRows) {
362     unmarkRow(currentlyMarkedRows[msgId]);
363   }
364 }
365 function deleteAllMarkedRows() {
366   for(msgId in currentlyMarkedRows) {
367     var row = currentlyMarkedRows[msgId];
368     var rowArrayId = row.ctdlRowId;
369     row.parentNode.removeChild(row);
370     delete currentlyMarkedRows[msgId];
371     delete rowArray[rowArrayId];
372   }
373   // Now we have to reconstruct rowarray as the array length has changed */
374   var newRowArray = new Array();
375   var x=0;
376   for(var i=0; i<rowArray.length; i++) {
377     var currentRow = rowArray[i];
378     if (currentRow != null) {
379       newRowArray[x] = currentRow;
380       x++;
381     }
382   }
383   rowArray = newRowArray;
384   resortAndDisplay(null);
385 }
386
387 function deleteAllSelectedMessages() {
388     for(msgId in currentlyMarkedRows) {
389       if (!room_is_trash) {
390       new Ajax.Request('ajax_servcmd', 
391                        {method: 'post',
392                            parameters: 'g_cmd=MOVE ' + msgId + '|_TRASH_|0'
393                            });
394       } else {
395         new Ajax.Request('ajax_servcmd', {method: 'post',
396               parameters: 'g_cmd=DELE '+msgId});
397       }
398     }
399     document.getElementById("preview_pane").innerHTML = "";
400     deleteAllMarkedRows();
401 }
402
403 function CtdlMessageListKeyUp(event) {
404         var key = event.which;
405         if (key == 46) { // DELETE
406                 deleteAllSelectedMessages();
407         }
408 }
409
410 function clearMessage(msgId) {
411   var row = document.getElementById('msg_'+msgId);
412   row.parentNode.removeChild(row);
413   delete currentlyMarkedRows[msgId];
414 }
415
416 function summaryViewContextMenu() {
417   if (!exitedMouseDown) {
418     var contextSource = document.getElementById("listViewContextMenu");
419     CtdlSpawnContextMenu(mouseDownEvent, contextSource);
420   }
421 }
422
423 function summaryViewDragAndDropHandler() {
424   var element = document.createElement("div");
425   var msgList = document.createElement("ul");
426   element.appendChild(msgList);
427   for(msgId in currentlyMarkedRows) {
428     msgRow = currentlyMarkedRows[msgId];
429     var subject = getTextContent(msgRow.getElementsByTagName("td")[0]);
430     var li = document.createElement("li");
431     msgList.appendChild(li);
432     setTextContent(li, subject);
433     li.ctdlMsgId = msgId;
434   }
435   return element;
436 }
437
438 var saved_y = 0;
439 function CtdlResizeMouseDown(event) {
440   $(document).observe('mousemove', CtdlResizeMouseMove);
441   $(document).observe('mouseup', CtdlResizeMouseUp);
442   saved_y = event.clientY;
443 }
444
445 function sizePreviewPane() {
446   var preview_pane = document.getElementById("preview_pane");
447   var summary_view = document.getElementById("summary_view");
448   var banner = document.getElementById("banner");
449   var message_list_hdr = document.getElementById("message_list_hdr");
450   var content = $('global');  // we'd like to use prototype methods here
451   var childElements = content.childElements();
452   var sizeOfElementsAbove = 0;
453   var heightOfViewPort = document.viewport.getHeight() // prototypejs method
454   var bannerHeight = banner.offsetHeight;
455   var contentViewPortHeight = heightOfViewPort-banner.offsetHeight-message_list_hdr.offsetHeight;
456   contentViewPortHeight = 0.98 * contentViewPortHeight; // leave some error
457   // Set summary_view to 20%;
458   var summary_height = ctdlLocalPrefs.readPref("svheight");
459   if (summary_height == null) {
460     summary_height = 0.20 * contentViewPortHeight;
461   }
462   // Set preview_pane to the remainder
463   var preview_height = contentViewPortHeight - summary_height;
464   
465   summary_view.style.height = (summary_height)+"px";
466   preview_pane.style.height = (preview_height)+"px";
467 }
468 function CtdlResizeMouseMove(event) {
469   var clientX = event.clientX;
470   var clientY = event.clientY;
471   var summary_view = document.getElementById("summary_view");
472   var summaryViewHeight = summary_view.offsetHeight;
473   var increment = clientY-saved_y;
474   var summary_view_height = increment+summaryViewHeight;
475   summary_view.style.height = (summary_view_height)+"px";
476   // store summary view height 
477   ctdlLocalPrefs.setPref("svheight",summary_view_height);
478   var msglist = document.getElementById("preview_pane");
479   var msgListHeight = msglist.offsetHeight;
480   msglist.style.height = (msgListHeight-increment)+"px";
481   saved_y = clientY;
482   /* For some reason the grippy doesn't work without position: absolute
483      so we need to set its top pos manually all the time */
484   var resize = document.getElementById("resize_msglist");
485   var resizePos = resize.offsetTop;
486   resize.style.top = (resizePos+increment)+"px";
487 }
488 function CtdlResizeMouseUp(event) {
489   $(document).stopObserving('mousemove', CtdlResizeMouseMove);
490   $(document).stopObserving('mouseup', CtdlResizeMouseUp);
491 }
492 function ApplySorterToggle() {
493   var className = currentSorterToggle.className;
494   className += " current_sort_mode";
495   if (currentSortMode[1] == sortRowsByDateDescending ||
496       currentSortMode[1] == sortRowsBySubjectDescending ||
497       currentSortMode[1] == sortRowsByFromDescending) {
498     className += " sort_descending";
499   } else {
500     className += " sort_ascending";
501   }
502   currentSorterToggle.className = className;
503 }
504 /** Hack to make the header table line up with the data */
505 function normalizeHeaderTable() {
506   var message_list_hdr = document.getElementById("message_list_hdr");
507   var summary_view = document.getElementById("summary_view");
508   var resize_msglist = document.getElementById("resize_msglist");
509   var headerTable = message_list_hdr.getElementsByTagName("table")[0];
510   var dataTable = summary_view.getElementsByTagName("table")[0];
511   var dataTableWidth = dataTable.offsetWidth;
512   headerTable.style.width = dataTableWidth+"px";
513 }
514
515 function setupPageSelector() {
516   var summpage = document.getElementById("summpage");
517   var select_page = document.getElementById("selectpage");
518   summpage.innerHTML = "";
519   if (is_safe_mode) {
520     WCLog("unhiding parent page");
521     select_page.className = "";
522   } else {
523     return;
524   }
525   var pages = nummsgs / 500;
526   for(var i=0; i<pages; i++) {
527     var opt = document.createElement("option");
528     var startmsg = i * 500;
529     opt.setAttribute("value",startmsg);
530     if (currentPage == i) {
531       opt.setAttribute("selected","selected");
532     }
533     opt.appendChild(document.createTextNode((i+1)));
534     summpage.appendChild(opt);
535   }
536 }
537 function getPage(event) {
538   var target = event.target;
539   startmsg = target.options.item(target.selectedIndex).value;
540   currentPage = target.selectedIndex;
541   //query = ""; // We are getting a page from the _entire_ msg list, don't query
542   getMessages();
543 }
544 function takeOverSearchOMatic() {
545   var searchForm = document.getElementById("searchomatic").getElementsByTagName("form")[0];
546   // First disable the form post
547   searchForm.setAttribute("action","javascript:void();");
548   searchForm.removeAttribute("method");
549   $(searchForm).observe('submit', doSearch);
550 }
551 function doSearch() {
552   query = document.getElementById("srchquery").value;
553   getMessages();
554   return false;
555 }