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